wip: Sicherungs-Commit aller Änderungen seit April + Arbeitsstruktur (CLAUDE.md, ROADMAP.md, PROGRESS.md, Autopilot)
6 Wochen uncommittete Arbeit (Voice-Channels, LiveKit-Manager, Settings-Modal u.v.m.) als ein WIP-Commit gesichert, damit nichts verloren geht und der Pi den aktuellen Stand klonen kann. Thematische Aufarbeitung: siehe ROADMAP M0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CPrAGBxBT6GfPXzeWQ4AXb
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
# 06 — E2EE Verschlüsselung
|
||||
|
||||
---
|
||||
|
||||
## Inhaltsverzeichnis
|
||||
- [Setup & Initialisierung](#setup--initialisierung)
|
||||
- [Verschlüsselung aktivieren](#verschlüsselung-aktivieren)
|
||||
- [Entschlüsselung & Fehlerfälle](#entschlüsselung--fehlerfälle)
|
||||
- [Device-Verifikation (SAS)](#device-verifikation-sas)
|
||||
- [Cross-Signing](#cross-signing)
|
||||
- [Key Backup](#key-backup)
|
||||
- [Fehlende Session Keys anfordern](#fehlende-session-keys-anfordern)
|
||||
- [Geräte-Keys abfragen](#geräte-keys-abfragen)
|
||||
- [Trust-Status](#trust-status)
|
||||
|
||||
---
|
||||
|
||||
## Setup & Initialisierung
|
||||
|
||||
```dart
|
||||
// flutter_vodozemac muss initialisiert sein BEVOR Client erstellt wird
|
||||
import 'package:flutter_vodozemac/flutter_vodozemac.dart' as vod;
|
||||
await vod.init();
|
||||
|
||||
// Client mit E2EE-Support
|
||||
final client = Client(
|
||||
'Pyramid',
|
||||
database: await MatrixSdkDatabase.init('pyramid', database: db),
|
||||
nativeImplementations: NativeImplementationsIsolate(compute),
|
||||
// Oder für Hintergrund-Isolaten (kein compute):
|
||||
// nativeImplementations: NativeImplementationsDummy(),
|
||||
verificationMethods: {
|
||||
KeyVerificationMethod.numbers,
|
||||
KeyVerificationMethod.emoji,
|
||||
// KeyVerificationMethod.qrCodeScan, // falls QR-Scanner vorhanden
|
||||
// KeyVerificationMethod.qrCodeShow,
|
||||
},
|
||||
);
|
||||
|
||||
await client.init(
|
||||
waitForFirstSync: false,
|
||||
waitUntilLoadCompletedLoaded: false,
|
||||
);
|
||||
|
||||
// E2EE verfügbar prüfen
|
||||
print(client.encryptionEnabled); // true wenn Vodozemac/Olm geladen
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verschlüsselung aktivieren
|
||||
|
||||
```dart
|
||||
// Raum-Verschlüsselung einschalten (PERMANENT, nicht rückgängig!)
|
||||
if (!room.isEncrypted) {
|
||||
await room.enableEncryption();
|
||||
}
|
||||
|
||||
// Verschlüsselung beim Erstellen aktivieren
|
||||
final roomId = await client.createRoom(
|
||||
name: 'Verschlüsselter Raum',
|
||||
enableEncryption: true,
|
||||
preset: CreateRoomPreset.privateChat,
|
||||
);
|
||||
|
||||
// Status prüfen
|
||||
print(room.isEncrypted); // bool
|
||||
print(room.encryptionAlgorithm); // 'm.megolm.v1.aes-sha2'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Entschlüsselung & Fehlerfälle
|
||||
|
||||
```dart
|
||||
// Beim Anzeigen von Events prüfen:
|
||||
if (event.isBadEncrypted) {
|
||||
// Anzeigen: "Nachricht kann nicht entschlüsselt werden"
|
||||
// Mögliche Ursachen:
|
||||
// - Gerät war nicht in der Raumschlüssel-Verteilung
|
||||
// - Session fehlt (Megolm Session nicht vorhanden)
|
||||
// - Falsches Gerät / fehlende Olm-Session
|
||||
}
|
||||
|
||||
// Entschlüsselungs-Fehler-Typ
|
||||
if (event.content['can_not_decrypt'] != null) {
|
||||
// Generischer Fehler
|
||||
}
|
||||
if (event.type == EventTypes.Encrypted && event.plaintextContent == null) {
|
||||
// Noch nicht entschlüsselt
|
||||
}
|
||||
|
||||
// Event-Body bei Fehler prüfen
|
||||
final body = event.body;
|
||||
// Enthält "** Unable to decrypt: ..." bei Fehlern
|
||||
|
||||
// Fehlende Schlüssel anfordern (von anderen Geräten)
|
||||
await client.requestMissingSessionKeys();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Device-Verifikation (SAS)
|
||||
|
||||
```dart
|
||||
// ── Verifikation starten (ich verifiziere ein anderes Gerät) ──────────────
|
||||
|
||||
// Verifikations-Anfrage senden
|
||||
final request = await client.userDeviceKeys[otherUserId]!
|
||||
.startVerification();
|
||||
// request.onUpdate: Stream mit Zustandsänderungen
|
||||
|
||||
// ── Verifikation empfangen (anderes Gerät will mich verifizieren) ─────────
|
||||
|
||||
// Eingehende Anfragen beobachten
|
||||
client.onKeyVerificationRequest.stream.listen((request) async {
|
||||
// Anfrage annehmen
|
||||
await request.acceptVerification();
|
||||
});
|
||||
|
||||
// ── SAS-Flow ─────────────────────────────────────────────────────────────
|
||||
|
||||
// Auf SAS-Ready warten und Emojis/Zahlen anzeigen
|
||||
request.onUpdate.stream.listen((_) async {
|
||||
if (request.state == KeyVerificationState.showSas) {
|
||||
// Emojis anzeigen
|
||||
final emojis = request.sasEmojis; // List<KeyVerificationEmoji>
|
||||
for (final emoji in emojis) {
|
||||
print('${emoji.emoji} ${emoji.name}');
|
||||
}
|
||||
|
||||
// Oder Dezimalzahlen
|
||||
final decimals = request.sasDecimals; // List<int> (3 Zahlen)
|
||||
print(decimals); // z.B. [1234, 5678, 9012]
|
||||
|
||||
// Wenn User bestätigt dass sie übereinstimmen:
|
||||
await request.confirmSas();
|
||||
|
||||
// Wenn sie nicht übereinstimmen:
|
||||
// await request.rejectSas();
|
||||
}
|
||||
|
||||
if (request.state == KeyVerificationState.done) {
|
||||
print('Verifikation erfolgreich!');
|
||||
}
|
||||
|
||||
if (request.state == KeyVerificationState.error) {
|
||||
print('Verifikation fehlgeschlagen: ${request.cancelCode}');
|
||||
}
|
||||
});
|
||||
|
||||
// ── QR-Code Verifikation ─────────────────────────────────────────────────
|
||||
|
||||
// QR-Code zum Anzeigen generieren
|
||||
final qrData = await request.generateQrCodeData();
|
||||
// qrData als QR-Code rendern (z.B. mit qr_flutter-Package)
|
||||
|
||||
// Gescannten QR-Code verarbeiten
|
||||
await request.scanQrCode(scannedData);
|
||||
|
||||
// Bestätigen dass QR gescannt wurde
|
||||
await request.confirmQrCodeScanned();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cross-Signing
|
||||
|
||||
```dart
|
||||
// Cross-Signing bootstrappen (erstellt Master-, Self- und User-Signing-Keys)
|
||||
// Nur einmal pro Account nötig
|
||||
await client.encryption!.bootstrapCrossSigning(
|
||||
setupMasterKey: true,
|
||||
setupSelfSigningKey: true,
|
||||
setupUserSigningKey: true,
|
||||
);
|
||||
|
||||
// Cross-Signing-Status
|
||||
final crossSigning = client.encryption!.crossSigning;
|
||||
print(crossSigning.enabled); // bool
|
||||
print(crossSigning.selfSigned); // bool: eigenes Gerät ist self-signed
|
||||
|
||||
// Eigenes Gerät ist verifiziert?
|
||||
final myDevice = client.userDeviceKeys[client.userID]
|
||||
?.deviceKeys[client.deviceID];
|
||||
print(myDevice?.verified); // bool
|
||||
|
||||
// Anderen User als vertrauenswürdig markieren
|
||||
await client.encryption!.crossSigning.signUser(otherUserId);
|
||||
|
||||
// Gerät verifizieren (nach SAS/QR)
|
||||
await myDevice?.setVerified(true);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Backup
|
||||
|
||||
```dart
|
||||
// ── Key Backup einrichten ─────────────────────────────────────────────────
|
||||
|
||||
// Vollständiges Crypto-Setup (empfohlen):
|
||||
// Erstellt Secret Storage + Cross-Signing + Key Backup
|
||||
await client.bootstrapCryptoIdentity(
|
||||
wipeRecovery: false, // true: bestehende Recovery überschreiben
|
||||
);
|
||||
|
||||
// Nur Key Backup:
|
||||
final backup = client.encryption!.keyManager;
|
||||
|
||||
// Recovery-Key generieren und anzeigen
|
||||
final recoveryKey = await client.encryption!.ssss.generateKey();
|
||||
print(recoveryKey); // Langer alphanumerischer Schlüssel → User aufschreiben lassen!
|
||||
|
||||
// ── Key Backup Status ─────────────────────────────────────────────────────
|
||||
|
||||
// Ist Key Backup aktiv?
|
||||
final keyBackupEnabled = client.encryption!.keyManager.enabled;
|
||||
|
||||
// Alle Sessions hochladen
|
||||
await client.encryption!.keyManager.uploadInboundGroupSessions();
|
||||
|
||||
// ── Backup wiederherstellen (Recovery) ───────────────────────────────────
|
||||
|
||||
// Keys von Server-Backup wiederherstellen
|
||||
await client.encryption!.keyManager.loadFromResponse(
|
||||
await client.getRoomKeysBackup(),
|
||||
);
|
||||
|
||||
// Oder mit Recovery-Key:
|
||||
await client.encryption!.ssss.unlock(recoveryKey: eingabe);
|
||||
await client.encryption!.keyManager.restore();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fehlende Session Keys anfordern
|
||||
|
||||
```dart
|
||||
// Schlüssel von anderen Geräten desselben Users anfordern
|
||||
await client.requestMissingSessionKeys();
|
||||
|
||||
// Für einen spezifischen Raum
|
||||
await client.requestMissingSessionKeys(
|
||||
rooms: [room],
|
||||
);
|
||||
|
||||
// Einzelne Session anfordern (für ein spezifisches Event)
|
||||
if (event.isBadEncrypted) {
|
||||
final sessionId = event.content
|
||||
.tryGet<String>('session_id');
|
||||
final senderKey = event.content
|
||||
.tryGet<String>('sender_key');
|
||||
if (sessionId != null) {
|
||||
await room.requestKey(sessionId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Geräte-Keys abfragen
|
||||
|
||||
```dart
|
||||
// Alle Geräte eines Users laden
|
||||
final userDevices = await client.getUserDeviceKeys(userId);
|
||||
// → Map<String, DeviceKeysList>
|
||||
|
||||
// Geräte eines Raums (alle Mitglieder)
|
||||
final roomDevices = await room.getUserDeviceKeys();
|
||||
|
||||
// Eigene Geräte
|
||||
final myDevices = client.userDeviceKeys[client.userID]?.deviceKeys;
|
||||
|
||||
// Gerät-Details
|
||||
for (final device in myDevices?.values ?? []) {
|
||||
print(device.deviceId);
|
||||
print(device.displayName);
|
||||
print(device.verified);
|
||||
print(device.blocked);
|
||||
print(device.ed25519Key); // Signatur-Schlüssel
|
||||
print(device.curve25519Key); // Encryption-Schlüssel
|
||||
}
|
||||
|
||||
// Gerät sperren (Nachrichten werden nicht mehr entschlüsselt gesendet)
|
||||
await myDevices?['DEVICEID']?.setBlocked(true);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trust-Status
|
||||
|
||||
```dart
|
||||
// Trust-Level für Raum berechnen
|
||||
final encryption = room.client.encryption;
|
||||
if (encryption != null) {
|
||||
final trust = await room.calcEncryptionHealthState();
|
||||
// trust: .allVerified | .unverifiedDevices | .unknown
|
||||
}
|
||||
|
||||
// User-Trust
|
||||
final userTrust = client.userDeviceKeys[userId]?.verified;
|
||||
// null = unbekannt, true = verifiziert, false = blockiert
|
||||
|
||||
// Nachrichten-Verifikation
|
||||
if (!event.encryptionHealthState.allVerified) {
|
||||
// Anzeigen: "Gesendet von unverifiziertem Gerät"
|
||||
}
|
||||
|
||||
// Eigene Identität
|
||||
final myIdentity = client.encryption?.crossSigning;
|
||||
print(myIdentity?.selfSigned); // Eigenes Gerät cross-signed?
|
||||
```
|
||||
Reference in New Issue
Block a user