f64398d3c6
- AppDatabase.open()/openForPush() kapseln Factory, Schluessel (Keystore), Klartext->SQLCipher-Migration (Backup/Verify/atomarer Tausch) und PRAGMAs - Android/Windows/Linux verschluesselt, iOS/macOS bewusst Klartext - 5 neue Tests mit echter SQLCipher-DLL; Windows-Praxistest mit echter DB erfolgreich (23 Tabellen verifiziert, E2EE intakt); Android UNGETESTET
135 lines
7.6 KiB
Markdown
135 lines
7.6 KiB
Markdown
# 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 bleibt UNGETESTET** (Punkt 4 des Testplans unten:
|
||
> Migration auf Gerät, Push-Isolate + secure_storage) – 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:
|
||
```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.
|