docs: LiveKit-Token-Minting-Migrationsplan (apiSecret raus aus Client)
Fertiger Umsetzungsplan fuer den offenen M0-Punkt: Token-Endpoint am Dashboard-Server mintet LiveKit-JWTs server-seitig (Matrix-whoami-Auth, stdlib-HMAC), Client holt nur das JWT. Braucht echten Call-Test -> nur Plan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1228,3 +1228,6 @@
|
|||||||
2026-07-04 04:02 [Edit] /home/steggi/pyramid/lib/widgets/settings/settings_encryption.dart
|
2026-07-04 04:02 [Edit] /home/steggi/pyramid/lib/widgets/settings/settings_encryption.dart
|
||||||
2026-07-04 04:03 [Edit] /home/steggi/pyramid/ROADMAP.md
|
2026-07-04 04:03 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||||
2026-07-04 04:03 [Edit] /home/steggi/pyramid/PROGRESS.md
|
2026-07-04 04:03 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 04:06 [Write] /home/steggi/pyramid/docs/LIVEKIT_TOKEN_MIGRATION.md
|
||||||
|
2026-07-04 04:06 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||||
|
2026-07-04 04:06 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
|||||||
+22
@@ -13,6 +13,28 @@ Schritt (und beim Abbruch mitten im Schritt den Zwischenstand). Format:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-07-04 – LiveKit-Token-Minting: Migrationsplan geschrieben (Prep, kein Code)
|
||||||
|
|
||||||
|
**Erledigt:** Für den offenen M0-Punkt „LiveKit-Token server-seitig minten"
|
||||||
|
(letzter Client-eingebetteter Secret nach der Diagnose-Entkopplung) den fertigen
|
||||||
|
Umsetzungsplan `docs/LIVEKIT_TOKEN_MIGRATION.md` geschrieben – gegen den echten
|
||||||
|
Client-Code (`livekit_token.dart`, `livekit_call_manager.dart:154`) und die echte
|
||||||
|
Pi-Konfig (`livekit.yaml`, `server.py`, Continuwuity `whoami`). Kern: Token-Route
|
||||||
|
`POST /api/livekit-token` am bestehenden Dashboard-Server, die den Matrix-Access-Token
|
||||||
|
per `whoami` prüft und daraus die LiveKit-Identität ableitet (kein Spoofing), JWT
|
||||||
|
per stdlib-HMAC mintet (kein neues Paket); Client holt nur noch das fertige JWT und
|
||||||
|
`apiSecret` verschwindet aus dem App-Code. Analog zum bewährten
|
||||||
|
`SQLCIPHER_MIGRATION.md`-Muster.
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** Umsetzung braucht echten Call-Test auf einem Gerät
|
||||||
|
(Calls „heilig", auf dem Pi kein GUI) – daher bewusst nur der Plan, kein Code.
|
||||||
|
Danach Secret-Rotation zusammen mit einem Release. Verbleibende M0-Secrets
|
||||||
|
(Admin-Token, Gitea-, Giphy-Token) brauchen Bernds Konten-Zugriff.
|
||||||
|
|
||||||
|
**Stolperfallen:** Keine – reine Doku.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-07-04 – Admin-Token aus dem Client entfernt (E2EE-Diagnose entkoppelt)
|
## 2026-07-04 – Admin-Token aus dem Client entfernt (E2EE-Diagnose entkoppelt)
|
||||||
|
|
||||||
**Erledigt:** Den kritischsten Teil des Secret-Befunds (k) wirklich behoben, statt
|
**Erledigt:** Den kritischsten Teil des Secret-Befunds (k) wirklich behoben, statt
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ Punkte abhaken (`[x]`), wenn erledigt UND in `PROGRESS.md` protokolliert.
|
|||||||
`apiSecret`, Giphy-Key. Alte jeweils widerrufen.
|
`apiSecret`, Giphy-Key. Alte jeweils widerrufen.
|
||||||
- **LiveKit-Token server-seitig minten:** kleiner Token-Endpoint auf dem Pi, damit
|
- **LiveKit-Token server-seitig minten:** kleiner Token-Endpoint auf dem Pi, damit
|
||||||
`apiSecret` aus `lib/core/livekit_token.dart` verschwindet (App holt nur das JWT).
|
`apiSecret` aus `lib/core/livekit_token.dart` verschwindet (App holt nur das JWT).
|
||||||
|
**Umsetzungsplan liegt fertig in `docs/LIVEKIT_TOKEN_MIGRATION.md`** (2026-07-04,
|
||||||
|
gegen Code + Pi-Konfig geschrieben) – braucht echten Call-Test auf einem Gerät.
|
||||||
- **E2EE-Diagnose-Upload: ERLEDIGT (2026-07-04).** Der Dashboard-Server auf dem Pi
|
- **E2EE-Diagnose-Upload: ERLEDIGT (2026-07-04).** Der Dashboard-Server auf dem Pi
|
||||||
(`/home/steggi/matrix/server.py`) benutzte für `/api/e2ee-diagnostics` denselben
|
(`/home/steggi/matrix/server.py`) benutzte für `/api/e2ee-diagnostics` denselben
|
||||||
String wie den Matrix-Admin-Token – deshalb steckte er im Client. Jetzt hat das
|
String wie den Matrix-Admin-Token – deshalb steckte er im Client. Jetzt hat das
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
# LiveKit-Token server-seitig minten (apiSecret aus dem Client entfernen)
|
||||||
|
|
||||||
|
**Status:** geplant, NICHT umgesetzt (braucht echten Call-Test auf einem Gerät –
|
||||||
|
Calls sind laut CLAUDE.md „heilig"). Dieser Plan ist gegen den echten Code und die
|
||||||
|
echte Pi-Konfiguration geschrieben (2026-07-04), damit ein PC-Termin direkt starten
|
||||||
|
kann. Analog zu `docs/SQLCIPHER_MIGRATION.md`.
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
`lib/core/livekit_token.dart` mintet die LiveKit-JWTs **im Client**. Dafür muss das
|
||||||
|
LiveKit-`apiSecret` im App-Code liegen (`livekit_token.dart:6`) – d. h. jeder
|
||||||
|
App-Nutzer (auch Uta) trägt das SFU-Secret im Gerät. Wer es hat, kann sich für
|
||||||
|
**jeden** Raum und **jede** Identität ein Beitritts-Token ausstellen. Das ist ein
|
||||||
|
Architekturproblem, kein bloßer Repo-Leak: Selbst nach einem History-Rewrite bliebe
|
||||||
|
das Secret im ausgelieferten Binary.
|
||||||
|
|
||||||
|
Aktueller Fluss (`lib/core/livekit_call_manager.dart:154`):
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final token = LiveKitTokenGenerator.generate(
|
||||||
|
roomName: roomName,
|
||||||
|
identity: identity,
|
||||||
|
displayName: identity,
|
||||||
|
ttlSeconds: 21600, // 6 h
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Erzeugtes JWT (HS256, `livekit_token.dart:14-33`): Grants `video` mit
|
||||||
|
`roomJoin/room/canPublish/canSubscribe/canPublishData`, `metadata` = Anzeigename,
|
||||||
|
`iss` = apiKey (`LKMatrixPi`), `sub` = identity, `exp` = jetzt+ttl.
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Das `apiSecret` liegt **nur noch auf dem Pi**. Der Client holt sich für einen
|
||||||
|
konkreten Raum ein fertiges JWT von einem kleinen Token-Endpoint und übergibt es
|
||||||
|
unverändert an `Room.connect`.
|
||||||
|
|
||||||
|
## Server-Seite (Pi)
|
||||||
|
|
||||||
|
Es gibt bereits einen schlanken Dashboard-Server ohne Fremd-Abhängigkeiten:
|
||||||
|
`/home/steggi/matrix/server.py` (stdlib `http.server`, läuft als
|
||||||
|
`matrix-stats.service` auf `0.0.0.0:8080`, nach außen `dashboard.steggi-matrix.work`).
|
||||||
|
Der Token-Endpoint kann dort als weitere Route `POST /api/livekit-token` andocken –
|
||||||
|
kein neuer Dienst nötig. Das LiveKit-`apiSecret` steht schon auf dem Pi in
|
||||||
|
`/home/steggi/matrix/livekit.yaml` (`keys: LKMatrixPi: <secret>`); der Server liest
|
||||||
|
es von dort, statt es erneut zu hinterlegen (eine Quelle der Wahrheit).
|
||||||
|
|
||||||
|
**Wichtig – Authentifizierung:** Der Endpoint darf NICHT anonym Tokens ausstellen
|
||||||
|
(sonst ist nichts gewonnen). Der Client schickt seinen **Matrix-Access-Token** mit;
|
||||||
|
der Server prüft ihn gegen Continuwuity und leitet daraus die Identität ab:
|
||||||
|
|
||||||
|
1. `POST /api/livekit-token`, Body `{ "room": "<roomName>", "matrix_token": "<access_token>" }`.
|
||||||
|
2. Server ruft `GET http://127.0.0.1:6167/_matrix/client/v3/account/whoami`
|
||||||
|
mit `Authorization: Bearer <matrix_token>` auf → liefert `user_id`
|
||||||
|
(401 → Endpoint gibt 401 zurück, kein Token).
|
||||||
|
3. `identity` = `user_id` (NICHT vom Client wählbar – verhindert Identitäts-Spoofing).
|
||||||
|
4. Optional, aber empfohlen: Raummitgliedschaft prüfen (`/_matrix/client/v3/rooms/
|
||||||
|
{roomId}/joined_members` oder der Matrix-Raum, zu dem der Voice-Channel gehört) –
|
||||||
|
nur ausstellen, wenn der Nutzer wirklich Mitglied ist. Für Phase 1 reicht die
|
||||||
|
whoami-Authentifizierung; Mitgliedschaftsprüfung als Härtung nachziehen.
|
||||||
|
5. Server mintet das LiveKit-JWT **mit denselben Grants wie heute** und gibt
|
||||||
|
`{ "token": "<jwt>", "url": "wss://livekit.steggi-matrix.work" }` zurück.
|
||||||
|
|
||||||
|
**JWT-Minting ohne Fremd-Paket (stdlib genügt):** LiveKit-Tokens sind HS256-JWTs.
|
||||||
|
Mit `hmac`/`hashlib`/`base64`/`json` aus der stdlib:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import base64, hmac, hashlib, json, time
|
||||||
|
|
||||||
|
def _b64url(b): return base64.urlsafe_b64encode(b).rstrip(b"=")
|
||||||
|
|
||||||
|
def mint_livekit(api_key, api_secret, identity, room, name, ttl=21600):
|
||||||
|
now = int(time.time())
|
||||||
|
header = {"alg": "HS256", "typ": "JWT"}
|
||||||
|
payload = {
|
||||||
|
"iss": api_key, "sub": identity, "name": name,
|
||||||
|
"nbf": now, "exp": now + ttl, "metadata": name,
|
||||||
|
"video": {"roomJoin": True, "room": room,
|
||||||
|
"canPublish": True, "canSubscribe": True,
|
||||||
|
"canPublishData": True},
|
||||||
|
}
|
||||||
|
segs = [_b64url(json.dumps(header, separators=(",", ":")).encode()),
|
||||||
|
_b64url(json.dumps(payload, separators=(",", ":")).encode())]
|
||||||
|
signing_input = b".".join(segs)
|
||||||
|
sig = hmac.new(api_secret.encode(), signing_input, hashlib.sha256).digest()
|
||||||
|
return (signing_input + b"." + _b64url(sig)).decode()
|
||||||
|
```
|
||||||
|
|
||||||
|
Verifikation lokal auf dem Pi (kein Gerät nötig): geminetes Token gegen den
|
||||||
|
laufenden LiveKit prüfen, z. B. per LiveKit-CLI `lk` oder indem man das JWT auf
|
||||||
|
jwt.io-Art dekodiert und Claims/Signatur vergleicht. Zusätzlich Gegentest mit dem
|
||||||
|
**alten** Client-Pfad: dasselbe Token muss `Room.connect` akzeptieren.
|
||||||
|
|
||||||
|
## Client-Seite
|
||||||
|
|
||||||
|
- `lib/core/livekit_token.dart`: `apiKey`/`apiSecret` **entfernen**. Die Klasse wird
|
||||||
|
entweder gelöscht oder zu einem dünnen HTTP-Client
|
||||||
|
`Future<String> fetchLiveKitToken({required String room, required String matrixToken})`,
|
||||||
|
der `POST https://dashboard.steggi-matrix.work/api/livekit-token` aufruft und das
|
||||||
|
`token`-Feld zurückgibt. `http`-Paket ist schon Abhängigkeit (siehe
|
||||||
|
`update_checker.dart`).
|
||||||
|
- `lib/core/livekit_call_manager.dart:154`: `LiveKitTokenGenerator.generate(...)`
|
||||||
|
→ `await fetchLiveKitToken(room: roomName, matrixToken: matrixClient.accessToken)`.
|
||||||
|
Der Manager hat den Matrix-Client bereits (`_matrixClient`, Zeile 149) – der
|
||||||
|
Access-Token ist also verfügbar. `identity`/`displayName` werden nicht mehr vom
|
||||||
|
Client bestimmt (Server leitet Identität aus whoami ab); den bisherigen
|
||||||
|
`identity`-Parameter entsprechend zurückbauen.
|
||||||
|
- Fehlerpfade: Netzfehler / 401 / 403 sauber als Call-Fehler anzeigen
|
||||||
|
(`error = ...; notifyListeners()`), NICHT still schlucken – sonst „Call verbindet
|
||||||
|
nicht" ohne Hinweis. TTL bleibt 21600 s (6 h), Erneuerung wie bisher beim
|
||||||
|
Neu-Beitreten.
|
||||||
|
|
||||||
|
## Rotation (Pflicht, danach)
|
||||||
|
|
||||||
|
Das alte `apiSecret` `rYUT2PRa…` ist geleakt (Client + Git-History). Nach der
|
||||||
|
Umstellung:
|
||||||
|
|
||||||
|
1. Neues `apiSecret` in `/home/steggi/matrix/livekit.yaml` (`keys:`) setzen.
|
||||||
|
2. LiveKit-Container neu starten
|
||||||
|
(`docker compose -f /home/steggi/matrix/docker-compose.yml restart livekit`).
|
||||||
|
3. `server.py` liest das Secret aus `livekit.yaml` – kein zweiter Ort zu pflegen.
|
||||||
|
4. Alte Clients (Utas installierte APK) minten mit dem alten Secret → deren
|
||||||
|
selbst-gemintete Tokens werden nach der Rotation abgelehnt. Deshalb Rotation
|
||||||
|
**zusammen mit einem neuen Release** ausrollen, das den neuen Fetch-Pfad nutzt.
|
||||||
|
|
||||||
|
## Reihenfolge / Testplan (PC + Gerät)
|
||||||
|
|
||||||
|
1. `server.py`-Route bauen, lokal auf dem Pi verifizieren (whoami-Auth greift,
|
||||||
|
geminetes Token dekodiert korrekt, `Room.connect` akzeptiert es).
|
||||||
|
2. Client umstellen, `flutter analyze` sauber, `flutter test` grün.
|
||||||
|
3. **Auf echtem Gerät** (Calls heilig, kein Headless-Test möglich): Voice-Channel
|
||||||
|
beitreten, Audio/Video/Screenshare, Verlassen/Wieder-Beitreten, zweiter
|
||||||
|
Teilnehmer. Erst wenn das steht: Secret rotieren + Release.
|
||||||
|
4. Danach ROADMAP-M0-Punkt „LiveKit-Token server-seitig minten" abhaken und den
|
||||||
|
Rotations-Teil des Secret-Punkts als erledigt markieren.
|
||||||
|
|
||||||
|
## Warum nicht jetzt (auf dem Pi) gemacht
|
||||||
|
|
||||||
|
Die Server-Route ließe sich hier bauen und headless testen – aber der zugehörige
|
||||||
|
Client-Umbau tauscht den **heiligen Call-Pfad** aus und ist auf diesem Pi ohne
|
||||||
|
GUI/Gerät nicht praktisch prüfbar. Einen halben, nicht end-to-end verifizierbaren
|
||||||
|
Umbau am Call-Pfad zu stapeln verbietet die CLAUDE.md-Leitplanke („keine
|
||||||
|
Verhaltensänderung ohne Not", Calls heilig). Deshalb hier nur der fertige Plan.
|
||||||
Reference in New Issue
Block a user