# 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: `); 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": "", "matrix_token": "" }`. 2. Server ruft `GET http://127.0.0.1:6167/_matrix/client/v3/account/whoami` mit `Authorization: Bearer ` 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": "", "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 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.