import 'dart:ffi'; import 'dart:io'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:sunny_mochi/core/storage/db_key_provider.dart'; import 'package:sunny_mochi/core/storage/tables/error_logs_table.dart'; import 'package:sunny_mochi/core/storage/tables/users_table.dart'; import 'package:sunny_mochi/core/sync/sync_status.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqlcipher_flutter_libs/sqlcipher_flutter_libs.dart'; // ignore: depend_on_referenced_packages — sqlite3 是 sqlcipher_flutter_libs 的传递依赖 import 'package:sqlite3/open.dart' as sqlite3_open; // ignore: depend_on_referenced_packages — sqlite3 是 sqlcipher_flutter_libs 的传递依赖 import 'package:sqlite3/sqlite3.dart' as sqlite3_pkg; part 'app_database.g.dart'; /// SQLCipher 库注册函数,同时用于主 Isolate 和后台 Isolate。 /// /// 必须是**顶层函数**(不能是匿名闭包)——Drift 通过 Isolate.spawn 将其传递给 /// 后台 Isolate 时走 SendPort.send 序列化路径,顶层函数引用保证可靠传递; /// 匿名闭包在某些 Drift 2.x / Dart VM 版本组合下会静默失效(无错误,仅 setup /// 不执行),导致后台 Isolate 继续使用系统 plain sqlite3。 void _sqlCipherIsolateSetup() { if (kIsWeb) return; if (Platform.isAndroid) { sqlite3_open.open.overrideFor( sqlite3_open.OperatingSystem.android, openCipherOnAndroid, ); } else if (Platform.isIOS) { sqlite3_open.open.overrideFor( sqlite3_open.OperatingSystem.iOS, DynamicLibrary.process, ); } else if (Platform.isMacOS) { sqlite3_open.open.overrideFor( sqlite3_open.OperatingSystem.macOS, DynamicLibrary.process, ); } } /// 应用本地加密数据库(Drift + SQLCipher AES-256)。 /// /// 脚手架简化版(schemaVersion = 1): /// - 只包含 UsersTable 和 ErrorLogsTable /// - 无历史迁移路径,初始 v1 schema 即为全量 schema /// /// 启动序列: /// 1. 注册 SQLCipher 原生库(主 Isolate + 后台 Isolate 各自注册) /// 2. 拿到加密密钥(来自 [DbKeyProvider],详见 T-0.5) /// 3. 探测现有文件是否可用当前密钥打开;失败则删除重建 /// 4. NativeDatabase setup 时执行 `PRAGMA key = '...'` 解锁 /// /// 详见 docs/flutter-architecture-design.md §九.1.1 + §十一.5。 @DriftDatabase(tables: [Users, ErrorLogs]) class AppDatabase extends _$AppDatabase { AppDatabase._(super.e); /// 测试专用:内存 SQLite(不走 SQLCipher,用于纯 schema/逻辑测试)。 /// /// 生产路径必须用 [open](SQLCipher AES-256)。 @visibleForTesting factory AppDatabase.testInMemory() => AppDatabase._(NativeDatabase.memory()); static Future open({DbKeyProvider? keyProvider}) async { // 必须在主 Isolate 执行(内部用 MethodChannel):确保 libsqlcipher.so 已通过 // Java System.loadLibrary 加载到进程内存,后台 Isolate 的 dlopen 才能找到它。 if (!kIsWeb && Platform.isAndroid) { await applyWorkaroundToOpenSqlCipherOnOldAndroidVersions(); } // 主 Isolate 也注册 SQLCipher override,供下方探测检查使用。 // 各 Isolate 全局状态独立,此处设置不影响后台 Isolate(由 isolateSetup 负责)。 if (!kIsWeb) _sqlCipherIsolateSetup(); final dir = await getApplicationDocumentsDirectory(); final file = File(p.join(dir.path, 'app.db')); final key = await (keyProvider ?? DbKeyProvider()).key; final escaped = key.replaceAll("'", "''"); // 探测:在后台 Isolate 启动前,用当前密钥尝试打开现有文件。 // 捕获所有不可恢复情况: // - plain SQLite 遗留文件(未加密,PRAGMA key 被忽略) // - SQLCipher 文件但密钥不匹配(SecureStorage 重置等场景) // - 文件损坏 // 检测失败直接删除 —— 开发阶段文件无不可恢复的用户数据。 if (!kIsWeb && file.existsSync()) { if (!_canOpenWithKey(file.path, escaped)) { file.deleteSync(); } } return AppDatabase._( NativeDatabase.createInBackground( file, // 顶层函数引用(见 _sqlCipherIsolateSetup 注释) isolateSetup: _sqlCipherIsolateSetup, setup: (db) { // R-SEC-1 / 2026-05-10:PRAGMA key 字符串格式 — 必须做 SQL 单引号转义, // 防止未来策略升级(PBKDF2 / 服务端下发)后 key 含 ' 字符触发注入。 // // 注意:不切换到 SQLCipher 推荐的 raw key HEX 格式(x'...'),原因: // 该格式要求恰好 64 个 hex 字符(32 字节 raw key),现有 RandomKeyStrategy // 产出的 base64Url(32 bytes) ≈ 43 字符不符合,切换会破坏已加密 DB 的兼容。 // 完整迁移到 raw key 需独立设计(含数据迁移路径),见 readiness checklist。 db.execute("PRAGMA key = '$escaped';"); }, ), ); } /// 用当前密钥同步探测文件是否可正常读取。 /// /// 在主 Isolate 执行(open() 在 runApp 之前 await),不阻塞 UI。 /// 主 Isolate 已通过 [_sqlCipherIsolateSetup] 注册 SQLCipher, /// 所以 sqlite3.open 走的是 SQLCipher,与后台 Isolate 行为一致。 static bool _canOpenWithKey(String path, String escapedKey) { try { final probe = sqlite3_pkg.sqlite3.open(path); try { probe ..execute("PRAGMA key = '$escapedKey';") ..select('PRAGMA user_version;'); return true; } finally { probe.dispose(); } } on Exception catch (_) { return false; } } @override int get schemaVersion => 1; @override MigrationStrategy get migration => MigrationStrategy( onCreate: (m) => m.createAll(), ); } /// 全局 AppDatabase provider。在 main.dart 通过 override 注入实例。 @Riverpod(keepAlive: true) AppDatabase appDatabase(Ref ref) => throw UnimplementedError( 'appDatabaseProvider must be overridden in main.dart', );