diff --git a/lib/main.dart b/lib/main.dart index ad8fd3f3..11acf2d2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -37,7 +37,7 @@ void main() async { Zone.current.handleUncaughtError(details.exception, details.stack); final clients = await ClientManager.getClients(); - Logs().level = kDebugMode ? Level.debug : Level.warning; + Logs().level = kReleaseMode ? Level.info : Level.verbose; if (PlatformInfos.isMobile) { BackgroundPush.clientOnly(clients.first); diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index e196a161..c70f1356 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -14,10 +14,8 @@ import 'package:uni_links/uni_links.dart'; import 'package:vrouter/vrouter.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/pages/chat_list/chat_list_view.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions.dart/flutter_matrix_hive_database.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import '../../../utils/account_bundles.dart'; import '../../main.dart'; @@ -175,11 +173,6 @@ class ChatListController extends State { void checkBootstrap() async { if (!Matrix.of(context).client.encryptionEnabled) return; - if ((Matrix.of(context).client.database as FlutterMatrixHiveStore) - .get(SettingKeys.dontAskForBootstrapKey) == - true) { - return; - } final crossSigning = await crossSigningCachedFuture; final needsBootstrap = Matrix.of(context).client.encryption?.crossSigning?.enabled == false || diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index 097fe30a..8201ce1d 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -10,6 +10,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'famedlysdk_store.dart'; import 'matrix_sdk_extensions.dart/flutter_matrix_hive_database.dart'; +import 'matrix_sdk_extensions.dart/flutter_matrix_sembast_database.dart'; abstract class ClientManager { static const String clientNamespace = 'im.fluffychat.store.clients'; @@ -74,10 +75,9 @@ abstract class ClientManager { if (PlatformInfos.isMobile || PlatformInfos.isLinux) KeyVerificationMethod.emoji, }, - importantStateEvents: { - 'im.ponies.room_emotes', // we want emotes to work properly - }, - databaseBuilder: FlutterMatrixHiveStore.hiveDatabaseBuilder, + importantStateEvents: {'im.ponies.room_emotes'}, + databaseBuilder: FlutterMatrixSembastDatabase.databaseBuilder, + legacyDatabaseBuilder: FlutterMatrixHiveStore.hiveDatabaseBuilder, supportedLoginTypes: { AuthenticationTypes.password, if (PlatformInfos.isMobile || PlatformInfos.isWeb) diff --git a/lib/utils/matrix_sdk_extensions.dart/flutter_matrix_hive_database.dart b/lib/utils/matrix_sdk_extensions.dart/flutter_matrix_hive_database.dart index ef56855d..cbe14b2e 100644 --- a/lib/utils/matrix_sdk_extensions.dart/flutter_matrix_hive_database.dart +++ b/lib/utils/matrix_sdk_extensions.dart/flutter_matrix_hive_database.dart @@ -20,32 +20,9 @@ class FlutterMatrixHiveStore extends FamedlySdkHiveDatabase { encryptionCipher: encryptionCipher, ); - Box _customBox; - String get _customBoxName => '$name.box.custom'; - static bool _hiveInitialized = false; static const String _hiveCipherStorageKey = 'hive_encryption_key'; - @override - Future open() async { - await super.open(); - _customBox = await Hive.openBox( - _customBoxName, - encryptionCipher: encryptionCipher, - ); - return; - } - - @override - Future clear() async { - await super.clear(); - await _customBox.deleteAll(_customBox.keys); - await _customBox.close(); - } - - dynamic get(dynamic key) => _customBox.get(key); - Future put(dynamic key, dynamic value) => _customBox.put(key, value); - static Future hiveDatabaseBuilder( Client client) async { if (!kIsWeb && !_hiveInitialized) { diff --git a/lib/utils/matrix_sdk_extensions.dart/flutter_matrix_sembast_database.dart b/lib/utils/matrix_sdk_extensions.dart/flutter_matrix_sembast_database.dart new file mode 100644 index 00000000..954eee56 --- /dev/null +++ b/lib/utils/matrix_sdk_extensions.dart/flutter_matrix_sembast_database.dart @@ -0,0 +1,214 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart' hide Key; +import 'package:flutter/services.dart'; + +import 'package:encrypt/encrypt.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:matrix/matrix.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:sembast/sembast.dart'; +import 'package:sembast/sembast_io.dart'; +import 'package:sembast_web/sembast_web.dart'; + +import '../platform_infos.dart'; + +class FlutterMatrixSembastDatabase extends MatrixSembastDatabase { + FlutterMatrixSembastDatabase( + String name, { + SembastCodec codec, + String path, + DatabaseFactory dbFactory, + }) : super( + name, + codec: codec, + path: path, + dbFactory: dbFactory, + ); + + static const String _cipherStorageKey = 'sembast_encryption_key'; + static const int _cipherStorageKeyLength = 512; + + static Future databaseBuilder( + Client client) async { + Logs().d('Open Sembast...'); + SembastCodec codec; + try { + // Workaround for secure storage is calling Platform.operatingSystem on web + if (kIsWeb) throw MissingPluginException(); + + const secureStorage = FlutterSecureStorage(); + final containsEncryptionKey = + await secureStorage.containsKey(key: _cipherStorageKey); + if (!containsEncryptionKey) { + final key = SecureRandom(_cipherStorageKeyLength).base64; + await secureStorage.write( + key: _cipherStorageKey, + value: key, + ); + } + + // workaround for if we just wrote to the key and it still doesn't exist + final rawEncryptionKey = await secureStorage.read(key: _cipherStorageKey); + if (rawEncryptionKey == null) throw MissingPluginException(); + + codec = getEncryptSembastCodec(password: rawEncryptionKey); + } on MissingPluginException catch (_) { + Logs().i('Sembast encryption is not supported on this platform'); + } + + final db = FlutterMatrixSembastDatabase( + client.clientName, + codec: codec, + path: await _findDatabasePath(client), + dbFactory: kIsWeb ? databaseFactoryWeb : databaseFactoryIo, + ); + try { + await db.open(); + Logs().d('Sembast is ready'); + } catch (e, s) { + Logs().e('Unable to open Sembast. Delete and try again...', e, s); + await db.clear(); + await db.open(); + } + return db; + } + + static Future _findDatabasePath(Client client) async { + String path = client.clientName; + if (!kIsWeb) { + Directory directory; + try { + directory = await getApplicationSupportDirectory(); + } catch (_) { + try { + directory = await getLibraryDirectory(); + } catch (_) { + directory = Directory.current; + } + } + path = '${directory.path}${client.clientName}.db'; + } + Logs().i('Use database path: "$path"'); + return path; + } + + @override + int get maxFileSize => supportsFileStoring ? 100 * 1024 * 1024 : 0; + @override + bool get supportsFileStoring => (PlatformInfos.isIOS || + PlatformInfos.isAndroid || + PlatformInfos.isDesktop); + + Future _getFileStoreDirectory() async { + try { + try { + return (await getApplicationSupportDirectory()).path; + } catch (_) { + return (await getApplicationDocumentsDirectory()).path; + } + } catch (_) { + return (await getDownloadsDirectory()).path; + } + } + + @override + Future getFile(Uri mxcUri) async { + if (!supportsFileStoring) return null; + final tempDirectory = await _getFileStoreDirectory(); + final file = + File('$tempDirectory/${Uri.encodeComponent(mxcUri.toString())}'); + if (await file.exists() == false) return null; + final bytes = await file.readAsBytes(); + return bytes; + } + + @override + Future storeFile(Uri mxcUri, Uint8List bytes, int time) async { + if (!supportsFileStoring) return null; + final tempDirectory = await _getFileStoreDirectory(); + final file = + File('$tempDirectory/${Uri.encodeComponent(mxcUri.toString())}'); + if (await file.exists()) return; + await file.writeAsBytes(bytes); + return; + } +} + +class _EncryptEncoder extends Converter, String> { + final String key; + final String signature; + _EncryptEncoder(this.key, this.signature); + + @override + String convert(Map input) { + String encoded; + switch (signature) { + case "Salsa20": + encoded = Encrypter(Salsa20(Key.fromUtf8(key))) + .encrypt(json.encode(input), iv: IV.fromLength(8)) + .base64; + break; + case "AES": + encoded = Encrypter(AES(Key.fromUtf8(key))) + .encrypt(json.encode(input), iv: IV.fromLength(16)) + .base64; + break; + default: + throw FormatException('invalid $signature'); + break; + } + return encoded; + } +} + +class _EncryptDecoder extends Converter> { + final String key; + final String signature; + _EncryptDecoder(this.key, this.signature); + + @override + Map convert(String input) { + dynamic decoded; + switch (signature) { + case "Salsa20": + decoded = json.decode(Encrypter(Salsa20(Key.fromUtf8(key))) + .decrypt64(input, iv: IV.fromLength(8))); + break; + case "AES": + decoded = json.decode(Encrypter(AES(Key.fromUtf8(key))) + .decrypt64(input, iv: IV.fromLength(16))); + break; + default: + break; + } + if (decoded is Map) { + return decoded.cast(); + } + throw FormatException('invalid input $input'); + } +} + +class _EncryptCodec extends Codec, String> { + final String signature; + _EncryptEncoder _encoder; + _EncryptDecoder _decoder; + _EncryptCodec(String password, this.signature) { + _encoder = _EncryptEncoder(password, signature); + _decoder = _EncryptDecoder(password, signature); + } + + @override + Converter> get decoder => _decoder; + + @override + Converter, String> get encoder => _encoder; +} + +// Salsa20 (16 length key required) or AES (32 length key required) +SembastCodec getEncryptSembastCodec( + {@required String password, String signature = "Salsa20"}) => + SembastCodec( + signature: signature, codec: _EncryptCodec(password, signature)); diff --git a/pubspec.lock b/pubspec.lock index fdd0eedd..f01d9678 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -64,6 +64,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.3.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" async: dependency: transitive description: @@ -260,6 +267,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.7" + encrypt: + dependency: "direct main" + description: + name: encrypt + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.1" fake_async: dependency: transitive description: @@ -630,6 +644,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.0.0" + idb_shim: + dependency: transitive + description: + name: idb_shim + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" image: dependency: transitive description: @@ -752,9 +773,11 @@ packages: matrix: dependency: "direct main" description: - name: matrix - url: "https://pub.dartlang.org" - source: hosted + path: "." + ref: "krille/sembast" + resolved-ref: d3398eb9d5ea77e00c59a4a75092779e2640c358 + url: "git@gitlab.com:famedly/company/frontend/famedlysdk.git" + source: git version: "0.7.0-nullsafety.5" matrix_api_lite: dependency: transitive @@ -1003,6 +1026,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + pointycastle: + dependency: transitive + description: + name: pointycastle + url: "https://pub.dartlang.org" + source: hosted + version: "3.4.0" pool: dependency: transitive description: @@ -1129,6 +1159,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + sembast: + dependency: transitive + description: + name: sembast + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" + sembast_web: + dependency: "direct main" + description: + name: sembast_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1+1" sentry: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d4b7afe3..ba9aa696 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: emoji_picker_flutter: ^1.0.7 #fcm_shared_isolate: # git: https://gitlab.com/famedly/libraries/fcm_shared_isolate.git + encrypt: ^5.0.1 file_picker_cross: ^4.5.0 flutter: sdk: flutter @@ -61,6 +62,7 @@ dependencies: receive_sharing_intent: ^1.4.5 record: ^3.0.0 scroll_to_index: ^2.1.0 + sembast_web: ^2.0.1+1 sentry: ^6.0.1 share: ^2.0.4 slugify: ^2.0.0 @@ -112,4 +114,8 @@ dependency_overrides: hosted: name: geolocator_android url: https://hanntech-gmbh.gitlab.io/free2pass/flutter-geolocator-floss - provider: 5.0.0 \ No newline at end of file + matrix: + git: + url: git@gitlab.com:famedly/company/frontend/famedlysdk.git + ref: krille/sembast + provider: 5.0.0