test: Live-E2E-Verifikation SQLCipher mit echtem Login (Test-Account)
Neuer gegateter Test (PYRAMID_LIVE_TEST=1, Windows): echter Login als pyramidtest1, Byte-Pruefung der Platte (kein SQLite-Header, Token + Nachricht nicht im Klartext, Positiv-Kontrolle in Klartext-DB), Neustart aus verschluesselter DB, Logout/Re-Login, Migration einer Klartext-DB mit echter eingeloggter Session. Zugangsdaten bleiben ausserhalb des Repos. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1450,3 +1450,8 @@
|
||||
2026-07-06 18:48 [Edit] C:\Users\nordm\pyramid - Kopie\lib\core\media_cache.dart
|
||||
2026-07-06 18:48 [Edit] C:\Users\nordm\pyramid - Kopie\lib\core\media_cache.dart
|
||||
2026-07-06 18:50 [Write] C:\Users\nordm\AppData\Local\Temp\claude\C--Users-nordm-pyramid---Kopie\5be37250-1cea-473c-b62c-923f3d071abd\scratchpad\ui.ps1
|
||||
2026-07-06 22:04 [Write] C:\Users\nordm\pyramid - Kopie\test\live_encryption_e2e_test.dart
|
||||
2026-07-06 22:08 [Edit] C:\Users\nordm\pyramid - Kopie\test\live_encryption_e2e_test.dart
|
||||
2026-07-06 22:08 [Edit] C:\Users\nordm\pyramid - Kopie\test\live_encryption_e2e_test.dart
|
||||
2026-07-06 22:10 [Edit] C:\Users\nordm\pyramid - Kopie\ROADMAP.md
|
||||
2026-07-06 22:11 [Edit] C:\Users\nordm\pyramid - Kopie\PROGRESS.md
|
||||
|
||||
+52
@@ -13,6 +13,58 @@ Schritt (und beim Abbruch mitten im Schritt den Zwischenstand). Format:
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-06 – Nacht-Session (A): SQLCipher mit ECHTEM Login live verifiziert (Test-Account, 2 neue E2E-Tests grün)
|
||||
|
||||
**Erledigt:** Bernds Fokus (A) für die Nacht – die Verschlüsselung nicht nur
|
||||
behaupten, sondern mit einem echten Konto belegen. Bernd hat dafür
|
||||
Test-Accounts bereitgestellt (`~/.pyramid-autopilot/test-accounts.txt`,
|
||||
pyramidtest1/pyramidtest2 auf steggi-matrix.work – NICHT Bernds/Utas Konten).
|
||||
Neuer Live-Test `test/live_encryption_e2e_test.dart` (nur auf Windows, nur
|
||||
mit `PYRAMID_LIVE_TEST=1`, Zugangsdaten bleiben AUSSERHALB des Repos):
|
||||
|
||||
- **Test 1 – frische Installation (grün, echter Lauf gegen den Homeserver):**
|
||||
DB verschlüsselt angelegt (Schlüssel-Injektion wie in den Bestands-Tests),
|
||||
echter Passwort-Login als pyramidtest1, Testraum angelegt, Nachricht mit
|
||||
Zufalls-Marker gesendet. Danach Byte-Prüfung der Platte: Header ist NICHT
|
||||
„SQLite format 3", und weder der echte Access-Token noch der
|
||||
Nachrichtentext sind in `pyramid.sqlite`/`-wal` im Klartext auffindbar.
|
||||
Gegenprobe: Raw-Query MIT Schlüssel findet beides in der Datei → die Daten
|
||||
stecken wirklich drin, nur verschlüsselt. „Neustart" (DB neu geöffnet,
|
||||
neuer Client): Session inkl. identischem Access-Token kommt aus der
|
||||
verschlüsselten DB, ohne Passwort. Dann Logout → erneuter Login → alte
|
||||
Nachricht wieder lesbar (Timeline inkl. History-Nachladen).
|
||||
- **Test 2 – Utas Update-Fall mit ECHTER Session (grün):** Klartext-DB wie
|
||||
vor der SQLCipher-Einführung, echter Login, Nachricht. POSITIV-KONTROLLE:
|
||||
in der Klartext-DB findet die Byte-Suche Token UND Nachricht (beweist,
|
||||
dass die Suche Leaks erkennen würde). Dann `migratePlaintextToEncrypted`
|
||||
(exakt der App-Codepfad): Migration grün, `.premigration`-Backup da,
|
||||
danach ist auf der Platte nichts mehr im Klartext, Session (gleicher
|
||||
Access-Token) und Nachricht überleben die Migration. Das war bisher der
|
||||
große UNGETESTET-Punkt „Migration mit echter eingeloggter Session".
|
||||
- Beide Tests räumen serverseitig auf (Logout am Ende). Gesamtsuite: 32
|
||||
Tests grün, die 2 Live-Tests skippen ohne Gate sauber. analyze sauber
|
||||
(nur bekannter onUpdate-Hinweis).
|
||||
|
||||
**Ehrlich abgegrenzt (bleibt UNGETESTET):** flutter_secure_storage wird im
|
||||
Host-Test umgangen (Schlüssel direkt injiziert; der Keystore-Pfad ist durch
|
||||
die echten App-Läufe Windows + Android-Emulator belegt). E2EE-verschlüsselte
|
||||
Räume kann der Host-Test nicht (kein vodozemac) – der Testraum ist
|
||||
unverschlüsselt, was für die Klartext-Suche der härtere Fall ist. Echtes
|
||||
ARM-Gerät + FCM-Push-Isolate bleiben offen (ROADMAP-Punkt weiter nicht
|
||||
abgehakt).
|
||||
|
||||
**Offen/Nächster Schritt:** Fokus (C) Streaming 60 FPS
|
||||
(`docs/STREAMING_60FPS.md`), danach Fokus (B) Call-Logik mit beiden
|
||||
Test-Accounts.
|
||||
|
||||
**Stolperfallen:** `Client.init(waitUntilLoadCompletedLoaded: false)` lädt
|
||||
die Raumliste ASYNCHRON – wer direkt nach dem Init `client.rooms`/
|
||||
`getRoomById` prüft, sieht eine leere Liste und meldet fälschlich
|
||||
Datenverlust. Für DB-Restaurations-Checks `waitUntilLoadCompletedLoaded:
|
||||
true` verwenden.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-06 – SQLCipher auf ANDROID getestet (Emulator API 36): Neuanlage, Migration und Neustart grün
|
||||
|
||||
**Erledigt:** Der größte offene Teil von Testplan Punkt 4
|
||||
|
||||
+10
-1
@@ -51,7 +51,16 @@ Punkte abhaken (`[x]`), wenn erledigt UND in `PROGRESS.md` protokolliert.
|
||||
**Nachtrag 2026-07-06 (vierte PC-Session): Android-EMULATOR-Test grün**
|
||||
(API 36: Neuanlage verschlüsselt, Klartext-Migration beim ersten Start,
|
||||
Neustart mit Keystore-Schlüssel ohne Re-Migration – Details im
|
||||
Status-Kopf von `docs/SQLCIPHER_MIGRATION.md`). **Bleibt offen:**
|
||||
Status-Kopf von `docs/SQLCIPHER_MIGRATION.md`).
|
||||
**Nachtrag 2026-07-06 (Nacht-Session): LIVE-E2E-Test mit ECHTEM Login grün**
|
||||
(`test/live_encryption_e2e_test.dart`, Test-Account pyramidtest1 auf
|
||||
steggi-matrix.work, Gate `PYRAMID_LIVE_TEST=1`): Platte nach echtem
|
||||
Login/Nachricht byte-geprüft (kein SQLite-Header, Access-Token und
|
||||
Nachrichtentext NICHT im Klartext auffindbar, Positiv-Kontrolle in
|
||||
Klartext-DB findet beides), Neustart restauriert Session aus der
|
||||
verschlüsselten DB, Logout→Re-Login→alte Nachricht lesbar, und die
|
||||
Klartext-Migration mit einer ECHTEN eingeloggten Session erhält
|
||||
Session+Daten. **Bleibt offen:**
|
||||
echtes ARM-Gerät mit eingeloggter Session + Push-Isolate-Test
|
||||
(FCM liest verschlüsselte DB, secure_storage im Background-Isolate) –
|
||||
Testplan Punkt 4 in `docs/SQLCIPHER_MIGRATION.md`;
|
||||
|
||||
@@ -0,0 +1,423 @@
|
||||
@TestOn('windows')
|
||||
library;
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'package:sqlite3/open.dart' as sqlite_open;
|
||||
import 'package:pyramid/core/app_database.dart';
|
||||
|
||||
/// LIVE-End-to-End-Test der SQLCipher-Verschlüsselung mit einem ECHTEN
|
||||
/// Matrix-Login gegen steggi-matrix.work (Test-Account, NICHT Bernd/Uta).
|
||||
///
|
||||
/// Bewusst doppelt gegated, damit er nie in einem normalen `flutter test`
|
||||
/// mitläuft (Netzwerk + echter Homeserver):
|
||||
/// 1. Umgebungsvariable `PYRAMID_LIVE_TEST=1` muss gesetzt sein.
|
||||
/// 2. Die Zugangsdaten-Datei (`PYRAMID_TEST_ACCOUNTS` bzw.
|
||||
/// `%USERPROFILE%\.pyramid-autopilot\test-accounts.txt`) muss existieren –
|
||||
/// sie liegt AUSSERHALB des Repos, hier stehen keine Secrets.
|
||||
///
|
||||
/// Was der Test wirklich beweist (auf Windows, mit der SQLCipher-DLL aus dem
|
||||
/// App-Build):
|
||||
/// * Eine frisch angelegte DB ist auf der Platte verschlüsselt: Header ist
|
||||
/// nicht "SQLite format 3", und weder der echte Access-Token noch der
|
||||
/// Klartext einer echten gesendeten Nachricht sind in den Datei-Bytes
|
||||
/// auffindbar (inkl. WAL-Sidecar).
|
||||
/// * Die Byte-Suche selbst funktioniert (Positiv-Kontrolle: in einer
|
||||
/// Klartext-DB werden Token UND Nachricht gefunden).
|
||||
/// * "Neustart": Die Session (Access-Token) und die Nachricht kommen
|
||||
/// nachweislich aus der verschlüsselten DB (Raw-Query mit Schlüssel,
|
||||
/// Client-Init ohne erneute Passwort-Eingabe).
|
||||
/// * Logout → erneuter Login → die alte Nachricht ist wieder lesbar.
|
||||
/// * Utas Update-Fall: eine Klartext-DB MIT ECHTER eingeloggter Session wird
|
||||
/// migriert; Session und Nachricht überleben, danach ist nichts mehr im
|
||||
/// Klartext auf der Platte.
|
||||
///
|
||||
/// NICHT abgedeckt (ehrlich): flutter_secure_storage (der Test injiziert den
|
||||
/// Schlüssel direkt – der Keystore-Pfad ist durch die echten App-Läufe auf
|
||||
/// Windows und dem Android-Emulator belegt) und E2EE-verschlüsselte Räume
|
||||
/// (kein vodozemac im Host-Test; der Testraum ist unverschlüsselt, was für
|
||||
/// die Klartext-Suche sogar der härtere Fall ist).
|
||||
|
||||
String? _findCipherDll() {
|
||||
for (final config in ['Debug', 'Profile', 'Release']) {
|
||||
final file = File('build/windows/x64/runner/$config/sqlite3.dll');
|
||||
if (file.existsSync()) return file.absolute.path;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
class _TestAccounts {
|
||||
_TestAccounts({
|
||||
required this.homeserver,
|
||||
required this.user1,
|
||||
required this.pass1,
|
||||
required this.user2,
|
||||
required this.pass2,
|
||||
});
|
||||
|
||||
final Uri homeserver;
|
||||
final String user1;
|
||||
final String pass1;
|
||||
final String user2;
|
||||
final String pass2;
|
||||
|
||||
static _TestAccounts? load() {
|
||||
final override = Platform.environment['PYRAMID_TEST_ACCOUNTS'];
|
||||
final profile = Platform.environment['USERPROFILE'] ?? '';
|
||||
final path = (override != null && override.isNotEmpty)
|
||||
? override
|
||||
: '$profile\\.pyramid-autopilot\\test-accounts.txt';
|
||||
final file = File(path);
|
||||
if (!file.existsSync()) return null;
|
||||
final map = <String, String>{};
|
||||
for (final line in file.readAsLinesSync()) {
|
||||
final trimmed = line.trim();
|
||||
if (trimmed.isEmpty || trimmed.startsWith('#')) continue;
|
||||
final idx = trimmed.indexOf('=');
|
||||
if (idx <= 0) continue;
|
||||
map[trimmed.substring(0, idx)] = trimmed.substring(idx + 1);
|
||||
}
|
||||
final hs = map['homeserver'];
|
||||
if (hs == null ||
|
||||
map['user1'] == null ||
|
||||
map['pass1'] == null ||
|
||||
map['user2'] == null ||
|
||||
map['pass2'] == null) {
|
||||
return null;
|
||||
}
|
||||
return _TestAccounts(
|
||||
homeserver: Uri.parse(hs),
|
||||
user1: map['user1']!,
|
||||
pass1: map['pass1']!,
|
||||
user2: map['user2']!,
|
||||
pass2: map['pass2']!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Zufälliger 32-Byte-Schlüssel wie in AppDatabase._getOrCreateKey – nur
|
||||
/// direkt injiziert statt über den Keystore (siehe Kopfkommentar).
|
||||
String _randomHexKey() {
|
||||
final rnd = Random.secure();
|
||||
return List.generate(
|
||||
32,
|
||||
(_) => rnd.nextInt(256).toRadixString(16).padLeft(2, '0'),
|
||||
).join();
|
||||
}
|
||||
|
||||
String _randomMarker(String prefix) {
|
||||
final rnd = Random.secure();
|
||||
final suffix =
|
||||
List.generate(8, (_) => rnd.nextInt(16).toRadixString(16)).join();
|
||||
return 'PYRAMID_${prefix}_PROBE_$suffix';
|
||||
}
|
||||
|
||||
/// Naive ASCII-Substring-Suche über rohe Datei-Bytes (DB + WAL). Bewusst kein
|
||||
/// String-Decode der Gesamtdatei: Binärdaten sind kein gültiges UTF-8.
|
||||
bool _bytesContain(Uint8List haystack, String needle) {
|
||||
final n = ascii.encode(needle);
|
||||
if (n.isEmpty || haystack.length < n.length) return false;
|
||||
outer:
|
||||
for (var i = 0; i <= haystack.length - n.length; i++) {
|
||||
for (var j = 0; j < n.length; j++) {
|
||||
if (haystack[i + j] != n[j]) continue outer;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Durchsucht Hauptdatei + WAL/SHM-Sidecars nach einem Klartext-String.
|
||||
Future<bool> _filesContain(String dbPath, String needle) async {
|
||||
for (final path in [dbPath, '$dbPath-wal', '$dbPath-shm']) {
|
||||
final file = File(path);
|
||||
if (!await file.exists()) continue;
|
||||
if (_bytesContain(await file.readAsBytes(), needle)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Öffnet die verschlüsselte DB mit Schlüssel und sucht den String in allen
|
||||
/// Tabelleninhalten – beweist, dass die Daten IN der verschlüsselten Datei
|
||||
/// stecken (und nicht etwa vom Server nachgeladen werden müssten).
|
||||
Future<bool> _encryptedDbContains(
|
||||
DatabaseFactory factory,
|
||||
String dbPath,
|
||||
String hexKey,
|
||||
String needle,
|
||||
) async {
|
||||
final db = await AppDatabase.openEncrypted(factory, dbPath, hexKey);
|
||||
try {
|
||||
final tables = await db.rawQuery(
|
||||
"SELECT name FROM sqlite_master "
|
||||
"WHERE type = 'table' AND name NOT LIKE 'sqlite_%'",
|
||||
);
|
||||
for (final row in tables) {
|
||||
final name = row['name'] as String;
|
||||
final rows = await db.rawQuery('SELECT * FROM "$name"');
|
||||
for (final r in rows) {
|
||||
for (final v in r.values) {
|
||||
if (v == null) continue;
|
||||
if (v is Uint8List) {
|
||||
if (_bytesContain(v, needle)) return true;
|
||||
} else if (v.toString().contains(needle)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
await db.close();
|
||||
}
|
||||
}
|
||||
|
||||
Future<Client> _newClient(
|
||||
Database db, {
|
||||
required String name,
|
||||
bool waitForRooms = false,
|
||||
}) async {
|
||||
final sdkDb = await MatrixSdkDatabase.init('pyramid', database: db);
|
||||
final client = Client(name, database: sdkDb);
|
||||
// waitForRooms: Die Raumliste wird beim Init ASYNCHRON aus der DB geladen –
|
||||
// wer direkt danach client.rooms prüfen will, muss darauf warten.
|
||||
await client.init(
|
||||
waitForFirstSync: false,
|
||||
waitUntilLoadCompletedLoaded: waitForRooms,
|
||||
);
|
||||
return client;
|
||||
}
|
||||
|
||||
Future<void> _login(Client client, _TestAccounts acc,
|
||||
{required String user, required String pass, required String device}) async {
|
||||
await client.checkHomeserver(acc.homeserver);
|
||||
await client.login(
|
||||
LoginType.mLoginPassword,
|
||||
identifier: AuthenticationUserIdentifier(user: user),
|
||||
password: pass,
|
||||
initialDeviceDisplayName: device,
|
||||
refreshToken: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// Holt (oder erzeugt) den unverschlüsselten Autopilot-Testraum.
|
||||
Future<Room> _ensureTestRoom(Client client) async {
|
||||
const roomName = 'Pyramid Autopilot Testraum';
|
||||
await client.oneShotSync().timeout(const Duration(seconds: 60));
|
||||
for (final room in client.rooms) {
|
||||
if (room.name == roomName && room.membership == Membership.join) {
|
||||
return room;
|
||||
}
|
||||
}
|
||||
final roomId = await client.createRoom(
|
||||
name: roomName,
|
||||
preset: CreateRoomPreset.privateChat,
|
||||
);
|
||||
await client.waitForRoomInSync(roomId, join: true).timeout(
|
||||
const Duration(seconds: 60),
|
||||
);
|
||||
final room = client.getRoomById(roomId);
|
||||
expect(room, isNotNull, reason: 'Testraum nach createRoom nicht im Sync');
|
||||
return room!;
|
||||
}
|
||||
|
||||
/// Sucht die Marker-Nachricht in der Timeline (lädt bei Bedarf Historie nach).
|
||||
Future<bool> _timelineContains(Room room, String marker) async {
|
||||
final timeline = await room.getTimeline();
|
||||
try {
|
||||
for (var i = 0; i < 10; i++) {
|
||||
if (timeline.events.any(
|
||||
(e) => e.type == EventTypes.Message && e.body.contains(marker),
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
if (!timeline.canRequestHistory) break;
|
||||
await timeline.requestHistory(historyCount: 100);
|
||||
}
|
||||
return timeline.events.any(
|
||||
(e) => e.type == EventTypes.Message && e.body.contains(marker),
|
||||
);
|
||||
} finally {
|
||||
timeline.cancelSubscriptions();
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
final dllPath = _findCipherDll();
|
||||
final accounts = _TestAccounts.load();
|
||||
final liveEnabled = Platform.environment['PYRAMID_LIVE_TEST'] == '1';
|
||||
|
||||
final skip = !liveEnabled
|
||||
? 'Live-Test nur mit PYRAMID_LIVE_TEST=1 (echter Homeserver!)'
|
||||
: dllPath == null
|
||||
? 'SQLCipher-DLL fehlt (erst "flutter build windows")'
|
||||
: accounts == null
|
||||
? 'Test-Account-Datei fehlt (~/.pyramid-autopilot/test-accounts.txt)'
|
||||
: null;
|
||||
|
||||
final factory = dllPath == null
|
||||
? null
|
||||
: createDatabaseFactoryFfi(
|
||||
ffiInit: () {
|
||||
sqlite_open.open.overrideFor(
|
||||
sqlite_open.OperatingSystem.windows,
|
||||
() => DynamicLibrary.open(dllPath),
|
||||
);
|
||||
},
|
||||
noIsolate: true,
|
||||
);
|
||||
|
||||
late Directory tempDir;
|
||||
|
||||
setUp(() async {
|
||||
tempDir = await Directory.systemTemp.createTemp('pyramid_live_test');
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
try {
|
||||
await tempDir.delete(recursive: true);
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
test(
|
||||
'Frische Installation: Login → Platte verschlüsselt → Neustart → '
|
||||
'Logout → Re-Login → alte Nachricht lesbar',
|
||||
() async {
|
||||
final acc = accounts!;
|
||||
final dbPath = '${tempDir.path}${Platform.pathSeparator}pyramid.sqlite';
|
||||
final hexKey = _randomHexKey();
|
||||
final marker = _randomMarker('ENC');
|
||||
|
||||
// ── Phase 1: frisch verschlüsselt anlegen, echter Login, Nachricht ──
|
||||
var db = await AppDatabase.openEncrypted(factory!, dbPath, hexKey);
|
||||
await db.execute('PRAGMA journal_mode=WAL');
|
||||
var client = await _newClient(db, name: 'PyramidLiveTest');
|
||||
await _login(client, acc,
|
||||
user: acc.user1, pass: acc.pass1, device: 'Autopilot Enc-Test');
|
||||
expect(client.isLogged(), isTrue);
|
||||
final accessToken = client.accessToken!;
|
||||
final room = await _ensureTestRoom(client);
|
||||
final eventId = await room.sendTextEvent(marker);
|
||||
expect(eventId, isNotNull, reason: 'Senden der Probe-Nachricht schlug fehl');
|
||||
await client.oneShotSync().timeout(const Duration(seconds: 60));
|
||||
await client.dispose(closeDatabase: true);
|
||||
|
||||
// ── Phase 2: Byte-Prüfung auf der Platte ──
|
||||
expect(await AppDatabase.isPlaintextSqlite(File(dbPath)), isFalse,
|
||||
reason: 'DB-Header ist "SQLite format 3" – NICHT verschlüsselt!');
|
||||
expect(await _filesContain(dbPath, accessToken), isFalse,
|
||||
reason: 'Access-Token im Klartext auf der Platte gefunden!');
|
||||
expect(await _filesContain(dbPath, marker), isFalse,
|
||||
reason: 'Nachrichtentext im Klartext auf der Platte gefunden!');
|
||||
|
||||
// Beweis, dass die Daten wirklich IN der Datei stecken (nur eben
|
||||
// verschlüsselt): Raw-Query mit Schlüssel findet beides.
|
||||
expect(
|
||||
await _encryptedDbContains(factory, dbPath, hexKey, accessToken),
|
||||
isTrue,
|
||||
reason: 'Access-Token fehlt in der DB – falsche Datei geprüft?',
|
||||
);
|
||||
expect(
|
||||
await _encryptedDbContains(factory, dbPath, hexKey, marker),
|
||||
isTrue,
|
||||
reason: 'Nachricht fehlt in der DB – falsche Datei geprüft?',
|
||||
);
|
||||
|
||||
// ── Phase 3: „Neustart" – Session kommt aus der verschlüsselten DB ──
|
||||
db = await AppDatabase.openEncrypted(factory, dbPath, hexKey);
|
||||
client = await _newClient(db, name: 'PyramidLiveTest');
|
||||
expect(client.isLogged(), isTrue,
|
||||
reason: 'Session hat den Neustart nicht überlebt');
|
||||
expect(client.accessToken, accessToken,
|
||||
reason: 'Access-Token nach Neustart nicht aus der DB restauriert');
|
||||
expect(client.userID?.toLowerCase(), acc.user1.toLowerCase());
|
||||
|
||||
// ── Phase 4: Logout → Re-Login → alte Nachricht wieder lesbar ──
|
||||
await client.logout();
|
||||
expect(client.isLogged(), isFalse);
|
||||
await _login(client, acc,
|
||||
user: acc.user1, pass: acc.pass1, device: 'Autopilot Enc-Test 2');
|
||||
final roomAfter = await _ensureTestRoom(client);
|
||||
expect(await _timelineContains(roomAfter, marker), isTrue,
|
||||
reason: 'Alte Nachricht nach Logout+Re-Login nicht mehr lesbar');
|
||||
// Aufräumen: Session serverseitig beenden, DB wird dabei geleert.
|
||||
await client.logout();
|
||||
await client.dispose(closeDatabase: true);
|
||||
},
|
||||
skip: skip,
|
||||
timeout: const Timeout(Duration(minutes: 8)),
|
||||
);
|
||||
|
||||
test(
|
||||
'Utas Update-Fall: Klartext-DB mit ECHTER Session → Migration → '
|
||||
'Session + Nachricht überleben, Platte danach dicht',
|
||||
() async {
|
||||
final acc = accounts!;
|
||||
final dbPath = '${tempDir.path}${Platform.pathSeparator}pyramid.sqlite';
|
||||
final hexKey = _randomHexKey();
|
||||
final marker = _randomMarker('MIG');
|
||||
|
||||
// ── Phase 1: Klartext-DB wie vor der SQLCipher-Einführung ──
|
||||
var db = await factory!.openDatabase(dbPath);
|
||||
await db.execute('PRAGMA journal_mode=WAL');
|
||||
var client = await _newClient(db, name: 'PyramidLiveTest');
|
||||
await _login(client, acc,
|
||||
user: acc.user1, pass: acc.pass1, device: 'Autopilot Mig-Test');
|
||||
final accessToken = client.accessToken!;
|
||||
final room = await _ensureTestRoom(client);
|
||||
final eventId = await room.sendTextEvent(marker);
|
||||
expect(eventId, isNotNull);
|
||||
await client.oneShotSync().timeout(const Duration(seconds: 60));
|
||||
await client.dispose(closeDatabase: true);
|
||||
|
||||
// Positiv-Kontrolle: In der Klartext-DB findet die Byte-Suche BEIDES –
|
||||
// damit ist bewiesen, dass die Suche Leaks erkennen würde.
|
||||
expect(await AppDatabase.isPlaintextSqlite(File(dbPath)), isTrue);
|
||||
expect(await _filesContain(dbPath, accessToken), isTrue,
|
||||
reason: 'Kontrolle kaputt: Token nicht mal im Klartext gefunden');
|
||||
expect(await _filesContain(dbPath, marker), isTrue,
|
||||
reason: 'Kontrolle kaputt: Nachricht nicht mal im Klartext gefunden');
|
||||
|
||||
// ── Phase 2: Migration (exakt der App-Codepfad) ──
|
||||
final ok = await AppDatabase.migratePlaintextToEncrypted(
|
||||
factory: factory,
|
||||
dbPath: dbPath,
|
||||
hexKey: hexKey,
|
||||
);
|
||||
expect(ok, isTrue, reason: 'Migration fehlgeschlagen');
|
||||
expect(await AppDatabase.isPlaintextSqlite(File(dbPath)), isFalse);
|
||||
expect(File('$dbPath.premigration').existsSync(), isTrue);
|
||||
expect(await _filesContain(dbPath, accessToken), isFalse,
|
||||
reason: 'Token nach Migration weiterhin im Klartext!');
|
||||
expect(await _filesContain(dbPath, marker), isFalse,
|
||||
reason: 'Nachricht nach Migration weiterhin im Klartext!');
|
||||
|
||||
// ── Phase 3: Session + Daten intakt? ──
|
||||
expect(
|
||||
await _encryptedDbContains(factory, dbPath, hexKey, marker),
|
||||
isTrue,
|
||||
reason: 'Nachricht hat die Migration nicht überlebt',
|
||||
);
|
||||
db = await AppDatabase.openEncrypted(factory, dbPath, hexKey);
|
||||
client = await _newClient(db, name: 'PyramidLiveTest', waitForRooms: true);
|
||||
expect(client.isLogged(), isTrue,
|
||||
reason: 'Session hat die Migration nicht überlebt');
|
||||
expect(client.accessToken, accessToken);
|
||||
final roomAfter = client.getRoomById(room.id);
|
||||
expect(roomAfter, isNotNull,
|
||||
reason: 'Testraum nach Migration nicht mehr in der DB');
|
||||
// Aufräumen (Session serverseitig beenden).
|
||||
await client.logout();
|
||||
await client.dispose(closeDatabase: true);
|
||||
},
|
||||
skip: skip,
|
||||
timeout: const Timeout(Duration(minutes: 8)),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user