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:
parent
9dea9fa50a
commit
461bb8b7a5
7 changed files with 135 additions and 56 deletions
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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'");
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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}');
|
||||
}
|
||||
|
|
@ -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() {}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue