Files
flutter-template/lib/core/storage/app_database.dart
T
SkyJourney b8b63282ee fix: 清除 platform-flutter 残留代码,重构文档与开发者体验
P0 代码修复:删除 imSdkAppId、清除内网 IP 与原项目域名、修正 USE_MOCK 默认值
P1 CLAUDE.md 重组:零容忍规则前置,新增 dart-defines 字段对照表
P2 文档优化:先跑再配的 setup 流程,新增 architecture.md 架构图
2026-05-14 13:21:06 +08:00

153 lines
6.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = '...'` 解锁
///
@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<AppDatabase> 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-10PRAGMA 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',
);