feat: Use sembast over sqflite
							parent
							
								
									f2935e9d9b
								
							
						
					
					
						commit
						b2341bf5f6
					
				@ -0,0 +1,120 @@
 | 
				
			|||||||
 | 
					//@dart=2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'dart:convert';
 | 
				
			||||||
 | 
					import 'dart:math';
 | 
				
			||||||
 | 
					import 'dart:typed_data';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:crypto/crypto.dart';
 | 
				
			||||||
 | 
					import 'package:encrypt/encrypt.dart';
 | 
				
			||||||
 | 
					import 'package:sembast/sembast.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var _random = Random.secure();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Random bytes generator
 | 
				
			||||||
 | 
					Uint8List _randBytes(int length) {
 | 
				
			||||||
 | 
					  return Uint8List.fromList(
 | 
				
			||||||
 | 
					      List<int>.generate(length, (i) => _random.nextInt(256)));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Generate an encryption password based on a user input password
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// It uses MD5 which generates a 16 bytes blob, size needed for Salsa20
 | 
				
			||||||
 | 
					Uint8List _generateEncryptPassword(String password) {
 | 
				
			||||||
 | 
					  final blob = Uint8List.fromList(md5.convert(utf8.encode(password)).bytes);
 | 
				
			||||||
 | 
					  assert(blob.length == 16);
 | 
				
			||||||
 | 
					  return blob;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Salsa20 based encoder
 | 
				
			||||||
 | 
					class _EncryptEncoder extends Converter<dynamic, String> {
 | 
				
			||||||
 | 
					  final Salsa20 salsa20;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  _EncryptEncoder(this.salsa20);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String convert(dynamic input) {
 | 
				
			||||||
 | 
					    // Generate random initial value
 | 
				
			||||||
 | 
					    final iv = _randBytes(8);
 | 
				
			||||||
 | 
					    final ivEncoded = base64.encode(iv);
 | 
				
			||||||
 | 
					    assert(ivEncoded.length == 12);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Encode the input value
 | 
				
			||||||
 | 
					    final encoded =
 | 
				
			||||||
 | 
					        Encrypter(salsa20).encrypt(json.encode(input), iv: IV(iv)).base64;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Prepend the initial value
 | 
				
			||||||
 | 
					    return '$ivEncoded$encoded';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Salsa20 based decoder
 | 
				
			||||||
 | 
					class _EncryptDecoder extends Converter<String, dynamic> {
 | 
				
			||||||
 | 
					  final Salsa20 salsa20;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  _EncryptDecoder(this.salsa20);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  dynamic convert(String input) {
 | 
				
			||||||
 | 
					    // Read the initial value that was prepended
 | 
				
			||||||
 | 
					    assert(input.length >= 12);
 | 
				
			||||||
 | 
					    final iv = base64.decode(input.substring(0, 12));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Extract the real input
 | 
				
			||||||
 | 
					    input = input.substring(12);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Decode the input
 | 
				
			||||||
 | 
					    final decoded =
 | 
				
			||||||
 | 
					        json.decode(Encrypter(salsa20).decrypt64(input, iv: IV(iv)));
 | 
				
			||||||
 | 
					    if (decoded is Map) {
 | 
				
			||||||
 | 
					      return decoded.cast<String, dynamic>();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return decoded;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Salsa20 based Codec
 | 
				
			||||||
 | 
					class _EncryptCodec extends Codec<dynamic, String> {
 | 
				
			||||||
 | 
					  late _EncryptEncoder _encoder;
 | 
				
			||||||
 | 
					  late _EncryptDecoder _decoder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  _EncryptCodec(Uint8List passwordBytes) {
 | 
				
			||||||
 | 
					    final salsa20 = Salsa20(Key(passwordBytes));
 | 
				
			||||||
 | 
					    _encoder = _EncryptEncoder(salsa20);
 | 
				
			||||||
 | 
					    _decoder = _EncryptDecoder(salsa20);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Converter<String, dynamic> get decoder => _decoder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Converter<dynamic, String> get encoder => _encoder;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Our plain text signature
 | 
				
			||||||
 | 
					const _encryptCodecSignature = 'encrypt';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create a codec to use to open a database with encrypted stored data.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Hash (md5) of the password is used (but never stored) as a key to encrypt
 | 
				
			||||||
 | 
					/// the data using the Salsa20 algorithm with a random (8 bytes) initial value
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// This is just used as a demonstration and should not be considered as a
 | 
				
			||||||
 | 
					/// reference since its implementation (and storage format) might change.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// No performance metrics has been made to check whether this is a viable
 | 
				
			||||||
 | 
					/// solution for big databases.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// The usage is then
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// ```dart
 | 
				
			||||||
 | 
					/// // Initialize the encryption codec with a user password
 | 
				
			||||||
 | 
					/// var codec = getEncryptSembastCodec(password: '[your_user_password]');
 | 
				
			||||||
 | 
					/// // Open the database with the codec
 | 
				
			||||||
 | 
					/// Database db = await factory.openDatabase(dbPath, codec: codec);
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// // ...your database is ready to use
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					SembastCodec getEncryptSembastCodec({required String password}) => SembastCodec(
 | 
				
			||||||
 | 
					      signature: _encryptCodecSignature,
 | 
				
			||||||
 | 
					      codec: _EncryptCodec(_generateEncryptPassword(password)),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
@ -0,0 +1,131 @@
 | 
				
			|||||||
 | 
					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';
 | 
				
			||||||
 | 
					import 'codec.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FlutterMatrixSembastDatabaseOld extends MatrixSembastDatabase {
 | 
				
			||||||
 | 
					  FlutterMatrixSembastDatabaseOld(
 | 
				
			||||||
 | 
					    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<FlutterMatrixSembastDatabaseOld> 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 = FlutterMatrixSembastDatabaseOld(
 | 
				
			||||||
 | 
					      client.clientName,
 | 
				
			||||||
 | 
					      codec: codec,
 | 
				
			||||||
 | 
					      path: await _findDatabasePath(client),
 | 
				
			||||||
 | 
					      dbFactory: kIsWeb ? databaseFactoryWeb : databaseFactoryIo,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    await db.open();
 | 
				
			||||||
 | 
					    Logs().d('Sembast is ready');
 | 
				
			||||||
 | 
					    return db;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static Future<String> _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';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return path;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get maxFileSize => supportsFileStoring ? 100 * 1024 * 1024 : 0;
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool get supportsFileStoring => (PlatformInfos.isIOS ||
 | 
				
			||||||
 | 
					      PlatformInfos.isAndroid ||
 | 
				
			||||||
 | 
					      PlatformInfos.isDesktop);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<String> _getFileStoreDirectory() async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        return (await getApplicationSupportDirectory()).path;
 | 
				
			||||||
 | 
					      } catch (_) {
 | 
				
			||||||
 | 
					        return (await getApplicationDocumentsDirectory()).path;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (_) {
 | 
				
			||||||
 | 
					      return (await getDownloadsDirectory()).path;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<Uint8List> 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;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
					Loading…
					
					
				
		Reference in New Issue