docs: SQLCipher-Migrationsplan + konsolidierter PC-Testplan (M1-Vorbereitung)

Die zwei obersten offenen ROADMAP-Punkte sind PC-only in der Umsetzung,
aber jetzt maximal vorbereitet: docs/SQLCIPHER_MIGRATION.md (gegen beide
DB-Öffnungsstellen und matrix-6.2.0 verifiziert, inkl. Push-Isolate-Falle
und Migration-Race) und docs/PC_TESTPLAN.md (alle UNGETESTET(Pi)-Punkte
aus 7 verstreuten PROGRESS-Einträgen an einer Stelle).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Bernd Steckmeister
2026-07-03 17:50:58 +02:00
parent 87b29b5912
commit 16561bbdeb
5 changed files with 278 additions and 2 deletions
+125
View File
@@ -0,0 +1,125 @@
# 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 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.