16561bbdeb
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>
126 lines
6.9 KiB
Markdown
126 lines
6.9 KiB
Markdown
# 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.
|