Pages
Contents
Chat Identity – Concept
Status: CONCEPT · Erstellt April 2026 Übergeordnet:
TOOL_chat_roadmap.mdVerwandt:TOOL_chat_concept-device-sync.md,TOOL_chat_adr-003-localstorage-keys.mdVerwandt:@xpulse/crypto
Grundsätze
Privacy First – keine Identitätsdaten auf dem Server, niemals. Datenverlust vermeiden – jede Entscheidung wird danach bewertet. Technische Nachvollziehbarkeit – schafft Vertrauen.
Warum ein eigenes Konzept?
Identität ist die Wurzel von allem in xPulse Chat – Profil, Nachrichten, Sync, Devices, Export. Ohne ein stabiles, klar definiertes Identitätsmodell können Daten verloren gehen, Peers sich nicht wiedererkennen, oder verschiedene User auf demselben Gerät sich gegenseitig stören.
Dieses Konzept legt fest:
- Was
userIdundclientIdsind - Wie sie generiert werden
- Warum sie stabil und reproduzierbar sind
- Was bei Datenverlust passiert
Die zwei Identitäten
| userId → identifiziert den USER – geräteübergreifend stabil |
| clientId → identifiziert das GERÄT – userübergreifend stabil |
Ein User kann mehrere Geräte haben. Ein Gerät kann mehrere User haben. Beide Identitäten sind voneinander unabhängig.
| Gerät (clientId: abc...) |
| ├── User A (userId: xyz...) |
| └── User B (userId: def...) |
| User A (userId: xyz...) |
| ├── iPhone (clientId: abc...) |
| └── MacBook (clientId: ghi...) |
Keypair-Prinzip (wie SSH)
Sowohl userId als auch clientId basieren auf dem gleichen Prinzip:
einem kryptografischen Schlüsselpaar – genau wie ein SSH-Key.
Was ist ein Keypair?
| Private Key = dein geheimer Schlüssel |
| → verlässt nie das Gerät |
| → wird niemals übertragen |
| → damit werden Nachrichten "unterschrieben" |
| Public Key = dein öffentliches Schloss |
| → kann jedem gegeben werden |
| → Peers kennen dich dadurch |
| → daraus wird die ID abgeleitet |
Wer deinen publicKey kennt, kann verifizieren dass eine Nachricht
wirklich von dir kommt. Wer deinen privateKey nicht hat, kann sich
nicht als du ausgeben.
Warum ist die ID dadurch reproduzierbar?
| privateKey existiert → publicKey immer ableitbar → ID immer gleich ✅ |
| privateKey verloren → neue ID – wie ein neuer SSH-Key ⚠️ |
Solange der privateKey sicher in localStorage liegt, ist die ID stabil –
auf ewig, ohne Server, ohne Koordination.
clientId – Geräte-Identität
Generierung
| Erster Start auf diesem Gerät → |
| Ed25519 Keypair generieren (Web Crypto API) |
| privateKey → xpulse_identity_client_private_key (localStorage, AES-GCM verschlüsselt) |
| publicKey → SHA-256(publicKey) = clientId |
| → xpulse_identity_client_id (localStorage) |
| → xpulse_identity_client_public_key (localStorage) |
Stabilität
| App startet → |
| xpulse_identity_client_id vorhanden? → |
| JA → laden, verwenden – FERTIG, nie neu generieren |
| NEIN → erster Start → Keypair generieren → speichern |
Regel: clientId wird einmalig generiert und nie überschrieben.
Logout, Login, User-Wechsel – clientId bleibt immer gleich.
Sie gehört dem Gerät, nicht dem User.
Wiederherstellung nach Datenverlust
Der privateKey steckt im verschlüsselten Backup (Export).
Nach localStorage clear:
| Backup importieren → privateKey wiederhergestellt |
| → clientId reproduzierbar → Peers kennen das Gerät noch ✅ |
Ohne Backup: neue clientId → einmaliges Re-Pairing mit allen Peers nötig.
Das ist der einzig akzeptable Datenverlust-Fall – und er ist kommunizierbar:
"Backup sichern = Geräte-Identität sichern."
userId – User-Identität
Generierung
| Erster Login / Registrierung → |
| Ed25519 Keypair generieren (Web Crypto API) |
| Eingaben: login + password + timestamp + UUID v4 |
| salt → crypto.getRandomValues(32 bytes) |
| → xpulse_identity_{userId}_salt (localStorage) |
| privateKey → xpulse_identity_{userId}_private_key (localStorage, AES-GCM verschlüsselt |
| mit PBKDF2(password + salt)) |
| publicKey → SHA-256(publicKey) = userId |
| → xpulse_identity_{userId}_public_key (localStorage) |
userId wird aus einem Keypair abgeleitet – nicht direkt aus Login/Passwort.
Das bedeutet: Passwort-Änderung ändert die userId nicht.
Bei Passwort-Änderung: privateKey entschlüsseln → mit neuem Passwort neu verschlüsseln.
Stabilität – oberstes Gebot
| ⚠️ userId DARF NIEMALS neu generiert werden solange Daten existieren. |
| ⚠️ Datenverlust durch userId-Änderung ist inakzeptabel. |
| ⚠️ Vor JEDEM Breaking Change: prüfen ob userId migriert werden muss. |
| App startet → |
| xpulse_identity_{userId}_public_key vorhanden? → |
| JA → laden, verwenden – FERTIG, nie neu generieren |
| NEIN → erster Start → Keypair + Salt generieren → speichern |
Mehrere User auf einem Gerät
Jeder User hat seine eigene userId, seinen eigenen privateKey und sein eigenes salt.
Alle localStorage-Daten sind durch userId isoliert:
| xpulse_identity_{userIdA}_private_key ← nur User A |
| xpulse_identity_{userIdB}_private_key ← nur User B |
| xpulse_chat_{userIdA}_profile ← nur User A |
| xpulse_chat_{userIdB}_profile ← nur User B |
Logout User A → seine Daten bleiben unangetastet in localStorage. Login User B → komplett separate Datenwelt.
Zusammenspiel userId + clientId
| Nachricht gesendet: |
| senderId = userId ← wer hat geschrieben? |
| clientId = clientId ← von welchem Gerät? |
Peers adressieren Nachrichten an userId – sie kommen auf allen
verbundenen Geräten dieses Users an. clientId zeigt welches Gerät
gerade online ist.
| Chat mit Johnny: |
| userId: xyz... ← immer gleich, egal welches Gerät |
| clientId: abc... ← iPhone |
| clientId: ghi... ← MacBook (wenn auch verbunden) |
Export / Backup
Das Backup enthält:
clientId(public) – Geräte-IDclient privateKey(verschlüsselt) – für Geräte-WiederherstellunguserId(public) – User-IDuser privateKey(verschlüsselt) – für User-Wiederherstellung- Alle Chat-Daten, Profil, Graveyard etc.
Verschlüsselung der Private Keys im Backup:
Mit einem vom User gewählten Backup-Passwort via @xpulse/crypto (AES-GCM).
Ohne das Passwort sind die Keys im Backup wertlos.
Datenverlust-Szenarien
| Szenario | Konsequenz | Wiederherstellung |
|---|---|---|
| localStorage clear | clientId + userId weg | Backup importieren → alles OK |
| Backup vorhanden, kein localStorage | Neuer Start | Backup importieren → alles OK |
| Kein Backup, localStorage clear | Neue Identität | Re-Pairing nötig, Chat-History weg |
| Neues Gerät, Backup vorhanden | Backup importieren | clientId neu, userId gleich → Peers kennen User noch |
| Neues Gerät, kein Backup | Device-Pairing mit bestehendem Gerät | userId übertragen → Peers kennen User noch |
Algorithmus
| Ed25519 verfügbar (moderne Browser)? → verwenden ✅ |
| Nicht verfügbar? → Fallback: ECDSA (P-256) |
| + Info-Hinweis in UI: |
| "Dein Browser verwendet einen älteren |
| Verschlüsselungsstandard. |
| Ein Browser-Update wird empfohlen." |
Ed25519 ist der bevorzugte Algorithmus – schneller, sicherer, kleinere Keys. ECDSA (P-256) ist der bewährte Fallback mit breitem Browser-Support. Der Hinweis beruhigt nicht – er informiert sachlich und professionell.
privateKey Verschlüsselung in localStorage
Der privateKey liegt niemals im Klartext in localStorage.
Er wird mit AES-GCM via @xpulse/crypto verschlüsselt – genau wie alle
anderen sensiblen Daten auch.
| Verschlüsselungs-Key = PBKDF2(loginPassword + userSalt) → encryptionKey |
| privateKey speichern: |
| AES-GCM encrypt(privateKey, encryptionKey) → localStorage |
| privateKey lesen: |
| salt laden → PBKDF2(loginPassword + salt) → encryptionKey |
| AES-GCM decrypt(encrypted, encryptionKey) → privateKey |
Das Salt ist user-spezifisch, einmalig generiert und nicht geheim –
es liegt offen in localStorage (xpulse_identity_{userId}_salt).
Nur das Login-Passwort muss geheim bleiben.
Auch wenn eine Browser-Extension den localStorage ausliest – ohne das
Login-Passwort ist der privateKey wertlos.
Passwort ändern:
→ privateKey entschlüsseln → mit neuem Passwort + Salt neu verschlüsseln → speichern.
Backup-Passwort
Beim Export wird ein Backup-Passwort gesetzt:
- Keine Mindestlänge erzwungen – auch kurze Passwörter sind erlaubt
- Stärke-Indikator (Balken) in der UI – informiert, zwingt nicht
- User ist mündig – die Entscheidung liegt bei ihm
| Backup-Passwort Stärke-Indikator: |
| < 8 Zeichen → schwach (roter Balken) |
| 8-12 Zeichen → mittel (gelber Balken) |
| > 12 Zeichen → stark (grüner Balken) |
Migration – Bestehende Installationen
Bestehende xPulse Chat Installationen haben noch keine Keypairs, keine
userId, kein Salt und noch die alten xp_* Key-Namespaces.
Diese Migration folgt GLOBAL_adr-015-migration-strategy.md – dreiphasig
mit Backup, Build Space und atomarem Switch.
Was migriert wird
| Alt → Neu |
| ──────────────────────────────────────────────────── |
| xp_* Keys → xpulse_chat_* / xpulse_identity_* |
| kein Keypair → Ed25519 Keypair generieren |
| kein userId → SHA-256(publicKey) = userId |
| kein Salt → crypto.getRandomValues(32) = salt |
| kein verschlüsselter PK → privateKey mit PBKDF2(pw + salt) verschlüsseln |
| Profil ohne neue Felder → migrateProfile() – fehlende Felder ergänzen |
| Messages ohne v / id etc. → migrateV0() im ChatConverter |
Pflicht-Checks (Dry Run)
Zusätzlich zu den globalen Checks aus ADR-015:
| ✓ Keypair generierbar und verifizierbar? |
| ✓ userId aus publicKey ableitbar? |
| ✓ Salt generiert? |
| ✓ privateKey verschlüsselbar und wieder entschlüsselbar? (Test-Runde) |
| ✓ Alle Chat-IDs aus altem Format gefunden? |
| ✓ Alle Messages pro Chat vollständig? |
| ✓ Profil vollständig migriert? |
| ✓ Alle Graveyard-Einträge übernommen? |
| ✓ Neue Key-Struktur konsistent mit ADR-003? |
Kritischer Hinweis
| ⚠️ userId wird bei dieser Migration NEU generiert. |
| ⚠️ Alle Chat-Daten werden unter die neue userId migriert. |
| ⚠️ Erst wenn ALLE Checks bestanden sind wird der Switch durchgeführt. |
| ⚠️ Bei Fehler: vollständiger Rollback, Original unangetastet. |
→ Vollständiger Migrationsprozess: GLOBAL_adr-015-migration-strategy.md
| Thema | Stand |
|---|---|
| Key-Rotation – kann ein User seinen Keypair erneuern? | TBD – komplex, später |