chore: unify desktop and mobile databases (#764)
* chore: unify desktop and mobile databases - migrate `package:sqflite_flutter` to `sqlcipher_flutter_libs` - use FFI for all SQLite operations - use `SQfLiteEncryptionHelper` for database encryption - enforce encryption for new SQLite datbase implementation - migrate existing SQLite databases - encrypt unencrypted ones - migrate database locations to unified approach - drop dependency on sqlite Signed-off-by: The one with the braid <info@braid.business> * chore: add sqlcipher to macos CI Signed-off-by: The one with the braid <info@braid.business> --------- Signed-off-by: The one with the braid <info@braid.business>krille/new-html-formatting rc1.19.0-2
parent
3e9ff75efe
commit
3c532f90ba
@ -0,0 +1,125 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
|
||||
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 'sqlcipher_stub.dart'
|
||||
if (dart.library.io) 'package:sqlcipher_flutter_libs/sqlcipher_flutter_libs.dart';
|
||||
|
||||
Future<DatabaseApi> flutterMatrixSdkDatabaseBuilder(Client client) async {
|
||||
MatrixSdkDatabase? database;
|
||||
try {
|
||||
database = await _constructDatabase(client);
|
||||
await database.open();
|
||||
return database;
|
||||
} catch (e) {
|
||||
// Try to delete database so that it can created again on next init:
|
||||
database?.delete().catchError(
|
||||
(e, s) => Logs().w(
|
||||
'Unable to delete database, after failed construction',
|
||||
e,
|
||||
s,
|
||||
),
|
||||
);
|
||||
|
||||
// Send error notification:
|
||||
final l10n = lookupL10n(PlatformDispatcher.instance.locale);
|
||||
ClientManager.sendInitNotification(
|
||||
l10n.initAppError,
|
||||
l10n.databaseBuildErrorBody(
|
||||
AppConfig.newIssueUrl.toString(),
|
||||
e.toString(),
|
||||
),
|
||||
);
|
||||
|
||||
return FlutterHiveCollectionsDatabase.databaseBuilder(client);
|
||||
}
|
||||
}
|
||||
|
||||
Future<MatrixSdkDatabase> _constructDatabase(Client client) async {
|
||||
if (kIsWeb) {
|
||||
html.window.navigator.storage?.persist();
|
||||
return MatrixSdkDatabase(client.clientName);
|
||||
}
|
||||
|
||||
final cipher = await getDatabaseCipher();
|
||||
|
||||
final fileStoragePath = PlatformInfos.isIOS || PlatformInfos.isMacOS
|
||||
? await getLibraryDirectory()
|
||||
: await getApplicationSupportDirectory();
|
||||
|
||||
final path = join(fileStoragePath.path, '${client.clientName}.sqlite');
|
||||
|
||||
// fix dlopen for old Android
|
||||
await applyWorkaroundToOpenSqlCipherOnOldAndroidVersions();
|
||||
// import the SQLite / SQLCipher shared objects / dynamic libraries
|
||||
final factory =
|
||||
createDatabaseFactoryFfi(ffiInit: SQfLiteEncryptionHelper.ffiInit);
|
||||
|
||||
// migrate from potential previous SQLite database path to current one
|
||||
await _migrateLegacyLocation(path, client.clientName);
|
||||
|
||||
// required for [getDatabasesPath]
|
||||
databaseFactory = factory;
|
||||
|
||||
// in case we got a cipher, we use the encryption helper
|
||||
// to manage SQLite encryption
|
||||
final helper = SQfLiteEncryptionHelper(
|
||||
factory: factory,
|
||||
path: path,
|
||||
cipher: cipher,
|
||||
);
|
||||
|
||||
// check whether the DB is already encrypted and otherwise do so
|
||||
await helper.ensureDatabaseFileEncrypted();
|
||||
|
||||
final database = await factory.openDatabase(
|
||||
path,
|
||||
options: OpenDatabaseOptions(
|
||||
version: 1,
|
||||
// most important : apply encryption when opening the DB
|
||||
onConfigure: helper.applyPragmaKey,
|
||||
),
|
||||
);
|
||||
|
||||
return MatrixSdkDatabase(
|
||||
client.clientName,
|
||||
database: database,
|
||||
maxFileSize: 1024 * 1024 * 10,
|
||||
fileStoragePath: fileStoragePath,
|
||||
deleteFilesAfterDuration: const Duration(days: 30),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _migrateLegacyLocation(
|
||||
String sqlFilePath,
|
||||
String clientName,
|
||||
) async {
|
||||
final oldPath = PlatformInfos.isDesktop
|
||||
? (await getApplicationSupportDirectory()).path
|
||||
: await getDatabasesPath();
|
||||
|
||||
final oldFilePath = join(oldPath, clientName);
|
||||
if (oldFilePath == sqlFilePath) return;
|
||||
|
||||
final maybeOldFile = File(oldFilePath);
|
||||
if (await maybeOldFile.exists()) {
|
||||
Logs().i(
|
||||
'Migrate legacy location for database from "$oldFilePath" to "$sqlFilePath"',
|
||||
);
|
||||
await maybeOldFile.copy(sqlFilePath);
|
||||
await maybeOldFile.delete();
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
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);
|
||||
}
|
||||
|
||||
// with the new database, we should no longer allow unencrypted storage
|
||||
// secure_storage now supports all platforms we support
|
||||
assert(password != null);
|
||||
|
||||
return password!;
|
||||
}
|
@ -0,0 +1 @@
|
||||
Future<void> applyWorkaroundToOpenSqlCipherOnOldAndroidVersions() async {}
|
@ -1,107 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
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;
|
||||
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
|
||||
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';
|
||||
|
||||
Future<DatabaseApi> flutterMatrixSdkDatabaseBuilder(Client client) async {
|
||||
MatrixSdkDatabase? database;
|
||||
try {
|
||||
database = await _constructDatabase(client);
|
||||
await database.open();
|
||||
return database;
|
||||
} catch (e, s) {
|
||||
Logs().wtf('Unable to build database!', e, s);
|
||||
|
||||
// Send error notification:
|
||||
final l10n = lookupL10n(PlatformDispatcher.instance.locale);
|
||||
ClientManager.sendInitNotification(
|
||||
l10n.initAppError,
|
||||
l10n.databaseBuildErrorBody(
|
||||
AppConfig.newIssueUrl.toString(),
|
||||
e.toString(),
|
||||
),
|
||||
);
|
||||
|
||||
return FlutterHiveCollectionsDatabase.databaseBuilder(client);
|
||||
}
|
||||
}
|
||||
|
||||
Future<MatrixSdkDatabase> _constructDatabase(Client client) async {
|
||||
if (kIsWeb) {
|
||||
html.window.navigator.storage?.persist();
|
||||
return MatrixSdkDatabase(client.clientName);
|
||||
}
|
||||
if (PlatformInfos.isDesktop) {
|
||||
final path = await getApplicationSupportDirectory();
|
||||
return MatrixSdkDatabase(
|
||||
client.clientName,
|
||||
database: await ffi.databaseFactoryFfi.openDatabase(
|
||||
'${path.path}/${client.clientName}',
|
||||
),
|
||||
maxFileSize: 1024 * 1024 * 10,
|
||||
fileStoragePath: path,
|
||||
deleteFilesAfterDuration: const Duration(days: 30),
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return MatrixSdkDatabase(
|
||||
client.clientName,
|
||||
database: await openDatabase(
|
||||
'$path/${client.clientName}',
|
||||
password: password,
|
||||
),
|
||||
maxFileSize: 1024 * 1024 * 10,
|
||||
fileStoragePath: await getTemporaryDirectory(),
|
||||
deleteFilesAfterDuration: const Duration(days: 30),
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue