import 'dart:ffi'; import 'dart:io'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:sqlcipher_flutter_libs/sqlcipher_flutter_libs.dart'; import 'package:sqlite3/open.dart'; import 'package:sqlite3/sqlite3.dart'; /// Routes package:sqlite3 to the **SQLCipher** build instead of plain SQLite. /// /// - Android: the bundled SQLCipher `.so` (from sqlcipher_flutter_libs). /// - Linux: the system `libsqlcipher.so` (dev machines / CI install it). /// - iOS & macOS: SQLCipher is linked into the app binary — no override needed. void useSqlCipher() { open ..overrideFor(OperatingSystem.android, openCipherOnAndroid) ..overrideFor(OperatingSystem.linux, _openLinuxCipher); } DynamicLibrary _openLinuxCipher() { // sqlcipher_flutter_libs statically links SQLCipher inside its plugin .so and // exports the sqlite3_* symbols — and it is ABI-matched to the `sqlite3` Dart // package. Prefer it; the system libsqlcipher is a different build (linked to // system libcrypto) and segfaults on open. Fall back to the system lib only // if the bundled plugin isn't present (e.g. some test/CI layouts). const candidates = [ 'libsqlcipher_flutter_libs_plugin.so', 'libsqlcipher.so', 'libsqlcipher.so.1', 'libsqlcipher.so.0', ]; Object? lastError; for (final name in candidates) { try { return DynamicLibrary.open(name); } on ArgumentError catch (e) { lastError = e; } } throw StateError('Could not load SQLCipher (tried $candidates): $lastError'); } /// Opens [file] as an encrypted database using the raw 256-bit [keyHex]. /// /// Verifies SQLCipher is actually linked (`PRAGMA cipher_version`) and refuses /// to fall back to plaintext — enforcing "no plaintext at rest, ever". QueryExecutor openEncryptedExecutor(File file, String keyHex) { return LazyDatabase(() async { if (Platform.isAndroid) { await applyWorkaroundToOpenSqlCipherOnOldAndroidVersions(); } return NativeDatabase.createInBackground( file, isolateSetup: useSqlCipher, setup: (db) => applyKeyAndVerify(db, keyHex), ); }); } /// Applies the SQLCipher key to [db] and asserts encryption is active. /// /// `x'…'` passes the key as raw bytes, skipping the KDF (our key is already a /// random 256-bit value from the OS keystore). Exposed for the security test. void applyKeyAndVerify(Database db, String keyHex) { db.execute('PRAGMA key = "x\'$keyHex\'";'); final cipher = db.select('PRAGMA cipher_version;'); if (cipher.isEmpty) { throw StateError( 'SQLCipher is not linked: PRAGMA cipher_version is empty. Encryption at ' 'rest is mandatory — refusing to open a plaintext database.', ); } }