The Linux app segfaulted when opening the encrypted DB. Root cause: on Linux we overrode package:sqlite3 to the SYSTEM libsqlcipher.so, but sqlcipher_flutter_libs statically links its own SQLCipher into libsqlcipher_flutter_libs_plugin.so and exports the sqlite3_* symbols globally. Two SQLite/SQLCipher builds loaded in one process → symbol clash → segfault at sqlite3_open (before PRAGMA key). Fix: _openLinuxCipher() now prefers the ABI-matched bundled plugin .so (libsqlcipher_flutter_libs_plugin.so), falling back to the system lib only if the plugin isn't present (e.g. host test/CI layouts). Android is unaffected (uses the bundled Android lib as before). Verified: Linux desktop build runs without crashing; full suite 37/37, and the "no plaintext at rest" test now runs (not skipped) on the dev host with system libsqlcipher, confirming real encryption.
74 lines
2.7 KiB
Dart
74 lines
2.7 KiB
Dart
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.',
|
|
);
|
|
}
|
|
}
|