twonly-app/lib/src/services/key_verification.service.dart
2026-08-31 15:24:18 +02:00

152 lines
4.4 KiB
Dart

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<void> _verificationSuccessCloseController =
StreamController<void>.broadcast();
static Stream<void> 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<List<int>> getNewSecretVerificationToken() async {
final token = getRandomUint8List(16);
await twonlyDB.keyVerificationDao.insertVerificationToken(token);
return token.toList();
}
static Future<void> handleScannedVerificationToken(
int contactId,
Uint8List contactPubKey,
List<int> 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<void> 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<void> verifySharedContact({
required int contactId,
required List<int> 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<List<int>> _createVerificationBytes(
int contactId,
Uint8List contactPubKey,
List<int> secretToken,
bool verifying,
) async {
final bytes = <int>[];
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<int> _userIdToLeBytes(int userId) {
final byteData = ByteData(8)..setInt64(0, userId, Endian.little);
return byteData.buffer.asUint8List();
}