diff --git a/CHANGES.md b/CHANGES.md index bc7ac65..c9cae32 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1228,3 +1228,6 @@ 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/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 diff --git a/PROGRESS.md b/PROGRESS.md index cfb6314..5326131 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -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) **Erledigt:** Den kritischsten Teil des Secret-Befunds (k) wirklich behoben, statt diff --git a/ROADMAP.md b/ROADMAP.md index 292f5f0..0eb2a33 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -68,6 +68,8 @@ Punkte abhaken (`[x]`), wenn erledigt UND in `PROGRESS.md` protokolliert. `apiSecret`, Giphy-Key. Alte jeweils widerrufen. - **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). + **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 (`/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 diff --git a/docs/LIVEKIT_TOKEN_MIGRATION.md b/docs/LIVEKIT_TOKEN_MIGRATION.md new file mode 100644 index 0000000..f00a37a --- /dev/null +++ b/docs/LIVEKIT_TOKEN_MIGRATION.md @@ -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: `); 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.