diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6f3e21c..03b8227 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -37,6 +37,8 @@ PODS: - DKPhotoGallery/Resource (0.0.19): - SDWebImage - SwiftyGif + - fast_rsa (0.6.0): + - Flutter - file_picker (0.0.1): - DKImagePickerController/PhotoGallery - Flutter @@ -262,6 +264,7 @@ DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - croppy (from `.symlinks/plugins/croppy/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - fast_rsa (from `.symlinks/plugins/fast_rsa/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - file_saver (from `.symlinks/plugins/file_saver/ios`) - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) @@ -331,6 +334,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/croppy/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" + fast_rsa: + :path: ".symlinks/plugins/fast_rsa/ios" file_picker: :path: ".symlinks/plugins/file_picker/ios" file_saver: @@ -411,6 +416,7 @@ SPEC CHECKSUMS: device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 + fast_rsa: dc48fb05f26bb108863de122b2a9f5554e8e2591 file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49 file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf diff --git a/lib/database/database.g.dart b/lib/database/database.g.dart index dee6ef8..40ef60a 100644 --- a/lib/database/database.g.dart +++ b/lib/database/database.g.dart @@ -538,6 +538,282 @@ class SnLocalChatMessageCompanion } } +class $SnLocalKeyPairTable extends SnLocalKeyPair + with TableInfo<$SnLocalKeyPairTable, SnLocalKeyPairData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SnLocalKeyPairTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _accountIdMeta = + const VerificationMeta('accountId'); + @override + late final GeneratedColumn accountId = GeneratedColumn( + 'account_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _publicKeyMeta = + const VerificationMeta('publicKey'); + @override + late final GeneratedColumn publicKey = GeneratedColumn( + 'public_key', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _privateKeyMeta = + const VerificationMeta('privateKey'); + @override + late final GeneratedColumn privateKey = GeneratedColumn( + 'private_key', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [id, accountId, publicKey, privateKey]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'sn_local_key_pair'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('account_id')) { + context.handle(_accountIdMeta, + accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta)); + } else if (isInserting) { + context.missing(_accountIdMeta); + } + if (data.containsKey('public_key')) { + context.handle(_publicKeyMeta, + publicKey.isAcceptableOrUnknown(data['public_key']!, _publicKeyMeta)); + } else if (isInserting) { + context.missing(_publicKeyMeta); + } + if (data.containsKey('private_key')) { + context.handle( + _privateKeyMeta, + privateKey.isAcceptableOrUnknown( + data['private_key']!, _privateKeyMeta)); + } + return context; + } + + @override + Set get $primaryKey => const {}; + @override + SnLocalKeyPairData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SnLocalKeyPairData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + accountId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}account_id'])!, + publicKey: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}public_key'])!, + privateKey: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}private_key']), + ); + } + + @override + $SnLocalKeyPairTable createAlias(String alias) { + return $SnLocalKeyPairTable(attachedDatabase, alias); + } +} + +class SnLocalKeyPairData extends DataClass + implements Insertable { + final String id; + final int accountId; + final String publicKey; + final String? privateKey; + const SnLocalKeyPairData( + {required this.id, + required this.accountId, + required this.publicKey, + this.privateKey}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['account_id'] = Variable(accountId); + map['public_key'] = Variable(publicKey); + if (!nullToAbsent || privateKey != null) { + map['private_key'] = Variable(privateKey); + } + return map; + } + + SnLocalKeyPairCompanion toCompanion(bool nullToAbsent) { + return SnLocalKeyPairCompanion( + id: Value(id), + accountId: Value(accountId), + publicKey: Value(publicKey), + privateKey: privateKey == null && nullToAbsent + ? const Value.absent() + : Value(privateKey), + ); + } + + factory SnLocalKeyPairData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SnLocalKeyPairData( + id: serializer.fromJson(json['id']), + accountId: serializer.fromJson(json['accountId']), + publicKey: serializer.fromJson(json['publicKey']), + privateKey: serializer.fromJson(json['privateKey']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'accountId': serializer.toJson(accountId), + 'publicKey': serializer.toJson(publicKey), + 'privateKey': serializer.toJson(privateKey), + }; + } + + SnLocalKeyPairData copyWith( + {String? id, + int? accountId, + String? publicKey, + Value privateKey = const Value.absent()}) => + SnLocalKeyPairData( + id: id ?? this.id, + accountId: accountId ?? this.accountId, + publicKey: publicKey ?? this.publicKey, + privateKey: privateKey.present ? privateKey.value : this.privateKey, + ); + SnLocalKeyPairData copyWithCompanion(SnLocalKeyPairCompanion data) { + return SnLocalKeyPairData( + id: data.id.present ? data.id.value : this.id, + accountId: data.accountId.present ? data.accountId.value : this.accountId, + publicKey: data.publicKey.present ? data.publicKey.value : this.publicKey, + privateKey: + data.privateKey.present ? data.privateKey.value : this.privateKey, + ); + } + + @override + String toString() { + return (StringBuffer('SnLocalKeyPairData(') + ..write('id: $id, ') + ..write('accountId: $accountId, ') + ..write('publicKey: $publicKey, ') + ..write('privateKey: $privateKey') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, accountId, publicKey, privateKey); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SnLocalKeyPairData && + other.id == this.id && + other.accountId == this.accountId && + other.publicKey == this.publicKey && + other.privateKey == this.privateKey); +} + +class SnLocalKeyPairCompanion extends UpdateCompanion { + final Value id; + final Value accountId; + final Value publicKey; + final Value privateKey; + final Value rowid; + const SnLocalKeyPairCompanion({ + this.id = const Value.absent(), + this.accountId = const Value.absent(), + this.publicKey = const Value.absent(), + this.privateKey = const Value.absent(), + this.rowid = const Value.absent(), + }); + SnLocalKeyPairCompanion.insert({ + required String id, + required int accountId, + required String publicKey, + this.privateKey = const Value.absent(), + this.rowid = const Value.absent(), + }) : id = Value(id), + accountId = Value(accountId), + publicKey = Value(publicKey); + static Insertable custom({ + Expression? id, + Expression? accountId, + Expression? publicKey, + Expression? privateKey, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (accountId != null) 'account_id': accountId, + if (publicKey != null) 'public_key': publicKey, + if (privateKey != null) 'private_key': privateKey, + if (rowid != null) 'rowid': rowid, + }); + } + + SnLocalKeyPairCompanion copyWith( + {Value? id, + Value? accountId, + Value? publicKey, + Value? privateKey, + Value? rowid}) { + return SnLocalKeyPairCompanion( + id: id ?? this.id, + accountId: accountId ?? this.accountId, + publicKey: publicKey ?? this.publicKey, + privateKey: privateKey ?? this.privateKey, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (accountId.present) { + map['account_id'] = Variable(accountId.value); + } + if (publicKey.present) { + map['public_key'] = Variable(publicKey.value); + } + if (privateKey.present) { + map['private_key'] = Variable(privateKey.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SnLocalKeyPairCompanion(') + ..write('id: $id, ') + ..write('accountId: $accountId, ') + ..write('publicKey: $publicKey, ') + ..write('privateKey: $privateKey, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); $AppDatabaseManager get managers => $AppDatabaseManager(this); @@ -545,12 +821,13 @@ abstract class _$AppDatabase extends GeneratedDatabase { $SnLocalChatChannelTable(this); late final $SnLocalChatMessageTable snLocalChatMessage = $SnLocalChatMessageTable(this); + late final $SnLocalKeyPairTable snLocalKeyPair = $SnLocalKeyPairTable(this); @override Iterable> get allTables => allSchemaEntities.whereType>(); @override List get allSchemaEntities => - [snLocalChatChannel, snLocalChatMessage]; + [snLocalChatChannel, snLocalChatMessage, snLocalKeyPair]; } typedef $$SnLocalChatChannelTableCreateCompanionBuilder @@ -869,6 +1146,165 @@ typedef $$SnLocalChatMessageTableProcessedTableManager = ProcessedTableManager< ), SnLocalChatMessageData, PrefetchHooks Function()>; +typedef $$SnLocalKeyPairTableCreateCompanionBuilder = SnLocalKeyPairCompanion + Function({ + required String id, + required int accountId, + required String publicKey, + Value privateKey, + Value rowid, +}); +typedef $$SnLocalKeyPairTableUpdateCompanionBuilder = SnLocalKeyPairCompanion + Function({ + Value id, + Value accountId, + Value publicKey, + Value privateKey, + Value rowid, +}); + +class $$SnLocalKeyPairTableFilterComposer + extends Composer<_$AppDatabase, $SnLocalKeyPairTable> { + $$SnLocalKeyPairTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get accountId => $composableBuilder( + column: $table.accountId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get publicKey => $composableBuilder( + column: $table.publicKey, builder: (column) => ColumnFilters(column)); + + ColumnFilters get privateKey => $composableBuilder( + column: $table.privateKey, builder: (column) => ColumnFilters(column)); +} + +class $$SnLocalKeyPairTableOrderingComposer + extends Composer<_$AppDatabase, $SnLocalKeyPairTable> { + $$SnLocalKeyPairTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get accountId => $composableBuilder( + column: $table.accountId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get publicKey => $composableBuilder( + column: $table.publicKey, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get privateKey => $composableBuilder( + column: $table.privateKey, builder: (column) => ColumnOrderings(column)); +} + +class $$SnLocalKeyPairTableAnnotationComposer + extends Composer<_$AppDatabase, $SnLocalKeyPairTable> { + $$SnLocalKeyPairTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get accountId => + $composableBuilder(column: $table.accountId, builder: (column) => column); + + GeneratedColumn get publicKey => + $composableBuilder(column: $table.publicKey, builder: (column) => column); + + GeneratedColumn get privateKey => $composableBuilder( + column: $table.privateKey, builder: (column) => column); +} + +class $$SnLocalKeyPairTableTableManager extends RootTableManager< + _$AppDatabase, + $SnLocalKeyPairTable, + SnLocalKeyPairData, + $$SnLocalKeyPairTableFilterComposer, + $$SnLocalKeyPairTableOrderingComposer, + $$SnLocalKeyPairTableAnnotationComposer, + $$SnLocalKeyPairTableCreateCompanionBuilder, + $$SnLocalKeyPairTableUpdateCompanionBuilder, + ( + SnLocalKeyPairData, + BaseReferences<_$AppDatabase, $SnLocalKeyPairTable, SnLocalKeyPairData> + ), + SnLocalKeyPairData, + PrefetchHooks Function()> { + $$SnLocalKeyPairTableTableManager( + _$AppDatabase db, $SnLocalKeyPairTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SnLocalKeyPairTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$SnLocalKeyPairTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$SnLocalKeyPairTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value id = const Value.absent(), + Value accountId = const Value.absent(), + Value publicKey = const Value.absent(), + Value privateKey = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SnLocalKeyPairCompanion( + id: id, + accountId: accountId, + publicKey: publicKey, + privateKey: privateKey, + rowid: rowid, + ), + createCompanionCallback: ({ + required String id, + required int accountId, + required String publicKey, + Value privateKey = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SnLocalKeyPairCompanion.insert( + id: id, + accountId: accountId, + publicKey: publicKey, + privateKey: privateKey, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$SnLocalKeyPairTableProcessedTableManager = ProcessedTableManager< + _$AppDatabase, + $SnLocalKeyPairTable, + SnLocalKeyPairData, + $$SnLocalKeyPairTableFilterComposer, + $$SnLocalKeyPairTableOrderingComposer, + $$SnLocalKeyPairTableAnnotationComposer, + $$SnLocalKeyPairTableCreateCompanionBuilder, + $$SnLocalKeyPairTableUpdateCompanionBuilder, + ( + SnLocalKeyPairData, + BaseReferences<_$AppDatabase, $SnLocalKeyPairTable, SnLocalKeyPairData> + ), + SnLocalKeyPairData, + PrefetchHooks Function()>; class $AppDatabaseManager { final _$AppDatabase _db; @@ -877,4 +1313,6 @@ class $AppDatabaseManager { $$SnLocalChatChannelTableTableManager(_db, _db.snLocalChatChannel); $$SnLocalChatMessageTableTableManager get snLocalChatMessage => $$SnLocalChatMessageTableTableManager(_db, _db.snLocalChatMessage); + $$SnLocalKeyPairTableTableManager get snLocalKeyPair => + $$SnLocalKeyPairTableTableManager(_db, _db.snLocalKeyPair); } diff --git a/lib/database/keypair.dart b/lib/database/keypair.dart index 54e75c3..f0af5e0 100644 --- a/lib/database/keypair.dart +++ b/lib/database/keypair.dart @@ -1,32 +1,4 @@ -import 'dart:convert'; - import 'package:drift/drift.dart'; -import 'package:surface/types/keypair.dart'; - -class SnKeyPairConverter extends TypeConverter - with JsonTypeConverter2> { - const SnKeyPairConverter(); - - @override - SnKeyPair fromSql(String fromDb) { - return fromJson(jsonDecode(fromDb) as Map); - } - - @override - String toSql(SnKeyPair value) { - return jsonEncode(toJson(value)); - } - - @override - SnKeyPair fromJson(Map json) { - return SnKeyPair.fromJson(json); - } - - @override - Map toJson(SnKeyPair value) { - return value.toJson(); - } -} class SnLocalKeyPair extends Table { TextColumn get id => text()(); diff --git a/lib/main.dart b/lib/main.dart index 1ad95aa..edf4f48 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -25,6 +25,7 @@ import 'package:surface/providers/channel.dart'; import 'package:surface/providers/chat_call.dart'; import 'package:surface/providers/config.dart'; import 'package:surface/providers/database.dart'; +import 'package:surface/providers/keypair.dart'; import 'package:surface/providers/link_preview.dart'; import 'package:surface/providers/navigation.dart'; import 'package:surface/providers/notification.dart'; @@ -108,7 +109,8 @@ void main() async { } if (!kIsWeb && Platform.isAndroid) { - final ImagePickerPlatform imagePickerImplementation = ImagePickerPlatform.instance; + final ImagePickerPlatform imagePickerImplementation = + ImagePickerPlatform.instance; if (imagePickerImplementation is ImagePickerAndroid) { imagePickerImplementation.useAndroidPhotoPicker = true; } @@ -160,6 +162,7 @@ class SolianApp extends StatelessWidget { Provider(create: (ctx) => SnStickerProvider(ctx)), ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)), ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)), + Provider(create: (ctx) => KeyPairProvider(ctx)), ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)), ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)), ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)), @@ -227,7 +230,8 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { if (prefs.containsKey('first_boot_time')) { final rawTime = prefs.getString('first_boot_time'); final time = DateTime.tryParse(rawTime ?? ''); - if (time != null && time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) { + if (time != null && + time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) { final inAppReview = InAppReview.instance; if (prefs.getBool('rating_requested') == true) return; if (await inAppReview.isAvailable()) { @@ -258,12 +262,18 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { final remoteVersionString = resp.data?['tag_name'] ?? '0.0.0+0'; final remoteVersion = Version.parse(remoteVersionString.split('+').first); final localVersion = Version.parse(localVersionString.split('+').first); - final remoteBuildNumber = int.tryParse(remoteVersionString.split('+').last) ?? 0; - final localBuildNumber = int.tryParse(localVersionString.split('+').last) ?? 0; - logging.info("[Update] Local: $localVersionString, Remote: $remoteVersionString"); - if ((remoteVersion > localVersion || remoteBuildNumber > localBuildNumber) && mounted) { + final remoteBuildNumber = + int.tryParse(remoteVersionString.split('+').last) ?? 0; + final localBuildNumber = + int.tryParse(localVersionString.split('+').last) ?? 0; + logging.info( + "[Update] Local: $localVersionString, Remote: $remoteVersionString"); + if ((remoteVersion > localVersion || + remoteBuildNumber > localBuildNumber) && + mounted) { final config = context.read(); - config.setUpdate(remoteVersionString, resp.data?['body'] ?? 'No changelog'); + config.setUpdate( + remoteVersionString, resp.data?['body'] ?? 'No changelog'); logging.info("[Update] Update available: $remoteVersionString"); } } catch (e) { @@ -298,6 +308,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { notify.listen(); await notify.registerPushNotifications(); if (!mounted) return; + final kp = context.read(); + kp.listen(); + if (!mounted) return; final sticker = context.read(); await sticker.listSticker(); logging.info('[Bootstrap] Everything initialized!'); @@ -355,7 +368,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { Future _trayInitialization() async { if (kIsWeb || Platform.isAndroid || Platform.isIOS) return; - final icon = Platform.isWindows ? 'assets/icon/tray-icon.ico' : 'assets/icon/tray-icon.png'; + final icon = Platform.isWindows + ? 'assets/icon/tray-icon.ico' + : 'assets/icon/tray-icon.png'; final appVersion = await PackageInfo.fromPlatform(); trayManager.addListener(this); diff --git a/lib/providers/keypair.dart b/lib/providers/keypair.dart new file mode 100644 index 0000000..f209c70 --- /dev/null +++ b/lib/providers/keypair.dart @@ -0,0 +1,211 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:drift/drift.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:surface/database/database.dart'; +import 'package:surface/logger.dart'; +import 'package:surface/providers/database.dart'; +import 'package:surface/providers/userinfo.dart'; +import 'package:surface/providers/websocket.dart'; +import 'package:surface/types/keypair.dart'; +import 'package:fast_rsa/fast_rsa.dart'; +import 'package:surface/types/websocket.dart'; +import 'package:uuid/uuid.dart'; + +// Currently the keypair only provide RSA encryption +// Supported by the `fast_rsa` package +class KeyPairProvider { + late final DatabaseProvider _dt; + late final UserProvider _ua; + late final WebSocketProvider _ws; + + SnKeyPair? activeKp; + + KeyPairProvider(BuildContext context) { + _dt = context.read(); + _ua = context.read(); + _ws = context.read(); + + reloadActive(); + } + + void listen() { + _ws.pk.stream.listen((event) { + switch (event.method) { + case 'kex.ack': + ackKeyExchange(event); + break; + case 'key.ask': + replyAskKeyExchange(event); + break; + } + }); + } + + Future decryptText(String text, String kpId) async { + final kp = await (_dt.db.snLocalKeyPair.select() + ..where((e) => e.id.equals(kpId))) + .getSingleOrNull(); + if (kp == null) throw Exception('Key pair not found'); + return await RSA.decryptPKCS1v15(text, kp.privateKey!); + } + + Future encryptText(String text) async { + if (activeKp == null) throw Exception('No active key pair'); + return await RSA.encryptPKCS1v15(text, activeKp!.publicKey); + } + + final Map> _requests = {}; + + Future askKeyExchange(int kpOwner, String kpId) async { + if (_requests.containsKey(kpId)) return await _requests[kpId]!.future; + + final completer = Completer(); + _requests[kpId] = completer; + + _ws.conn?.sink.add( + jsonEncode(WebSocketPackage( + method: 'key.ask', + endpoint: 'id', + payload: { + 'keypair_id': kpId, + 'user_id': kpOwner, + }, + )), + ); + + return Future.any([ + _requests[kpId]!.future, + Future.delayed(const Duration(seconds: 60), () { + _requests.remove(kpId); + throw TimeoutException("Key exchange timed out"); + }), + ]); + } + + Future ackKeyExchange(WebSocketPackage pkt) async { + if (pkt.payload == null) return; + final kpMeta = SnKeyPair( + id: pkt.payload!['keypair_id'] as String, + accountId: pkt.payload!['user_id'] as int, + publicKey: pkt.payload!['public_key'] as String, + privateKey: pkt.payload?['private_key'] as String?, + ); + + if (_requests.containsKey(kpMeta.id)) { + _requests[kpMeta.id]!.complete(kpMeta); + _requests.remove(kpMeta.id); + } + + // Save the keypair to the local database + await _dt.db.snLocalKeyPair.insertOne( + SnLocalKeyPairCompanion.insert( + id: kpMeta.id, + accountId: kpMeta.accountId, + publicKey: kpMeta.publicKey, + privateKey: Value(kpMeta.privateKey), + ), + onConflict: DoUpdate( + (_) => SnLocalKeyPairCompanion.custom( + publicKey: Constant(kpMeta.publicKey), + privateKey: Constant(kpMeta.privateKey), + ), + ), + ); + } + + Future replyAskKeyExchange(WebSocketPackage pkt) async { + final kpId = pkt.payload!['keypair_id'] as String; + final userId = pkt.payload!['user_id'] as int; + final clientId = pkt.payload!['client_id'] as String; + + final localKp = await (_dt.db.snLocalKeyPair.select() + ..where((e) => e.id.equals(kpId)) + ..limit(1)) + .getSingleOrNull(); + if (localKp == null) return; + + logging.info( + '[Kex] Reply to key exchange request of $kpId from user $userId', + ); + + // We do not give the private key to the client + _ws.conn?.sink.add(jsonEncode( + WebSocketPackage( + method: 'kex.ack', + endpoint: 'id', + payload: { + 'keypair_id': localKp.id, + 'user_id': localKp.accountId, + 'public_key': localKp.publicKey, + 'client_id': clientId, + }, + ).toJson(), + )); + } + + Future reloadActive({bool autoEnroll = true}) async { + final kp = await (_dt.db.snLocalKeyPair.select() + ..where((e) => e.accountId.equals(_ua.user!.id)) + ..where((e) => e.privateKey.isNotNull()) + ..limit(1)) + .getSingleOrNull(); + + if (kp != null) { + activeKp = SnKeyPair( + id: kp.id, + accountId: kp.accountId, + publicKey: kp.publicKey, + privateKey: kp.privateKey, + ); + } + + if (kp == null && autoEnroll) { + return await enrollNew(); + } + + return activeKp; + } + + Future enrollNew() async { + if (!_ua.isAuthorized) throw Exception('Unauthorized'); + + final existsOne = await (_dt.db.snLocalKeyPair.select() + ..where((e) => e.accountId.equals(_ua.user!.id)) + ..where((e) => e.privateKey.isNotNull()) + ..limit(1)) + .getSingleOrNull(); + + final id = existsOne?.id ?? const Uuid().v4(); + final kp = await RSA.generate(2048); + final kpMeta = SnKeyPair( + id: id, + accountId: _ua.user!.id, + publicKey: kp.publicKey, + privateKey: kp.privateKey, + ); + + // Save the keypair to the local database + // If there is already one with private key, it will be overwritten + await _dt.db.snLocalKeyPair.insertOne( + SnLocalKeyPairCompanion.insert( + id: kpMeta.id, + accountId: kpMeta.accountId, + publicKey: kpMeta.publicKey, + privateKey: Value(kpMeta.privateKey), + ), + onConflict: DoUpdate( + (_) => SnLocalKeyPairCompanion.custom( + publicKey: Constant(kpMeta.publicKey), + privateKey: Constant(kpMeta.privateKey), + ), + ), + ); + + await reloadActive(autoEnroll: false); + + return kpMeta; + } +} diff --git a/lib/providers/websocket.dart b/lib/providers/websocket.dart index 91003ec..c66a59b 100644 --- a/lib/providers/websocket.dart +++ b/lib/providers/websocket.dart @@ -117,7 +117,8 @@ class WebSocketProvider extends ChangeNotifier { (event) { final packet = WebSocketPackage.fromJson(jsonDecode(event)); logging.debug( - '[Websocket] Incoming message: ${packet.method} ${packet.message}'); + '[Websocket] Incoming message: ${packet.method} ${packet.message}', + ); pk.sink.add(packet); }, onDone: () { diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index ca76b95..4e08997 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -25,6 +26,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin"); bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_linux_registrar); + g_autoptr(FlPluginRegistrar) fast_rsa_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FastRsaPlugin"); + fast_rsa_plugin_register_with_registrar(fast_rsa_registrar); g_autoptr(FlPluginRegistrar) file_saver_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin"); file_saver_plugin_register_with_registrar(file_saver_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 8908477..3646f22 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST bitsdojo_window_linux + fast_rsa file_saver file_selector_linux flutter_timezone diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 66c7b43..75c5ada 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import bitsdojo_window_macos import connectivity_plus import device_info_plus +import fast_rsa import file_picker import file_saver import file_selector_macos @@ -43,6 +44,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + FastRsaPlugin.register(with: registry.registrar(forPlugin: "FastRsaPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 2a53880..29a05ef 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -513,6 +513,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + fast_rsa: + dependency: "direct main" + description: + name: fast_rsa + sha256: "205a36c0412b9fabebf3e18ccb5221d819cc28cfb3da988c0bf7b646368d0270" + url: "https://pub.dev" + source: hosted + version: "3.8.0" ffi: dependency: transitive description: @@ -665,6 +673,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.70.2" + flat_buffers: + dependency: transitive + description: + name: flat_buffers + sha256: "380bdcba5664a718bfd4ea20a45d39e13684f5318fcd8883066a55e21f37f4c3" + url: "https://pub.dev" + source: hosted + version: "23.5.26" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 7addab9..20ca838 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -137,6 +137,7 @@ dependencies: flutter_timezone: ^4.1.0 flutter_map: ^8.1.0 geolocator: ^13.0.2 + fast_rsa: ^3.8.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 4ec2695..e2ebc79 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -35,6 +36,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("BitsdojoWindowPlugin")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); + FastRsaPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FastRsaPlugin")); FileSaverPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSaverPlugin")); FileSelectorWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index fa1c0b0..f1ec2eb 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST bitsdojo_window_windows connectivity_plus + fast_rsa file_saver file_selector_windows firebase_core