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

Stand: 2026-07-03, geschrieben auf dem Pi (Plan gegen den echten Code verifiziert,
Umsetzung braucht PC + Android-Testgerät). Gehört zum offenen M1-Punkt
„Sichere Speicherung von Access-Token & Krypto-DB".

## 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:
   ```dart
   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.sqlite` → `pyramid.sqlite.premigration` kopieren
   (Bytes, nicht rename). Bleibt bis zum verifizierten Erfolg liegen.
4. Export in NEUE Datei (Original bleibt unangetastet):
   ```sql
   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.sqlite` →
   `pyramid.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 1–4 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.
