Files
pyramid/docs/SQLCIPHER_MIGRATION.md

8.4 KiB
Raw Permalink Blame History

SQLCipher-Migration der pyramid.sqlite Umsetzungsplan (PC-Termin)

STATUS 2026-07-06: UMGESETZT (lib/core/app_database.dart) und auf Windows mit Bernds echter 14-MB-DB praxisgetestet (Migration gelaufen, 23 Tabellen verifiziert, Login/E2EE danach intakt, Neustart öffnet die verschlüsselte DB, flutter test deckt Migration/Fehlpfade mit echtem SQLCipher ab). Android-Emulator-Test 2026-07-06 (API 36, x86_64, Debug-APK) GRÜN: (a) frische Installation legt die DB direkt verschlüsselt an (Header = Zufallsbytes, per run-as verifiziert), kein Cipher-Fallback im auth_log; (b) Update-Szenario nach pm clear + untergeschobener Klartext-DB (synthetisch, 2 Tabellen/550 Zeilen): Migration lief beim ersten Start („2 Tabellen verifiziert" im auth_log), .premigration liegt da, DB danach verschlüsselt; (c) Neustart öffnet mit dem Keystore-Schlüssel OHNE Re-Migration, App rendert normal den Login-Screen. Damit sind openCipherOnAndroid, sqlcipher_export und flutter_secure_storage auf Android (Haupt-Isolate) real belegt. Offen bleibt (deshalb Punkt 4 NICHT voll abgehakt): echtes ARM-Gerät, Migration einer ECHTEN eingeloggten Session auf Android und der Push-Test (FCM-Isolate liest verschlüsselte DB, secure_storage im Background-Isolate) braucht ein Testgerät mit eingeloggtem Account. Erst danach Release und Uta. Abweichung vom Plan unten: Die Annahme „keine Desktop-Binaries" ist seit sqlcipher_flutter_libs 0.6.x überholt das Plugin baut SQLCipher auf Windows/Linux als sqlite3.dll/libsqlite3.so mit (im Build-Output verifiziert). Deshalb ist Phase 1 NICHT Android-only: Windows/Linux verschlüsseln ebenfalls, ein Test-Override ist unnötig. iOS/macOS bleiben Klartext (SQLCipher-Pod kollidiert mit firebase_messaging, kein Release-Ziel).

Für Bernd in einfach

Die Datenbank der App (Nachrichten, Zugangs-Token, Verschlüsselungs-Schlüssel) liegt heute unverschlüsselt auf dem Gerät geschützt nur durch die Android-App-Sandbox. Ein Angreifer bräuchte Root oder physischen Zugriff mit Entwickler-Tools, um sie zu lesen. Dieser Plan verschlüsselt die Datei selbst. Der riskante Teil ist nicht die Verschlüsselung, sondern die einmalige Umwandlung bestehender Installationen (Utas Handy!) deshalb: erst am PC mit Testdaten, dann eigenes Gerät, dann erst Release.

Offene Entscheidung (Frage in PROGRESS.md): Priorität vor/nach M2?

Ist-Zustand (verifiziert im Code)

Stelle Datei Factory heute
Haupt-App Android/iOS lib/core/matrix_client.dart:24 sqflite_native.databaseFactory (System-SQLite, kann kein SQLCipher)
Haupt-App Desktop lib/core/matrix_client.dart:27 databaseFactoryFfi (gebündeltes sqlite3)
Push-Isolate (nur Android) lib/core/background_push.dart _buildClient() sqflite_native.databaseFactory
  • sqlcipher_flutter_libs ^0.6.8 ist bereits in pubspec.yaml, wird nirgends benutzt.
  • flutter_secure_storage ^10 ist bereits da (→ Schlüsselablage im Android Keystore).
  • Beide Prozesse öffnen dieselbe Datei (pyramid.sqlite, WAL-Modus, busy_timeout 5000).

Zielarchitektur

  1. Ein gemeinsamer DB-Öffner lib/core/app_database.dart (neues Storage-Modul im Sinne von M2): kapselt Factory-Wahl, PRAGMAs, Schlüsselbeschaffung und Migration. matrix_client.dart und background_push.dart rufen NUR noch diese Fassade damit verschwindet auch die heutige Code-Duplikation der PRAGMA-Blöcke.
  2. Android: nicht mehr sqflite_native, sondern databaseFactoryFfi mit SQLCipher-Lib:
    import 'package:sqlite3/open.dart';
    import 'package:sqlcipher_flutter_libs/sqlcipher_flutter_libs.dart';
    open.overrideFor(OperatingSystem.android, openCipherOnAndroid);
    
    Achtung Push-Isolate: databaseFactoryFfi spawnt ein eigenes Isolate im Firebase-Background-Isolate ist das genau die Fallenklasse, an der schon NativeImplementationsIsolate scheiterte (Kommentar in background_push.dart). Dort deshalb databaseFactoryFfiNoIsolate benutzen.
  3. Schlüssel: 32 Byte kryptografisch zufällig (Random.secure()), hex-kodiert in flutter_secure_storage unter db_cipher_key. Beide Prozesse lesen denselben Eintrag. Schlüssel wird bei ERSTER Nutzung erzeugt; existiert schon eine verschlüsselte DB, aber kein Schlüssel im Storage → harter Fehler mit Klartext-Meldung, NIEMALS still neue DB anlegen (Datenverlust-Regel!).
  4. Öffnen: PRAGMA key = "x'<hex>'" als allererstes Statement (OpenDatabaseOptions(onConfigure: ...)), danach wie bisher WAL + busy_timeout.
  5. Windows/Linux-Desktop: sqlcipher_flutter_libs liefert dort KEINE Binaries. Phase 1 = nur Android verschlüsseln (dort liegt das echte Risiko, dort sind die echten Nutzer); Desktop bleibt vorerst Klartext, klar im Code kommentiert. Desktop-SQLCipher später als eigener Punkt (eigene DLL bündeln).

Migration bestehender Klartext-DBs (der heikle Teil)

Reihenfolge beim App-Start, VOR MatrixSdkDatabase.init, VOR jedem Push-Zugriff:

  1. Erkennung: Datei beginnt mit SQLite format 3\0 → Klartext → Migration nötig. (Verschlüsselte SQLCipher-Dateien haben keinen lesbaren Header.)
  2. WAL eindampfen: Klartext-DB kurz normal öffnen, PRAGMA wal_checkpoint(TRUNCATE), schließen sonst verlieren wir Daten, die noch in pyramid.sqlite-wal liegen.
  3. Backup: pyramid.sqlitepyramid.sqlite.premigration kopieren (Bytes, nicht rename). Bleibt bis zum verifizierten Erfolg liegen.
  4. Export in NEUE Datei (Original bleibt unangetastet):
    ATTACH DATABASE 'pyramid.enc.sqlite' AS enc KEY "x'<hex>'";
    SELECT sqlcipher_export('enc');
    DETACH DATABASE enc;
    
    (läuft über die SQLCipher-FFI-Verbindung auf der Klartext-DB)
  5. Verifikation: pyramid.enc.sqlite mit Schlüssel öffnen, PRAGMA integrity_check == ok UND Zeilenzahl einer Kerntabelle (z. B. box_inbound_group_session, Name gegen matrix-6.2.0 matrix_sdk_database.dart:139 verifiziert) identisch mit Original. Schlägt IRGENDWAS fehl → pyramid.enc.sqlite löschen, App startet normal mit Klartext-DB weiter, Fehler in auth_log.txt. Kein Zustand darf die Klartext-DB beschädigt zurücklassen.
  6. Tausch: Original + -wal/-shm löschen, pyramid.enc.sqlitepyramid.sqlite (rename, gleiche Partition = atomar).
  7. .premigration-Backup erst nach N erfolgreichen Tagen / manuell löschen (Entscheidung Bernd: sonst liegt weiter eine Klartext-Kopie herum Kompromiss: nach 7 Tagen beim Start automatisch löschen).

Race mit dem Push-Isolate

Kommt ein FCM-Push, während die Haupt-App migriert, würde das Isolate die DB mittendrin öffnen. Lösung: Marker-Datei pyramid.sqlite.migrating vor Schritt 2 anlegen, nach Schritt 6 löschen; _buildClient() im Push-Pfad bricht bei vorhandenem Marker sofort ab (Notification zeigt dann einmalig den „Neue Nachricht"-Platzhalter akzeptabel). Zusätzlich erkennt das Isolate selbst Klartext vs. verschlüsselt am Datei-Header und wählt PRAGMA key entsprechend es migriert aber NIE selbst.

Testplan (PC-Termin, in dieser Reihenfolge)

  1. Windows-Build mit Test-Account: Migration Klartext→SQLCipher durchlaufen lassen (Desktop kriegt fürs TESTEN die FFI-SQLCipher-Variante per lokalem Override, auch wenn Phase 1 sie im Release nicht aktiviert).
  2. Kernflow danach: Login erhalten? Alte verschlüsselte Nachrichten lesbar? Logout → Neu-Login → immer noch lesbar? (docs/PC_TESTPLAN.md Abschnitt 1)
  3. Fehlerpfade erzwingen: Migration mit vollem Datenträger / Kill mitten in Schritt 4 → App muss mit unversehrter Klartext-DB weiterlaufen.
  4. Android-Testgerät (eigenes, NICHT Uta): frische Installation (Neuanlage verschlüsselt), dann Update-Szenario (bestehende Klartext-DB migriert), dann Push-Test (Isolate liest verschlüsselte DB; secure_storage-Zugriff aus dem Background-Isolate explizit verifizieren bekanntes Risiko).
  5. Erst wenn 14 grün: Release, Uta informieren (ihr Gerät migriert beim ersten Start automatisch; vorher ihr Key-Backup verifizieren!).

Bewusst NICHT im Scope

  • auth_log.txt, SharedPreferences, Media-Cache bleiben Klartext (keine Geheimnisse bzw. nur Cache; einzeln bewerten, falls Bernd es wünscht).
  • Passphrase-basierter Schlüssel (Nutzer-Eingabe): bewusst nein Schlüssel liegt im Keystore, gleiche Vertrauensstufe wie der Rest der App-Daten, aber Datei-Exfiltration allein reicht dann nicht mehr.