Compare commits
2 commits
main
...
braid/sqli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
949b640c81 | ||
|
|
461bb8b7a5 |
7 changed files with 153 additions and 55 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/init_with_restore.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart';
|
import 'package:fluffychat/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart';
|
||||||
import 'package:fluffychat/utils/platform_infos.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 {
|
abstract class ClientManager {
|
||||||
static const String clientNamespace = 'im.fluffychat.store.clients';
|
static const String clientNamespace = 'im.fluffychat.store.clients';
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
import 'dart:convert';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.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:matrix/matrix.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart' as ffi;
|
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/client_manager.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart';
|
import 'package:fluffychat/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart';
|
||||||
import 'package:fluffychat/utils/platform_infos.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 {
|
Future<DatabaseApi> flutterMatrixSdkDatabaseBuilder(Client client) async {
|
||||||
MatrixSdkDatabase? database;
|
MatrixSdkDatabase? database;
|
||||||
|
|
@ -52,63 +51,64 @@ Future<MatrixSdkDatabase> _constructDatabase(Client client) async {
|
||||||
html.window.navigator.storage?.persist();
|
html.window.navigator.storage?.persist();
|
||||||
return MatrixSdkDatabase(client.clientName);
|
return MatrixSdkDatabase(client.clientName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final password = await getDatabaseCipher();
|
||||||
|
|
||||||
|
Database database;
|
||||||
|
Directory? fileStoragePath;
|
||||||
|
|
||||||
if (PlatformInfos.isDesktop) {
|
if (PlatformInfos.isDesktop) {
|
||||||
final path = await getApplicationSupportDirectory();
|
final dbFactory = ffi.createDatabaseFactoryFfi(ffiInit: ffiInit);
|
||||||
return MatrixSdkDatabase(
|
|
||||||
client.clientName,
|
fileStoragePath = await getApplicationSupportDirectory();
|
||||||
database: await ffi.databaseFactoryFfi.openDatabase(
|
|
||||||
'${path.path}/${client.clientName}',
|
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();
|
// migrating from petty unreliable `getDatabasePath`
|
||||||
const passwordStorageKey = 'database_password';
|
// See : https://pub.dev/packages/sqflite_common_ffi#limitations
|
||||||
String? password;
|
await _migrateLegacyLocation(sqlFilePath, client.clientName);
|
||||||
|
|
||||||
try {
|
fileStoragePath = await getTemporaryDirectory();
|
||||||
// Workaround for secure storage is calling Platform.operatingSystem on web
|
database = await openDatabase(
|
||||||
if (kIsWeb) throw MissingPluginException();
|
sqlFilePath,
|
||||||
|
password: password,
|
||||||
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(
|
return MatrixSdkDatabase(
|
||||||
client.clientName,
|
client.clientName,
|
||||||
database: await openDatabase(
|
database: database,
|
||||||
'$path/${client.clientName}',
|
|
||||||
password: password,
|
|
||||||
),
|
|
||||||
maxFileSize: 1024 * 1024 * 10,
|
maxFileSize: 1024 * 1024 * 10,
|
||||||
fileStoragePath: await getTemporaryDirectory(),
|
fileStoragePath: fileStoragePath,
|
||||||
deleteFilesAfterDuration: const Duration(days: 30),
|
deleteFilesAfterDuration: const Duration(days: 30),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _migrateLegacyLocation(
|
||||||
|
String sqlFilePath,
|
||||||
|
String clientName,
|
||||||
|
) async {
|
||||||
|
final oldPath = await getDatabasesPath();
|
||||||
|
final oldFilePath = '$oldPath/$clientName.sqlite';
|
||||||
|
if (oldFilePath == sqlFilePath) return;
|
||||||
|
|
||||||
|
final maybeOldFile = File(oldFilePath);
|
||||||
|
if (await maybeOldFile.exists()) {
|
||||||
|
await maybeOldFile.copy(sqlFilePath);
|
||||||
|
await maybeOldFile.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.1"
|
||||||
sqlite3:
|
sqlite3:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: sqlite3
|
name: sqlite3
|
||||||
sha256: db65233e6b99e99b2548932f55a987961bc06d82a31a0665451fa0b4fff4c3fb
|
sha256: c4a4c5a4b2a32e2d0f6837b33d7c91a67903891a5b7dbe706cf4b1f6b0c798c5
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.3.0"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ dependencies:
|
||||||
sqflite: ^2.3.0
|
sqflite: ^2.3.0
|
||||||
sqflite_common_ffi: ^2.3.0+4
|
sqflite_common_ffi: ^2.3.0+4
|
||||||
sqflite_sqlcipher: ^2.2.1
|
sqflite_sqlcipher: ^2.2.1
|
||||||
|
sqlite3: ^2.3.0
|
||||||
swipe_to_action: ^0.2.0
|
swipe_to_action: ^0.2.0
|
||||||
tor_detector_web: ^1.1.0
|
tor_detector_web: ^1.1.0
|
||||||
uni_links: ^0.5.1
|
uni_links: ^0.5.1
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue