Messaging: Ende-zu-Ende-Verschlüsselung (E2EE) #96

Closed
opened 2026-05-17 18:26:15 +00:00 by jreinemann-euris · 3 comments
jreinemann-euris commented 2026-05-17 18:26:15 +00:00 (Migrated from github.com)

Ziel

Ende-zu-Ende-Verschlüsselung (E2EE) für Nachrichten implementieren – ähnlich zu PGP. Nachrichten werden auf dem Sendegerät mit dem öffentlichen Schlüssel des Empfängers verschlüsselt und können nur auf dem Empfängergerät mit dem zugehörigen privaten Schlüssel entschlüsselt werden. Der Server sieht ausschließlich Chiffrat.


Kryptografisches Konzept

  • Asymmetrisches Verfahren: X25519 (ECDH, Schlüsselaustausch) + XSalsa20-Poly1305 (symmetrische Nachrichtenverschlüsselung) – entspricht NaCl/libsodium box-Primitive; alternativ RSA-OAEP + AES-GCM
  • Jeder User besitzt ein Schlüsselpaar (Public Key / Private Key)
  • Der Public Key wird auf dem Server gespeichert und ist für alle authentifizierten Clients abrufbar
  • Der Private Key verlässt das Gerät nie – er wird ausschließlich im verschlüsselten lokalen Speicher des Geräts abgelegt (Android Keystore oder Room-DB mit EncryptedSharedPreferences)

Funktionale Anforderungen

App-Seite: Schlüsselverwaltung

Initiale Schlüsselerzeugung

  • Beim ersten App-Start (oder nach dem Login) wird automatisch ein Schlüsselpaar erzeugt, falls noch keines vorhanden ist
  • Der Public Key wird sofort an den Server übermittelt

Schlüsselerneuerung (Key Rotation) in den Settings

  • In den Einstellungen gibt es eine Option „Verschlüsselungs-Schlüssel erneuern"
  • User muss die Aktion explizit bestätigen (Warnung: ältere, noch nicht entschlüsselte Nachrichten können danach nicht mehr gelesen werden)
  • Neues Schlüsselpaar wird erzeugt, privater Schlüssel ersetzt den alten, neuer Public Key wird an den Server gemeldet
  • Andere online verbundene Clients werden per Push-Nachricht informiert (siehe unten), damit sie den neuen Public Key cachen

Lokale Persistenz

  • Privater Schlüssel: sicher im Android Keystore oder in EncryptedSharedPreferences gespeichert
  • Öffentlicher Schlüssel des eigenen Users: ebenfalls lokal gecacht

App-Seite: Nachrichten

Versenden

  1. App ruft den Public Key des Empfängers ab (zuerst lokaler Cache, sonst Server-API)
  2. Nachricht wird lokal verschlüsselt (Plaintext verlässt das Gerät nie unverschlüsselt)
  3. Chiffrat wird an den Server gesendet

Empfangen

  1. Verschlüsseltes Chiffrat kommt via WebSocket-Push an
  2. App entschlüsselt es lokal mit dem eigenen Private Key
  3. Entschlüsselter Plaintext wird in der lokalen DB gespeichert (Room)

Server-Seite

Public Key Endpunkt

  • PUT /users/{id}/public-key – speichert / aktualisiert den Public Key eines Users
  • GET /users/{id}/public-key – gibt den aktuellen Public Key zurück

Datenbankschema (Server, Ergänzung)

  • Tabelle users: neue Spalte public_key TEXT (Base64-kodierter Public Key)

Key-Rotation Push-Notification

  • Wenn ein Client seinen Public Key aktualisiert (PUT /users/{id}/public-key), sendet der Server eine WebSocket-Push-Nachricht an alle anderen verbundenen Clients:
    { "type": "key_updated", "userId": "<id>", "publicKey": "<base64>" }
    
  • Clients aktualisieren daraufhin ihren lokalen Public-Key-Cache für diesen User

Nachrichtenspeicherung

  • Der Server speichert Nachrichten weiterhin in der messages-Tabelle, jedoch nur als Chiffrat (body enthält den verschlüsselten Blob)
  • Der Server kann den Inhalt nicht lesen – das ist das Sicherheitsziel

Nicht im Scope

  • Key-Backup / Cloud-Sync des privaten Schlüssels
  • Multi-Device-Support (ein Schlüsselpaar pro Gerät ist ausreichend)
  • Forward Secrecy (Double Ratchet / Signal-Protokoll)
  • Schlüsselverifikation via Fingerprint-Vergleich (zukünftiges Feature)

Abhängigkeiten

  • Messaging-Feature (Issue #XX) – muss vorhanden sein, bevor Verschlüsselung aufgesetzt wird
  • User-Konzept & Auth – User-IDs und WebSocket-Infrastruktur müssen vorhanden sein

Akzeptanzkriterien

  • Schlüsselpaar wird beim ersten Start automatisch erzeugt und Public Key an Server übermittelt
  • Nachrichten zwischen zwei Clients sind auf dem Server nur als Chiffrat gespeichert
  • Empfangene Nachrichten werden korrekt entschlüsselt und dargestellt
  • Key Rotation in den Settings funktioniert; andere Clients erhalten Push-Benachrichtigung
  • Nach Key Rotation können neue Nachrichten korrekt verschlüsselt/entschlüsselt werden
  • Privater Schlüssel ist im Android Keystore / EncryptedSharedPreferences gesichert
## Ziel Ende-zu-Ende-Verschlüsselung (E2EE) für Nachrichten implementieren – ähnlich zu PGP. Nachrichten werden auf dem Sendegerät mit dem **öffentlichen Schlüssel** des Empfängers verschlüsselt und können nur auf dem Empfängergerät mit dem zugehörigen **privaten Schlüssel** entschlüsselt werden. Der Server sieht ausschließlich Chiffrat. --- ## Kryptografisches Konzept - **Asymmetrisches Verfahren:** X25519 (ECDH, Schlüsselaustausch) + XSalsa20-Poly1305 (symmetrische Nachrichtenverschlüsselung) – entspricht NaCl/libsodium `box`-Primitive; alternativ RSA-OAEP + AES-GCM - Jeder User besitzt ein **Schlüsselpaar** (Public Key / Private Key) - Der **Public Key** wird auf dem Server gespeichert und ist für alle authentifizierten Clients abrufbar - Der **Private Key** verlässt das Gerät **nie** – er wird ausschließlich im verschlüsselten lokalen Speicher des Geräts abgelegt (Android Keystore oder Room-DB mit EncryptedSharedPreferences) --- ## Funktionale Anforderungen ### App-Seite: Schlüsselverwaltung **Initiale Schlüsselerzeugung** - Beim ersten App-Start (oder nach dem Login) wird automatisch ein Schlüsselpaar erzeugt, falls noch keines vorhanden ist - Der Public Key wird sofort an den Server übermittelt **Schlüsselerneuerung (Key Rotation) in den Settings** - In den Einstellungen gibt es eine Option „Verschlüsselungs-Schlüssel erneuern" - User muss die Aktion explizit bestätigen (Warnung: ältere, noch nicht entschlüsselte Nachrichten können danach nicht mehr gelesen werden) - Neues Schlüsselpaar wird erzeugt, privater Schlüssel ersetzt den alten, neuer Public Key wird an den Server gemeldet - Andere online verbundene Clients werden per Push-Nachricht informiert (siehe unten), damit sie den neuen Public Key cachen **Lokale Persistenz** - Privater Schlüssel: sicher im Android Keystore oder in `EncryptedSharedPreferences` gespeichert - Öffentlicher Schlüssel des eigenen Users: ebenfalls lokal gecacht ### App-Seite: Nachrichten **Versenden** 1. App ruft den Public Key des Empfängers ab (zuerst lokaler Cache, sonst Server-API) 2. Nachricht wird lokal verschlüsselt (Plaintext verlässt das Gerät nie unverschlüsselt) 3. Chiffrat wird an den Server gesendet **Empfangen** 1. Verschlüsseltes Chiffrat kommt via WebSocket-Push an 2. App entschlüsselt es lokal mit dem eigenen Private Key 3. Entschlüsselter Plaintext wird in der lokalen DB gespeichert (Room) --- ### Server-Seite **Public Key Endpunkt** - `PUT /users/{id}/public-key` – speichert / aktualisiert den Public Key eines Users - `GET /users/{id}/public-key` – gibt den aktuellen Public Key zurück **Datenbankschema (Server, Ergänzung)** - Tabelle `users`: neue Spalte `public_key TEXT` (Base64-kodierter Public Key) **Key-Rotation Push-Notification** - Wenn ein Client seinen Public Key aktualisiert (`PUT /users/{id}/public-key`), sendet der Server eine WebSocket-Push-Nachricht an alle anderen verbundenen Clients: ```json { "type": "key_updated", "userId": "<id>", "publicKey": "<base64>" } ``` - Clients aktualisieren daraufhin ihren lokalen Public-Key-Cache für diesen User **Nachrichtenspeicherung** - Der Server speichert Nachrichten weiterhin in der `messages`-Tabelle, jedoch **nur als Chiffrat** (`body` enthält den verschlüsselten Blob) - Der Server kann den Inhalt nicht lesen – das ist das Sicherheitsziel --- ## Nicht im Scope - Key-Backup / Cloud-Sync des privaten Schlüssels - Multi-Device-Support (ein Schlüsselpaar pro Gerät ist ausreichend) - Forward Secrecy (Double Ratchet / Signal-Protokoll) - Schlüsselverifikation via Fingerprint-Vergleich (zukünftiges Feature) --- ## Abhängigkeiten - Messaging-Feature (Issue #XX) – muss vorhanden sein, bevor Verschlüsselung aufgesetzt wird - User-Konzept & Auth – User-IDs und WebSocket-Infrastruktur müssen vorhanden sein --- ## Akzeptanzkriterien - [ ] Schlüsselpaar wird beim ersten Start automatisch erzeugt und Public Key an Server übermittelt - [ ] Nachrichten zwischen zwei Clients sind auf dem Server nur als Chiffrat gespeichert - [ ] Empfangene Nachrichten werden korrekt entschlüsselt und dargestellt - [ ] Key Rotation in den Settings funktioniert; andere Clients erhalten Push-Benachrichtigung - [ ] Nach Key Rotation können neue Nachrichten korrekt verschlüsselt/entschlüsselt werden - [ ] Privater Schlüssel ist im Android Keystore / EncryptedSharedPreferences gesichert
jreinemann-euris commented 2026-05-17 18:37:00 +00:00 (Migrated from github.com)

Migration / Bestehende App-Installationen

🔴 Schlüsselpaar-Initialisierung beim Update

Bestehende User haben beim Update auf die E2EE-Version noch kein Schlüsselpaar. Die App muss beim Start prüfen, ob ein Schlüsselpaar vorhanden ist, und es bei Bedarf transparent generieren + Public Key an den Server melden.

Empfehlung: Eine EnsureKeyPairUseCase-Komponente, die in MainActivity.onCreate (neben SeedDatabaseUseCase) aufgerufen wird:
if (keinSchlüsselpaarVorhanden) { generiereSchlüsselpaar() meldePublicKeyAnServer() }

🔴 fallbackToDestructiveMigration – Risiko für Schema-Änderungen

DatabaseModule verwendet allbackToDestructiveMigration(). Wenn E2EE ein neues Room-Schema erfordert, muss eine saubere @AutoMigration oder manuelle Migration implementiert werden. Der Fallback-Mechanismus darf bei dieser Feature-Entwicklung nicht greifen.

🟡 Bestehende Nachrichten (Plaintext-Altbestand)

Nachrichten vor der E2EE-Einführung sind unverschlüsselt in der DB. Klare Policy: Alte Nachrichten bleiben unkryptiert und werden als solche dargestellt; ab dem Feature-Go-live werden ausschließlich neue Nachrichten verschlüsselt übertragen.

--- ## Migration / Bestehende App-Installationen ### 🔴 Schlüsselpaar-Initialisierung beim Update Bestehende User haben beim Update auf die E2EE-Version noch kein Schlüsselpaar. Die App muss beim Start prüfen, ob ein Schlüsselpaar vorhanden ist, und es bei Bedarf transparent generieren + Public Key an den Server melden. Empfehlung: Eine EnsureKeyPairUseCase-Komponente, die in MainActivity.onCreate (neben SeedDatabaseUseCase) aufgerufen wird: ` if (keinSchlüsselpaarVorhanden) { generiereSchlüsselpaar() meldePublicKeyAnServer() } ` ### 🔴 fallbackToDestructiveMigration – Risiko für Schema-Änderungen DatabaseModule verwendet allbackToDestructiveMigration(). Wenn E2EE ein neues Room-Schema erfordert, muss eine saubere @AutoMigration oder manuelle Migration implementiert werden. Der Fallback-Mechanismus darf bei dieser Feature-Entwicklung **nicht** greifen. ### 🟡 Bestehende Nachrichten (Plaintext-Altbestand) Nachrichten vor der E2EE-Einführung sind unverschlüsselt in der DB. Klare Policy: Alte Nachrichten bleiben unkryptiert und werden als solche dargestellt; ab dem Feature-Go-live werden ausschließlich neue Nachrichten verschlüsselt übertragen.
jreinemann-euris commented 2026-05-17 18:37:06 +00:00 (Migrated from github.com)

Migration / Bestehende App-Installationen

Schlüsselpaar-Initialisierung beim Update (kritisch)

Bestehende User haben beim Update auf die E2EE-Version noch kein Schlüsselpaar. Die App muss beim Start prüfen, ob ein Schlüsselpaar vorhanden ist, und es bei Bedarf transparent generieren + Public Key an den Server melden. Empfehlung: EnsureKeyPairUseCase in MainActivity.onCreate neben SeedDatabaseUseCase aufrufen.

fallbackToDestructiveMigration – Risiko (kritisch)

DatabaseModule verwendet fallbackToDestructiveMigration(). Wenn E2EE ein neues Room-Schema erfordert (z.B. Speicherung des eigenen PublicKey in DB), MUSS eine saubere @AutoMigration oder manuelle Migration implementiert werden. Der Fallback darf nicht greifen.

Bestehende Nachrichten (Plaintext-Altbestand)

Nachrichten vor der E2EE-Einführung sind unverschlüsselt. Klare Policy: Alte Nachrichten bleiben unkryptiert dargestellt; ab Feature-Go-live werden nur neue Nachrichten verschlüsselt übertragen.

## Migration / Bestehende App-Installationen ### Schlüsselpaar-Initialisierung beim Update (kritisch) Bestehende User haben beim Update auf die E2EE-Version noch kein Schlüsselpaar. Die App muss beim Start prüfen, ob ein Schlüsselpaar vorhanden ist, und es bei Bedarf transparent generieren + Public Key an den Server melden. Empfehlung: EnsureKeyPairUseCase in MainActivity.onCreate neben SeedDatabaseUseCase aufrufen. ### fallbackToDestructiveMigration – Risiko (kritisch) DatabaseModule verwendet fallbackToDestructiveMigration(). Wenn E2EE ein neues Room-Schema erfordert (z.B. Speicherung des eigenen PublicKey in DB), MUSS eine saubere @AutoMigration oder manuelle Migration implementiert werden. Der Fallback darf nicht greifen. ### Bestehende Nachrichten (Plaintext-Altbestand) Nachrichten vor der E2EE-Einführung sind unverschlüsselt. Klare Policy: Alte Nachrichten bleiben unkryptiert dargestellt; ab Feature-Go-live werden nur neue Nachrichten verschlüsselt übertragen.
jreinemann-euris commented 2026-05-17 22:22:42 +00:00 (Migrated from github.com)

Abgeschlossen (2026-05-18)

Zyklen: 2 (1 Implementierung + 1 Review-Korrektur)
Tests: 320 Tests, 0 Fehler

Implementierte Artefakte

  • E2EEKeyManager (App): Tink HPKE X25519 + ChaCha20-Poly1305; Keypair-Generierung, Encrypt/Decrypt, Private Key in EncryptedSharedPreferences
  • EnsureKeyPairUseCase (App): Keypair-Initialisierung beim App-Start + Public-Key-Upload an Server
  • MessageRepositoryImpl (App): Encrypt vor Send, Decrypt nach Receive, Public-Key-Cache, key_updated Handler
  • WebSocketClient/Impl (App): KeyUpdated Event + key_updated Frame-Parsing
  • SettingsKey (App): E2EEPrivateKeyset + E2EEPublicKeyBase64 als SENSITIVE_KEYS
  • V4 Flyway Migration (Server): \ALTER TABLE users ADD COLUMN public_key TEXT\
  • PUT /api/users/{id}/public-key (Server): Auth + Owner-Check + Längenvalidierung ≤ 10.000 Zeichen
  • GET /api/users/{id}/public-key (Server): Abruf Public Key
  • WebSocketManager (Server): notifyKeyUpdated() Broadcast
  • MessageRepository (Server): E2EE-Bypass – EncryptionService für Nachrichten deaktiviert, Server speichert Ciphertext direkt
  • Tests: E2EEKeyManagerTest (5), EnsureKeyPairUseCaseTest (4), MessageRepositoryImplTest +5

Abweichungen

  • \ndroid.util.Log\ statt Timber (Timber nicht im Projekt)
  • \JsonKeysetWriter\ (Tink 1.15.0 Deprecation-Warning): funktioniert korrekt; Hardening via AndroidKeysetManager als Follow-up in #105

Follow-up

  • #105: Security Hardening – AndroidKeysetManager statt CleartextKeysetHandle für Private Key
## Abgeschlossen (2026-05-18) **Zyklen:** 2 (1 Implementierung + 1 Review-Korrektur) **Tests:** ✅ 320 Tests, 0 Fehler ### Implementierte Artefakte - ✅ **E2EEKeyManager** (App): Tink HPKE X25519 + ChaCha20-Poly1305; Keypair-Generierung, Encrypt/Decrypt, Private Key in EncryptedSharedPreferences - ✅ **EnsureKeyPairUseCase** (App): Keypair-Initialisierung beim App-Start + Public-Key-Upload an Server - ✅ **MessageRepositoryImpl** (App): Encrypt vor Send, Decrypt nach Receive, Public-Key-Cache, key_updated Handler - ✅ **WebSocketClient/Impl** (App): KeyUpdated Event + key_updated Frame-Parsing - ✅ **SettingsKey** (App): E2EEPrivateKeyset + E2EEPublicKeyBase64 als SENSITIVE_KEYS - ✅ **V4 Flyway Migration** (Server): \ALTER TABLE users ADD COLUMN public_key TEXT\ - ✅ **PUT /api/users/{id}/public-key** (Server): Auth + Owner-Check + Längenvalidierung ≤ 10.000 Zeichen - ✅ **GET /api/users/{id}/public-key** (Server): Abruf Public Key - ✅ **WebSocketManager** (Server): notifyKeyUpdated() Broadcast - ✅ **MessageRepository** (Server): E2EE-Bypass – EncryptionService für Nachrichten deaktiviert, Server speichert Ciphertext direkt - ✅ **Tests**: E2EEKeyManagerTest (5), EnsureKeyPairUseCaseTest (4), MessageRepositoryImplTest +5 ### Abweichungen - \ndroid.util.Log\ statt Timber (Timber nicht im Projekt) - \JsonKeysetWriter\ (Tink 1.15.0 Deprecation-Warning): funktioniert korrekt; Hardening via AndroidKeysetManager als Follow-up in #105 ### Follow-up - #105: Security Hardening – AndroidKeysetManager statt CleartextKeysetHandle für Private Key
Sign in to join this conversation.
No description provided.