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>
7.2 KiB
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):
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:
POST /api/livekit-token, Body{ "room": "<roomName>", "matrix_token": "<access_token>" }.- Server ruft
GET http://127.0.0.1:6167/_matrix/client/v3/account/whoamimitAuthorization: Bearer <matrix_token>auf → liefertuser_id(401 → Endpoint gibt 401 zurück, kein Token). identity=user_id(NICHT vom Client wählbar – verhindert Identitäts-Spoofing).- Optional, aber empfohlen: Raummitgliedschaft prüfen (
/_matrix/client/v3/rooms/ {roomId}/joined_membersoder 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. - 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:
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/apiSecretentfernen. Die Klasse wird entweder gelöscht oder zu einem dünnen HTTP-ClientFuture<String> fetchLiveKitToken({required String room, required String matrixToken}), derPOST https://dashboard.steggi-matrix.work/api/livekit-tokenaufruft und dastoken-Feld zurückgibt.http-Paket ist schon Abhängigkeit (sieheupdate_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/displayNamewerden nicht mehr vom Client bestimmt (Server leitet Identität aus whoami ab); den bisherigenidentity-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:
- Neues
apiSecretin/home/steggi/matrix/livekit.yaml(keys:) setzen. - LiveKit-Container neu starten
(
docker compose -f /home/steggi/matrix/docker-compose.yml restart livekit). server.pyliest das Secret auslivekit.yaml– kein zweiter Ort zu pflegen.- 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)
server.py-Route bauen, lokal auf dem Pi verifizieren (whoami-Auth greift, geminetes Token dekodiert korrekt,Room.connectakzeptiert es).- Client umstellen,
flutter analyzesauber,flutter testgrün. - 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.
- 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.