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(); } void listen() { _ws.pk.stream.listen((event) { switch (event.method) { case 'kex.ack': ackKeyExchange(event); break; case 'kex.ask': replyAskKeyExchange(event); break; } }); } Future decryptText(String text, String kpId, {int? kpOwner}) async { String? publicKey; final kp = await (_dt.db.snLocalKeyPair.select() ..where((e) => e.id.equals(kpId))) .getSingleOrNull(); if (kp == null) { if (kpOwner != null) { final out = await askKeyExchange(kpOwner, kpId); publicKey = out.publicKey; } } else { publicKey = kp.publicKey; } if (publicKey == null) { throw Exception('Key pair not found'); } return await RSA.decryptPKCS1v15(text, publicKey); } Future encryptText(String text) async { if (activeKp == null) throw Exception('No active key pair'); return await RSA.encryptPKCS1v15(text, activeKp!.privateKey!); } 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: 'kex.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: DoNothing(), ); } 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 ?? 0)) ..where((e) => e.privateKey.isNotNull()) ..where((e) => e.isActive.equals(true)) ..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> listKeyPair() async { final kps = await (_dt.db.snLocalKeyPair.select()).get(); return kps .map((e) => SnKeyPair( id: e.id, accountId: e.accountId, publicKey: e.publicKey, privateKey: e.privateKey, isActive: e.isActive, )) .toList(); } Future activeKeyPair(String kpId) async { final kp = await (_dt.db.snLocalKeyPair.select() ..where((e) => e.id.equals(kpId)) ..where((e) => e.privateKey.isNotNull()) ..limit(1)) .getSingleOrNull(); if (kp == null) return; await _dt.db.transaction(() async { await (_dt.db.update(_dt.db.snLocalKeyPair) ..where((e) => e.isActive.equals(true))) .write(SnLocalKeyPairCompanion(isActive: Value(false))); await (_dt.db.update(_dt.db.snLocalKeyPair) ..where((e) => e.id.equals(kp.id))) .write(SnLocalKeyPairCompanion(isActive: Value(true))); }); } Future enrollNew() async { if (!_ua.isAuthorized) throw Exception('Unauthorized'); final id = const Uuid().v4(); final kp = await RSA.generate(2048); final kpMeta = SnKeyPair( id: id, accountId: _ua.user!.id, // This is work as expected // We need to share private key to let everyone can decode the message publicKey: kp.privateKey, privateKey: kp.publicKey, ); // Save the keypair to the local database // If there is already one with private key, it will be overwritten await _dt.db.transaction(() async { await (_dt.db.update(_dt.db.snLocalKeyPair) ..where((e) => e.isActive.equals(true))) .write(SnLocalKeyPairCompanion(isActive: Value(false))); await _dt.db.snLocalKeyPair.insertOne( SnLocalKeyPairCompanion.insert( id: kpMeta.id, accountId: kpMeta.accountId, publicKey: kpMeta.publicKey, privateKey: Value(kpMeta.privateKey), isActive: Value(true), ), ); }); await reloadActive(autoEnroll: false); return kpMeta; } }