Files
pyramid/docs/matrix-sdk/06-encryption.md
T
Bernd Steckmeister 25ed765a03 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
2026-07-03 05:47:18 +02:00

8.9 KiB

06 — E2EE Verschlüsselung


Inhaltsverzeichnis


Setup & Initialisierung

// 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

// 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

// 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)

// ── 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

// 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

// ── 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

// 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

// 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

// 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?