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 <info@braid.business>
This commit is contained in:
The one with the braid 2024-01-02 19:35:30 +01:00
parent 9dea9fa50a
commit 461bb8b7a5
7 changed files with 135 additions and 56 deletions

View file

@ -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';

View file

@ -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<DatabaseApi> flutterMatrixSdkDatabaseBuilder(Client client) async {
MatrixSdkDatabase? database;
@ -52,63 +51,45 @@ Future<MatrixSdkDatabase> _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<void> _applySQLCipher(Database db, String cipher) =>
db.rawQuery("PRAGMA KEY='$cipher'");

View file

@ -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<String?> 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;
}

View file

@ -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/<id>/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}');
}

View file

@ -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() {}

View file

@ -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:

View file

@ -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