From 461bb8b7a5c03d2e16b2e4a6b8999faec595ed79 Mon Sep 17 00:00:00 2001 From: The one with the braid Date: Tue, 2 Jan 2024 19:35:30 +0100 Subject: [PATCH] GHSA-pcvr-f2f2-xq7m: add Desktop database encryption - refactor Database factory code - use cipher for Desktop FFI as well - build SQfLite factory with sqlcipher support Signed-off-by: The one with the braid --- lib/utils/client_manager.dart | 2 +- .../builder.dart} | 85 +++++++------------ .../cipher.dart | 44 ++++++++++ .../sqlite_ffi/io.dart | 49 +++++++++++ .../sqlite_ffi/stub.dart | 4 + pubspec.lock | 6 +- pubspec.yaml | 1 + 7 files changed, 135 insertions(+), 56 deletions(-) rename lib/utils/matrix_sdk_extensions/{flutter_matrix_sdk_database_builder.dart => flutter_matrix_dart_sdk_database/builder.dart} (50%) create mode 100644 lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart create mode 100644 lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/sqlite_ffi/io.dart create mode 100644 lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/sqlite_ffi/stub.dart diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index 2d171336a..600e85b7a 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -18,7 +18,7 @@ import 'package:fluffychat/utils/custom_image_resizer.dart'; import 'package:fluffychat/utils/init_with_restore.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'matrix_sdk_extensions/flutter_matrix_sdk_database_builder.dart'; +import 'matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart'; abstract class ClientManager { static const String clientNamespace = 'im.fluffychat.store.clients'; diff --git a/lib/utils/matrix_sdk_extensions/flutter_matrix_sdk_database_builder.dart b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart similarity index 50% rename from lib/utils/matrix_sdk_extensions/flutter_matrix_sdk_database_builder.dart rename to lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart index 3cc80e82e..9ed3220c4 100644 --- a/lib/utils/matrix_sdk_extensions/flutter_matrix_sdk_database_builder.dart +++ b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart @@ -1,11 +1,8 @@ -import 'dart:convert'; -import 'dart:math'; +import 'dart:io'; import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:matrix/matrix.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart' as ffi; @@ -16,6 +13,8 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'cipher.dart'; +import 'sqlite_ffi/stub.dart' if (dart.library.io) 'sqlite_ffi/io.dart'; Future flutterMatrixSdkDatabaseBuilder(Client client) async { MatrixSdkDatabase? database; @@ -52,63 +51,45 @@ Future _constructDatabase(Client client) async { html.window.navigator.storage?.persist(); return MatrixSdkDatabase(client.clientName); } + + final password = await getDatabaseCipher(); + + Database database; + Directory? fileStoragePath; + if (PlatformInfos.isDesktop) { - final path = await getApplicationSupportDirectory(); - return MatrixSdkDatabase( - client.clientName, - database: await ffi.databaseFactoryFfi.openDatabase( - '${path.path}/${client.clientName}', + final dbFactory = ffi.createDatabaseFactoryFfi(ffiInit: ffiInit); + + fileStoragePath = await getApplicationSupportDirectory(); + + database = await dbFactory.openDatabase( + '${fileStoragePath.path}/${client.clientName}', + options: OpenDatabaseOptions( + version: 1, + // pass + onConfigure: + password == null ? null : (db) => _applySQLCipher(db, password), ), - maxFileSize: 1024 * 1024 * 10, - fileStoragePath: path, - deleteFilesAfterDuration: const Duration(days: 30), ); - } + } else { + final path = await getApplicationSupportDirectory(); + final sqlFilePath = '$path/${client.clientName}.sqlite'; - final path = await getDatabasesPath(); - const passwordStorageKey = 'database_password'; - String? password; - - try { - // Workaround for secure storage is calling Platform.operatingSystem on web - if (kIsWeb) throw MissingPluginException(); - - const secureStorage = FlutterSecureStorage(); - final containsEncryptionKey = - await secureStorage.read(key: passwordStorageKey) != null; - if (!containsEncryptionKey) { - final rng = Random.secure(); - final list = Uint8List(32); - list.setAll(0, Iterable.generate(list.length, (i) => rng.nextInt(256))); - final newPassword = base64UrlEncode(list); - await secureStorage.write( - key: passwordStorageKey, - value: newPassword, - ); - } - // workaround for if we just wrote to the key and it still doesn't exist - password = await secureStorage.read(key: passwordStorageKey); - if (password == null) throw MissingPluginException(); - } on MissingPluginException catch (_) { - const FlutterSecureStorage() - .delete(key: passwordStorageKey) - .catchError((_) {}); - Logs().i('Database encryption is not supported on this platform'); - } catch (e, s) { - const FlutterSecureStorage() - .delete(key: passwordStorageKey) - .catchError((_) {}); - Logs().w('Unable to init database encryption', e, s); + fileStoragePath = await getTemporaryDirectory(); + database = await openDatabase( + sqlFilePath, + password: password, + ); } return MatrixSdkDatabase( client.clientName, - database: await openDatabase( - '$path/${client.clientName}', - password: password, - ), + database: database, maxFileSize: 1024 * 1024 * 10, - fileStoragePath: await getTemporaryDirectory(), + fileStoragePath: fileStoragePath, deleteFilesAfterDuration: const Duration(days: 30), ); } + +Future _applySQLCipher(Database db, String cipher) => + db.rawQuery("PRAGMA KEY='$cipher'"); diff --git a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart new file mode 100644 index 000000000..6656c777a --- /dev/null +++ b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart @@ -0,0 +1,44 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:flutter/services.dart'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:matrix/matrix.dart'; + +const _passwordStorageKey = 'database_password'; + +Future getDatabaseCipher() async { + String? password; + + try { + const secureStorage = FlutterSecureStorage(); + final containsEncryptionKey = + await secureStorage.read(key: _passwordStorageKey) != null; + if (!containsEncryptionKey) { + final rng = Random.secure(); + final list = Uint8List(32); + list.setAll(0, Iterable.generate(list.length, (i) => rng.nextInt(256))); + final newPassword = base64UrlEncode(list); + await secureStorage.write( + key: _passwordStorageKey, + value: newPassword, + ); + } + // workaround for if we just wrote to the key and it still doesn't exist + password = await secureStorage.read(key: _passwordStorageKey); + if (password == null) throw MissingPluginException(); + } on MissingPluginException catch (_) { + const FlutterSecureStorage() + .delete(key: _passwordStorageKey) + .catchError((_) {}); + Logs().i('Database encryption is not supported on this platform'); + } catch (e, s) { + const FlutterSecureStorage() + .delete(key: _passwordStorageKey) + .catchError((_) {}); + Logs().w('Unable to init database encryption', e, s); + } + + return password; +} diff --git a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/sqlite_ffi/io.dart b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/sqlite_ffi/io.dart new file mode 100644 index 000000000..4a18d8b78 --- /dev/null +++ b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/sqlite_ffi/io.dart @@ -0,0 +1,49 @@ +import 'dart:ffi'; +import 'dart:io'; +import 'dart:math'; + +import 'package:sqlite3/open.dart'; + +/// overrides the sqlite shared object / dynamic library with the SQLCipher one +/// +/// https://github.com/tekartik/sqflite/blob/master/sqflite_common_ffi/doc/encryption_support.md +void ffiInit() { + open.overrideFor(OperatingSystem.linux, sqlcipherOpen); +} + +DynamicLibrary sqlcipherOpen() { + // Taken from https://github.com/simolus3/sqlite3.dart/blob/e66702c5bec7faec2bf71d374c008d5273ef2b3b/sqlite3/lib/src/load_library.dart#L24 + // keeping Android here in case we should ever use FFI for Android too + if (Platform.isLinux || Platform.isAndroid) { + try { + return DynamicLibrary.open('libsqlcipher.so'); + } catch (_) { + if (Platform.isAndroid) { + // 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/libsqlite3.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'); + } + + rethrow; + } + } + if (Platform.isIOS) { + return DynamicLibrary.process(); + } + if (Platform.isMacOS) { + return DynamicLibrary.open('/usr/lib/libsqlite3.dylib'); + } + if (Platform.isWindows) { + return DynamicLibrary.open('sqlite3.dll'); + } + + throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}'); +} diff --git a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/sqlite_ffi/stub.dart b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/sqlite_ffi/stub.dart new file mode 100644 index 000000000..9c7315f58 --- /dev/null +++ b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/sqlite_ffi/stub.dart @@ -0,0 +1,4 @@ +/// overrides the sqlite shared object / dynamic library with the SQLCipher one +/// +/// https://github.com/tekartik/sqflite/blob/master/sqflite_common_ffi/doc/encryption_support.md +void ffiInit() {} diff --git a/pubspec.lock b/pubspec.lock index 0ab94d298..6e22f6a55 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1739,13 +1739,13 @@ packages: source: hosted version: "2.2.1" sqlite3: - dependency: transitive + dependency: "direct main" description: name: sqlite3 - sha256: db65233e6b99e99b2548932f55a987961bc06d82a31a0665451fa0b4fff4c3fb + sha256: c4a4c5a4b2a32e2d0f6837b33d7c91a67903891a5b7dbe706cf4b1f6b0c798c5 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.3.0" stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 35716fece..870b0ee2b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -81,6 +81,7 @@ dependencies: sqflite: ^2.3.0 sqflite_common_ffi: ^2.3.0+4 sqflite_sqlcipher: ^2.2.1 + sqlite3: ^2.3.0 swipe_to_action: ^0.2.0 tor_detector_web: ^1.1.0 uni_links: ^0.5.1