From 092c4d7766cbca677edc6c56c07fffae3a151d71 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 7 Jul 2026 14:29:35 +0000 Subject: [PATCH 1/3] build: update sqlite3 to 3.x and adapt encryption helper to build hooks --- .../sqflite_encryption_helper/io.dart | 80 ++++--------------- .../sqflite_encryption_helper/stub.dart | 25 +++--- pubspec.yaml | 4 +- 3 files changed, 32 insertions(+), 77 deletions(-) diff --git a/lib/src/database/sqflite_encryption_helper/io.dart b/lib/src/database/sqflite_encryption_helper/io.dart index 7c3148ad6..fb4901076 100644 --- a/lib/src/database/sqflite_encryption_helper/io.dart +++ b/lib/src/database/sqflite_encryption_helper/io.dart @@ -2,18 +2,14 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later -import 'dart:ffi'; import 'dart:io'; -import 'dart:math' show max; import 'package:matrix/matrix.dart'; import 'package:sqflite_common/sqlite_api.dart'; -import 'package:sqlite3/open.dart'; // ignore: unused-code /// A helper utility for SQfLite related encryption operations /// -/// * helps loading the required dynamic libraries - even on cursed systems /// * migrates unencrypted SQLite databases to SQLCipher /// * applies the PRAGMA key to a database and ensure it is properly loading class SQfLiteEncryptionHelper { @@ -32,69 +28,23 @@ class SQfLiteEncryptionHelper { required this.cipher, }); - /// Loads the correct [DynamicLibrary] required for SQLCipher + /// No-op, kept for backwards compatibility. /// - /// To be used with `package:sqlite3/open.dart`: - /// ```dart - /// void main() { - /// final factory = createDatabaseFactoryFfi( - /// ffiInit: SQfLiteEncryptionHelper.ffiInit, - /// ); - /// } + /// Since `sqlite3` 3.x the SQLite library is bundled via + /// [build hooks](https://dart.dev/tools/hooks) and can no longer be + /// overridden at runtime. To use SQLCipher, select it in the `hooks` + /// section of your application's `pubspec.yaml` instead: + /// ```yaml + /// hooks: + /// user_defines: + /// sqlite3: + /// source: sqlcipher /// ``` - static void ffiInit() => open.overrideForAll(_loadSQLCipherDynamicLibrary); - - static DynamicLibrary _loadSQLCipherDynamicLibrary() { - // Taken from https://github.com/simolus3/sqlite3.dart/blob/e66702c5bec7faec2bf71d374c008d5273ef2b3b/sqlite3/lib/src/load_library.dart#L24 - if (Platform.isAndroid) { - try { - return DynamicLibrary.open('libsqlcipher.so'); - } catch (_) { - // On some (especially old) Android devices, we somehow can't dlopen - // libraries shipped with the apk. We need to find the full path of the - // library (/data/data//lib/libsqlcipher.so) and open that one. - // For details, see https://github.com/simolus3/moor/issues/420 - final appIdAsBytes = File('/proc/self/cmdline').readAsBytesSync(); - - // app id ends with the first \0 character in here. - final endOfAppId = max(appIdAsBytes.indexOf(0), 0); - final appId = String.fromCharCodes(appIdAsBytes.sublist(0, endOfAppId)); - - return DynamicLibrary.open('/data/data/$appId/lib/libsqlcipher.so'); - } - } - if (Platform.isLinux) { - // *not my fault grumble* - // - // On many Linux systems, I encountered issues opening the system provided - // libsqlcipher.so. I hence decided to ship an own one - statically linked - // against a patched version of OpenSSL compiled with the correct options. - // - // This was the only way I reached to run on particular Fedora and Arch - // systems. - // - // Hours wasted : 12 - try { - return DynamicLibrary.open('libsqlcipher_flutter_libs_plugin.so'); - } catch (_) { - return DynamicLibrary.open('libsqlcipher.so'); - } - } - if (Platform.isIOS) { - return DynamicLibrary.process(); - } - if (Platform.isMacOS) { - return DynamicLibrary.open( - 'sqlcipher_flutter_libs.framework/Versions/Current/' - 'sqlcipher_flutter_libs', - ); - } - if (Platform.isWindows) { - return DynamicLibrary.open('libsqlcipher.dll'); - } - - throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}'); - } + @Deprecated( + 'sqlite3 is now loaded through build hooks. Select SQLCipher via the ' + 'hooks user_defines in your pubspec.yaml instead.', + ) + static void ffiInit() {} /// checks whether the database exists and is encrypted /// diff --git a/lib/src/database/sqflite_encryption_helper/stub.dart b/lib/src/database/sqflite_encryption_helper/stub.dart index f520893c0..f51514b51 100644 --- a/lib/src/database/sqflite_encryption_helper/stub.dart +++ b/lib/src/database/sqflite_encryption_helper/stub.dart @@ -6,7 +6,6 @@ import 'package:sqflite_common/sqlite_api.dart'; /// A helper utility for SQfLite related encryption operations /// -/// * helps loading the required dynamic libraries - even on cursed systems /// * migrates unencrypted SQLite databases to SQLCipher /// * applies the PRAGMA key to a database and ensure it is properly loading class SQfLiteEncryptionHelper { @@ -25,17 +24,23 @@ class SQfLiteEncryptionHelper { required this.cipher, }); - /// Loads the correct [DynamicLibrary] required for SQLCipher + /// No-op, kept for backwards compatibility. /// - /// To be used with `package:sqlite3/open.dart`: - /// ```dart - /// void main() { - /// final factory = createDatabaseFactoryFfi( - /// ffiInit: SQfLiteEncryptionHelper.ffiInit, - /// ); - /// } + /// Since `sqlite3` 3.x the SQLite library is bundled via + /// [build hooks](https://dart.dev/tools/hooks) and can no longer be + /// overridden at runtime. To use SQLCipher, select it in the `hooks` + /// section of your application's `pubspec.yaml` instead: + /// ```yaml + /// hooks: + /// user_defines: + /// sqlite3: + /// source: sqlcipher /// ``` - static void ffiInit() => throw UnimplementedError(); + @Deprecated( + 'sqlite3 is now loaded through build hooks. Select SQLCipher via the ' + 'hooks user_defines in your pubspec.yaml instead.', + ) + static void ffiInit() {} /// checks whether the database exists and is encrypted /// diff --git a/pubspec.yaml b/pubspec.yaml index ac2ad096e..993c071a3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,7 +30,7 @@ dependencies: sdp_transform: ^0.3.2 slugify: ^2.0.0 sqflite_common: ^2.4.5 - sqlite3: ^2.1.0 + sqlite3: ^3.3.4 typed_data: ^1.3.2 vodozemac: ^0.5.0 web: ^1.1.1 @@ -45,5 +45,5 @@ dev_dependencies: url: https://github.com/famedly/frontend-ci-templates.git path: lints/dart lints: any - sqflite_common_ffi: ^2.3.4+4 # sqflite_common_ffi aggressively requires newer dart versions + sqflite_common_ffi: ^2.4.0+3 # sqflite_common_ffi aggressively requires newer dart versions test: ^1.27.0 From 32c46bde05d6d0f78cb2c39ac6c2f49b50ac3cbd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 7 Jul 2026 14:47:46 +0000 Subject: [PATCH 2/3] build: ignore unused-dep warning for sqlite3 in dependency validator --- dart_dependency_validator.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dart_dependency_validator.yaml b/dart_dependency_validator.yaml index 35a241a11..c28827f4d 100644 --- a/dart_dependency_validator.yaml +++ b/dart_dependency_validator.yaml @@ -5,4 +5,8 @@ exclude: - example/** ignore: - - lints \ No newline at end of file + - lints + # Not imported directly anymore (sqlite3 3.x dropped `package:sqlite3/open.dart`), + # but kept as a direct dependency so downstream apps are forced onto the + # hooks-based sqlite3 3.x that SQLCipher selection now relies on. + - sqlite3 \ No newline at end of file From 458326253c04c23da11baa8687cf87886a14f978 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 7 Jul 2026 15:04:46 +0000 Subject: [PATCH 3/3] fix: fail loudly when migrating a database without SQLCipher available Also covers SQfLiteEncryptionHelper with tests. --- .../sqflite_encryption_helper/io.dart | 37 +++++--- test/sqflite_encryption_helper_test.dart | 91 +++++++++++++++++++ 2 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 test/sqflite_encryption_helper_test.dart diff --git a/lib/src/database/sqflite_encryption_helper/io.dart b/lib/src/database/sqflite_encryption_helper/io.dart index fb4901076..f078cc7b5 100644 --- a/lib/src/database/sqflite_encryption_helper/io.dart +++ b/lib/src/database/sqflite_encryption_helper/io.dart @@ -71,6 +71,16 @@ class SQfLiteEncryptionHelper { // hell, it's unencrypted. This should not happen. Time to encrypt it. final plainDb = await factory.openDatabase(path); + // Ensure SQLCipher is actually loaded before running the + // SQLCipher-specific migration statements below - they would otherwise + // fail with an unclear error. + try { + await _ensureSQLCipherAvailable(plainDb); + } catch (_) { + await plainDb.close(); + rethrow; + } + final encryptedPath = '$path.encrypted'; await plainDb.execute( @@ -107,25 +117,30 @@ class SQfLiteEncryptionHelper { /// * applies [cipher] as PRAGMA key /// * checks whether this operation was successful Future applyPragmaKey(Database database) async { + await _ensureSQLCipherAvailable(database); + + final result = await database.rawQuery("PRAGMA KEY='$cipher';"); + assert(result.single['ok'] == 'ok'); + } + + /// ensures the given [database] is actually backed by SQLCipher + /// + /// Throws a [StateError] otherwise, since the encryption PRAGMAs just fail + /// silently with regular sqlite3 (meaning that we'd accidentally use + /// plaintext databases). + Future _ensureSQLCipherAvailable(Database database) async { final cipherVersion = await database.rawQuery('PRAGMA cipher_version;'); if (cipherVersion.isEmpty) { - // Make sure that we're actually using SQLCipher, since the pragma - // used to encrypt databases just fails silently with regular - // sqlite3 - // (meaning that we'd accidentally use plaintext databases). throw StateError( 'SQLCipher library is not available, ' 'please check your dependencies!', ); - } else { - final version = cipherVersion.singleOrNull?['cipher_version']; - Logs().d( - 'PRAGMA supported by bundled SQLite. Encryption supported. SQLCipher version: $version.', - ); } - final result = await database.rawQuery("PRAGMA KEY='$cipher';"); - assert(result.single['ok'] == 'ok'); + final version = cipherVersion.singleOrNull?['cipher_version']; + Logs().d( + 'PRAGMA supported by bundled SQLite. Encryption supported. SQLCipher version: $version.', + ); } /// checks whether a File has a plain text SQLite header diff --git a/test/sqflite_encryption_helper_test.dart b/test/sqflite_encryption_helper_test.dart new file mode 100644 index 000000000..bd88c376d --- /dev/null +++ b/test/sqflite_encryption_helper_test.dart @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2019-Present Famedly GmbH +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +@TestOn('vm') +library; + +import 'dart:io'; + +import 'package:matrix/matrix.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:test/test.dart'; + +void main() { + group('SQfLiteEncryptionHelper', () { + late Directory tempDir; + late String dbPath; + late SQfLiteEncryptionHelper helper; + + setUp(() async { + tempDir = await Directory.systemTemp.createTemp('encryption_helper'); + dbPath = '${tempDir.path}/test.sqlite'; + helper = SQfLiteEncryptionHelper( + factory: databaseFactoryFfi, + path: dbPath, + cipher: 'secret', + ); + }); + + tearDown(() async { + await tempDir.delete(recursive: true); + }); + + test('ffiInit is a no-op', () { + // ignore: deprecated_member_use_from_same_package + expect(SQfLiteEncryptionHelper.ffiInit, returnsNormally); + }); + + test( + 'ensureDatabaseFileEncrypted does nothing without a database file', + () async { + await helper.ensureDatabaseFileEncrypted(); + expect(await File(dbPath).exists(), false); + }, + ); + + test( + 'ensureDatabaseFileEncrypted skips already encrypted databases', + () async { + // an encrypted database does not start with the plain text SQLite header + final bytes = List.generate(32, (i) => 255 - i); + await File(dbPath).writeAsBytes(bytes); + + await helper.ensureDatabaseFileEncrypted(); + + expect(await File(dbPath).readAsBytes(), bytes); + }, + ); + + test( + 'ensureDatabaseFileEncrypted fails loudly when SQLCipher is not available', + () async { + // create a plain text SQLite database + final db = await databaseFactoryFfi.openDatabase(dbPath); + await db.execute('CREATE TABLE cats (name TEXT)'); + await db.close(); + + // the bundled sqlite3 is not SQLCipher, so the migration must not + // silently do the wrong thing + await expectLater( + helper.ensureDatabaseFileEncrypted(), + throwsStateError, + ); + + // the plain database file is left untouched + expect(await File(dbPath).exists(), true); + }, + ); + + test( + 'applyPragmaKey fails loudly when SQLCipher is not available', + () async { + final db = await databaseFactoryFfi.openDatabase(dbPath); + + await expectLater(helper.applyPragmaKey(db), throwsStateError); + + await db.close(); + }, + ); + }); +}