25ed765a03
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
314 lines
8.9 KiB
Markdown
314 lines
8.9 KiB
Markdown
# 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?
|
|
```
|