Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion dart_dependency_validator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@
exclude:
- example/**
ignore:
- lints
- 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
117 changes: 41 additions & 76 deletions lib/src/database/sqflite_encryption_helper/io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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/<id>/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}');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Migration lacks SQLCipher availability check

Medium Severity

This commit makes ffiInit a deprecated no-op, so SQLCipher is no longer loaded before database use. applyPragmaKey still validates PRAGMA cipher_version, but ensureDatabaseFileEncrypted runs ATTACH/sqlcipher_export on the opened database without that check, so apps that still rely on ffiInit or omit build hooks hit unclear migration failures instead of the same explicit error.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 32c46bd. Configure here.

@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
///
Expand All @@ -121,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(
Expand Down Expand Up @@ -157,25 +117,30 @@ class SQfLiteEncryptionHelper {
/// * applies [cipher] as PRAGMA key
/// * checks whether this operation was successful
Future<void> 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<void> _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
Expand Down
25 changes: 15 additions & 10 deletions lib/src/database/sqflite_encryption_helper/stub.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
///
Expand Down
4 changes: 2 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not ^3.0.0?

typed_data: ^1.3.2
vodozemac: ^0.5.0
web: ^1.1.1
Expand All @@ -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
91 changes: 91 additions & 0 deletions test/sqflite_encryption_helper_test.dart
Original file line number Diff line number Diff line change
@@ -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<int>.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();
},
);
});
}
Loading