import 'dart:async'; import 'dart:typed_data'; import 'package:collection/collection.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:flutter/material.dart'; import 'package:twonly/core/bridge/wrapper/signal.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb; import 'package:twonly/src/providers/routing.provider.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/verification_success_dialog.comp.dart'; class KeyVerificationService { static final StreamController _verificationSuccessCloseController = StreamController.broadcast(); static Stream get onVerificationSuccessClose => _verificationSuccessCloseController.stream; static void notifyVerificationSuccessClose() { _verificationSuccessCloseController.add(null); } static void closeVerificationFlows(BuildContext context) { Navigator.popUntil(context, (route) { final name = route.settings.name; if (name == null) return false; return !name.contains('qr_scanner') && !name.contains('verifybadge'); }); } static Future> getNewSecretVerificationToken() async { final token = getRandomUint8List(16); await twonlyDB.keyVerificationDao.insertVerificationToken(token); return token.toList(); } static Future handleScannedVerificationToken( int contactId, Uint8List contactPubKey, List secretToken, ) async { Log.info('Notifying verified user using the secret token'); final calculatedMac = await _createVerificationBytes( contactId, contactPubKey, secretToken, false, ); await RustApi.sendEncryptedContent( contactId: contactId, content: pb.EncryptedContent( keyVerificationProof: pb.EncryptedContent_KeyVerificationProof( calculatedMac: calculatedMac, ), ).writeToBuffer(), ); } static Future handleVerificationSucceeded(int fromUserId) async { final contact = await twonlyDB.contactsDao.getContactById(fromUserId); final context = rootNavigatorKey.currentContext; if (context != null && context.mounted && contact != null) { unawaited( VerificationSuccessDialog.show( context, contact, message: context.lang.secretQrTokenVerifiedSnackbar( getContactDisplayName(contact), ), ), ); } } static Future verifySharedContact({ required int contactId, required List sharedPublicIdentityKey, required int senderId, }) async { if (sharedPublicIdentityKey.isEmpty) { Log.info('Shared contact $contactId carries no public key'); return; } final publicIdentityKey = await RustSignal.getContactPublicKey( contactId: contactId, ); if (publicIdentityKey == null) { Log.info('No public key stored for contact $contactId'); return; } if (publicIdentityKey.equals(sharedPublicIdentityKey)) { Log.info('Verified a user which was shared by a contact'); await twonlyDB.keyVerificationDao.addKeyVerification( contactId, VerificationType.contactSharedByVerified, verifiedBy: senderId, ); } else { Log.error('Public identity keys do not match for contact $contactId'); } } } Future> _createVerificationBytes( int contactId, Uint8List contactPubKey, List secretToken, bool verifying, ) async { final bytes = []; final userPublicKey = await RustSignal.getUserPublicKey(); final ownBytes = [ ..._userIdToLeBytes(userService.currentUser.userId), ...userPublicKey, ]; final contactBytes = [..._userIdToLeBytes(contactId), ...contactPubKey]; if (verifying) { bytes ..addAll(ownBytes) ..addAll(contactBytes); } else { bytes ..addAll(contactBytes) ..addAll(ownBytes); } final hmac = Hmac.sha256(); final mac = await hmac.calculateMac( Uint8List.fromList(bytes), secretKey: SecretKey(secretToken), ); return mac.bytes; } List _userIdToLeBytes(int userId) { final byteData = ByteData(8)..setInt64(0, userId, Endian.little); return byteData.buffer.asUint8List(); }