feat: qr key verification
This commit is contained in:
parent
69292b1fd1
commit
6a0a252912
|
|
@ -11,3 +11,7 @@ Uint8List base64decodeUnpadded(String s) {
|
||||||
final needEquals = (4 - (s.length % 4)) % 4;
|
final needEquals = (4 - (s.length % 4)) % 4;
|
||||||
return base64.decode(s + ('=' * needEquals));
|
return base64.decode(s + ('=' * needEquals));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String encodeBase64Unpadded(List<int> s) {
|
||||||
|
return base64Encode(s).replaceAll(RegExp(r'=+$', multiLine: true), '');
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,17 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:canonical_json/canonical_json.dart';
|
import 'package:canonical_json/canonical_json.dart';
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
import 'package:typed_data/typed_data.dart';
|
||||||
|
|
||||||
import 'package:matrix/encryption/encryption.dart';
|
import 'package:matrix/encryption/encryption.dart';
|
||||||
|
import 'package:matrix/encryption/utils/base64_unpadded.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
import 'package:matrix/src/utils/crypto/crypto.dart' as uc;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
+-------------+ +-----------+
|
+-------------+ +-----------+
|
||||||
|
|
@ -61,21 +65,115 @@ import 'package:matrix/matrix.dart';
|
||||||
| |
|
| |
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/// QR key verification
|
||||||
|
/// You create possible methods from `client.verificationMethods` on device A
|
||||||
|
/// and send a request using `request.start()` which calls `sendRequest()` your client
|
||||||
|
/// now is in `waitingAccept` state, where ideally your client would now show some
|
||||||
|
/// waiting indicator.
|
||||||
|
///
|
||||||
|
/// On device B you now get a `m.key.verification.request`, you check the
|
||||||
|
/// `methods` from the request payload and see if anything is possible.
|
||||||
|
/// If not you cancel the request. (should this be cancelled? couldn't another device handle this?)
|
||||||
|
/// you the set the state to `askAccept`.
|
||||||
|
///
|
||||||
|
/// Your client would now show a button to accept/decline the request.
|
||||||
|
/// The user can then use `acceptVerification()`to accept the verification which
|
||||||
|
/// then sends a `m.key.verification.ready`. This also calls `generateQrCode()`
|
||||||
|
/// in it which populates the `request.qrData` depending on the qr mode.
|
||||||
|
/// B now sets the state `askChoice`
|
||||||
|
///
|
||||||
|
/// On device A you now get the ready event, which setups the `possibleMethods`
|
||||||
|
/// and `qrData` on A's side. Similarly A now sets their state to `askChoice`
|
||||||
|
///
|
||||||
|
/// At his point both sides are on the `askChoice` state.
|
||||||
|
///
|
||||||
|
/// BACKWARDS COMPATIBILITY HACK:
|
||||||
|
/// To work well with sdks prior to QR verification (0.20.5 and older), start will
|
||||||
|
/// be sent with ready itself if only sas is supported. This avoids weird glare
|
||||||
|
/// issues faced with start from both sides if clients are not on the same sdk
|
||||||
|
/// version (0.20.5 vs next)
|
||||||
|
/// https://matrix.to/#/!KBwfdofYJUmnsVoqwn:famedly.de/$wlHXlLQJdfrqKAF5KkuQrXydwOhY_uyqfH4ReasZqnA?via=neko.dev&via=famedly.de&via=lihotzki.de
|
||||||
|
|
||||||
|
/// Here your clients would ideally show a list of the `possibleMethods` and the
|
||||||
|
/// user can choose one. For QR specifically, you can show the QR code on the
|
||||||
|
/// device which supports showing the qr code and the device which supports
|
||||||
|
/// scanning can scan this code.
|
||||||
|
///
|
||||||
|
/// Assuming device A scans device B's code, device A would now send a `m.key.verification.start`,
|
||||||
|
/// you do this using the `continueVerificatio()` method. You can pass
|
||||||
|
/// `m.reciprocate.v1` or `m.sas.v1` here, and also attach the qrData here.
|
||||||
|
/// This then calls `verifyQrData()` internally, which sets the `randomSharedSecretForQRCode`
|
||||||
|
/// to the one from the QR code. Device A is now set to `showQRSuccess` state and shows
|
||||||
|
/// a green sheild. (Maybe add a text below saying tell device B you scanned the
|
||||||
|
/// code successfully.)
|
||||||
|
///
|
||||||
|
/// (some keys magic happens here, check `verifyQrData()`, `verifyKeysQR()` to know more)
|
||||||
|
///
|
||||||
|
/// On device B you get the `m.key.verification.start` event. The secret sent in
|
||||||
|
/// the start request is then verified, device B is then set to the `confirmQRScan`
|
||||||
|
/// state. Your device should show a dialog to confirm from B that A's device shows
|
||||||
|
/// the green shield (is in the done state). Once B confirms this physically, you
|
||||||
|
/// call the `acceptQRScanConfirmation()` function, which then does some keys
|
||||||
|
/// magic and sets B's state to `done`.
|
||||||
|
///
|
||||||
|
/// A gets the `m.key.verification.done` messsage and sends a done back, both
|
||||||
|
/// users can now dismiss the verification dialog safely.
|
||||||
|
|
||||||
enum KeyVerificationState {
|
enum KeyVerificationState {
|
||||||
|
askChoice,
|
||||||
askAccept,
|
askAccept,
|
||||||
askSSSS,
|
askSSSS,
|
||||||
waitingAccept,
|
waitingAccept,
|
||||||
askSas,
|
askSas,
|
||||||
|
showQRSuccess, // scanner after QR scan was successfull
|
||||||
|
confirmQRScan, // shower after getting start
|
||||||
waitingSas,
|
waitingSas,
|
||||||
done,
|
done,
|
||||||
error
|
error
|
||||||
}
|
}
|
||||||
|
|
||||||
enum KeyVerificationMethod { emoji, numbers }
|
enum KeyVerificationMethod { emoji, numbers, qrShow, qrScan, reciprocate }
|
||||||
|
|
||||||
|
bool isQrSupported(List knownVerificationMethods, List possibleMethods) {
|
||||||
|
return knownVerificationMethods.contains(EventTypes.QRShow) &&
|
||||||
|
possibleMethods.contains(EventTypes.QRScan) ||
|
||||||
|
knownVerificationMethods.contains(EventTypes.QRScan) &&
|
||||||
|
possibleMethods.contains(EventTypes.QRShow);
|
||||||
|
}
|
||||||
|
|
||||||
List<String> _intersect(List<String>? a, List<dynamic>? b) =>
|
List<String> _intersect(List<String>? a, List<dynamic>? b) =>
|
||||||
(b == null || a == null) ? [] : a.where(b.contains).toList();
|
(b == null || a == null) ? [] : a.where(b.contains).toList();
|
||||||
|
|
||||||
|
List<String> _calculatePossibleMethods(
|
||||||
|
List<String> knownMethods, List<dynamic> payloadMethods) {
|
||||||
|
final output = <String>[];
|
||||||
|
final copyKnownMethods = List<String>.from(knownMethods);
|
||||||
|
final copyPayloadMethods = List.from(payloadMethods);
|
||||||
|
|
||||||
|
copyKnownMethods
|
||||||
|
.removeWhere((element) => !copyPayloadMethods.contains(element));
|
||||||
|
|
||||||
|
// remove qr modes for now, check if they are possible and add later
|
||||||
|
copyKnownMethods.removeWhere((element) => element.startsWith('m.qr_code'));
|
||||||
|
output.addAll(copyKnownMethods);
|
||||||
|
|
||||||
|
if (isQrSupported(knownMethods, payloadMethods)) {
|
||||||
|
// scan/show combo found, add whichever is known to us to our possible methods.
|
||||||
|
if (payloadMethods.contains(EventTypes.QRScan) &&
|
||||||
|
knownMethods.contains(EventTypes.QRShow)) {
|
||||||
|
output.add(EventTypes.QRShow);
|
||||||
|
}
|
||||||
|
if (payloadMethods.contains(EventTypes.QRShow) &&
|
||||||
|
knownMethods.contains(EventTypes.QRScan)) {
|
||||||
|
output.add(EventTypes.QRScan);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output.remove(EventTypes.Reciprocate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
List<int> _bytesToInt(Uint8List bytes, int totalBits) {
|
List<int> _bytesToInt(Uint8List bytes, int totalBits) {
|
||||||
final ret = <int>[];
|
final ret = <int>[];
|
||||||
var current = 0;
|
var current = 0;
|
||||||
|
|
@ -96,9 +194,12 @@ List<int> _bytesToInt(Uint8List bytes, int totalBits) {
|
||||||
|
|
||||||
_KeyVerificationMethod _makeVerificationMethod(
|
_KeyVerificationMethod _makeVerificationMethod(
|
||||||
String type, KeyVerification request) {
|
String type, KeyVerification request) {
|
||||||
if (type == 'm.sas.v1') {
|
if (type == EventTypes.Sas) {
|
||||||
return _KeyVerificationMethodSas(request: request);
|
return _KeyVerificationMethodSas(request: request);
|
||||||
}
|
}
|
||||||
|
if (type == EventTypes.Reciprocate) {
|
||||||
|
return _KeyVerificationMethodQRReciprocate(request: request);
|
||||||
|
}
|
||||||
throw Exception('Unkown method type');
|
throw Exception('Unkown method type');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,7 +214,10 @@ class KeyVerification {
|
||||||
String? _deviceId;
|
String? _deviceId;
|
||||||
bool startedVerification = false;
|
bool startedVerification = false;
|
||||||
_KeyVerificationMethod? _method;
|
_KeyVerificationMethod? _method;
|
||||||
|
|
||||||
List<String> possibleMethods = [];
|
List<String> possibleMethods = [];
|
||||||
|
List<String> oppositePossibleMethods = [];
|
||||||
|
|
||||||
Map<String, dynamic>? startPayload;
|
Map<String, dynamic>? startPayload;
|
||||||
String? _nextAction;
|
String? _nextAction;
|
||||||
List<SignableKey> _verifiedDevices = [];
|
List<SignableKey> _verifiedDevices = [];
|
||||||
|
|
@ -129,6 +233,11 @@ class KeyVerification {
|
||||||
canceled ||
|
canceled ||
|
||||||
{KeyVerificationState.error, KeyVerificationState.done}.contains(state);
|
{KeyVerificationState.error, KeyVerificationState.done}.contains(state);
|
||||||
|
|
||||||
|
String? chosenMethod;
|
||||||
|
// qr stuff
|
||||||
|
QRCode? qrCode;
|
||||||
|
String? randomSharedSecretForQRCode;
|
||||||
|
SignableKey? keyToVerify;
|
||||||
KeyVerification(
|
KeyVerification(
|
||||||
{required this.encryption,
|
{required this.encryption,
|
||||||
this.room,
|
this.room,
|
||||||
|
|
@ -140,6 +249,7 @@ class KeyVerification {
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
Logs().i('[Key Verification] disposing object...');
|
Logs().i('[Key Verification] disposing object...');
|
||||||
|
randomSharedSecretForQRCode = null;
|
||||||
_method?.dispose();
|
_method?.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,15 +261,63 @@ class KeyVerification {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> get knownVerificationMethods {
|
List<String> get knownVerificationMethods {
|
||||||
final methods = <String>[];
|
final methods = <String>{};
|
||||||
if (client.verificationMethods.contains(KeyVerificationMethod.numbers) ||
|
if (client.verificationMethods.contains(KeyVerificationMethod.numbers) ||
|
||||||
client.verificationMethods.contains(KeyVerificationMethod.emoji)) {
|
client.verificationMethods.contains(KeyVerificationMethod.emoji)) {
|
||||||
methods.add('m.sas.v1');
|
methods.add(EventTypes.Sas);
|
||||||
}
|
}
|
||||||
return methods;
|
|
||||||
|
/// `qrCanWork` - qr cannot work if we are verifying another master key but our own is unverified
|
||||||
|
final qrCanWork = (userId != client.userID)
|
||||||
|
? ((client.userDeviceKeys[client.userID]?.masterKey?.verified ?? false)
|
||||||
|
? true
|
||||||
|
: false)
|
||||||
|
: true;
|
||||||
|
|
||||||
|
if (client.verificationMethods.contains(KeyVerificationMethod.qrShow) &&
|
||||||
|
qrCanWork) {
|
||||||
|
methods.add(EventTypes.QRShow);
|
||||||
|
methods.add(EventTypes.Reciprocate);
|
||||||
|
}
|
||||||
|
if (client.verificationMethods.contains(KeyVerificationMethod.qrScan) &&
|
||||||
|
qrCanWork) {
|
||||||
|
methods.add(EventTypes.QRScan);
|
||||||
|
methods.add(EventTypes.Reciprocate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return methods.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sendStart() async {
|
/// Once you get a ready event, i.e both sides are in a `askChoice` state,
|
||||||
|
/// send either `m.reciprocate.v1` or `m.sas.v1` here. If you continue with
|
||||||
|
/// qr, send the qrData you just scanned
|
||||||
|
Future<void> continueVerification(String type,
|
||||||
|
{Uint8List? qrDataRawBytes}) async {
|
||||||
|
chosenMethod = type;
|
||||||
|
bool qrChecksOut = false;
|
||||||
|
if (possibleMethods.contains(type)) {
|
||||||
|
if (qrDataRawBytes != null) {
|
||||||
|
qrChecksOut = await verifyQrData(qrDataRawBytes);
|
||||||
|
// after this scanners state is done
|
||||||
|
}
|
||||||
|
if (type != EventTypes.Reciprocate || qrChecksOut) {
|
||||||
|
final method = _method = _makeVerificationMethod(type, this);
|
||||||
|
await method.sendStart();
|
||||||
|
if (type == EventTypes.Sas) {
|
||||||
|
setState(KeyVerificationState.waitingAccept);
|
||||||
|
}
|
||||||
|
} else if (type == EventTypes.Reciprocate && !qrChecksOut) {
|
||||||
|
Logs().e('[KeyVerification] qr did not check out');
|
||||||
|
await cancel('m.invalid_key');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logs().e(
|
||||||
|
'[KeyVerification] tried to continue verification with a unknown method');
|
||||||
|
await cancel('m.unknown_method');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> sendRequest() async {
|
||||||
await send(
|
await send(
|
||||||
EventTypes.KeyVerificationRequest,
|
EventTypes.KeyVerificationRequest,
|
||||||
{
|
{
|
||||||
|
|
@ -182,12 +340,27 @@ class KeyVerification {
|
||||||
setState(KeyVerificationState.askSSSS);
|
setState(KeyVerificationState.askSSSS);
|
||||||
_nextAction = 'request';
|
_nextAction = 'request';
|
||||||
} else {
|
} else {
|
||||||
await sendStart();
|
await sendRequest();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _handlePayloadLock = false;
|
bool _handlePayloadLock = false;
|
||||||
|
|
||||||
|
QRMode getOurQRMode() {
|
||||||
|
QRMode mode = QRMode.verifyOtherUser;
|
||||||
|
if (client.userID == userId) {
|
||||||
|
if (client.encryption != null &&
|
||||||
|
client.encryption!.enabled &&
|
||||||
|
(client.userDeviceKeys[client.userID]?.masterKey?.directVerified ??
|
||||||
|
false)) {
|
||||||
|
mode = QRMode.verifySelfTrusted;
|
||||||
|
} else {
|
||||||
|
mode = QRMode.verifySelfUntrusted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> handlePayload(String type, Map<String, dynamic> payload,
|
Future<void> handlePayload(String type, Map<String, dynamic> payload,
|
||||||
[String? eventId]) async {
|
[String? eventId]) async {
|
||||||
if (isDone) {
|
if (isDone) {
|
||||||
|
|
@ -216,14 +389,6 @@ class KeyVerification {
|
||||||
now.subtract(Duration(minutes: 20)).isAfter(verifyTime));
|
now.subtract(Duration(minutes: 20)).isAfter(verifyTime));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// verify it has a method we can use
|
|
||||||
possibleMethods =
|
|
||||||
_intersect(knownVerificationMethods, payload['methods']);
|
|
||||||
if (possibleMethods.isEmpty) {
|
|
||||||
// reject it outright
|
|
||||||
await cancel('m.unknown_method');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure we have the other sides keys
|
// ensure we have the other sides keys
|
||||||
if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
|
if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
|
||||||
|
|
@ -234,11 +399,22 @@ class KeyVerification {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oppositePossibleMethods = List<String>.from(payload['methods']);
|
||||||
|
// verify it has a method we can use
|
||||||
|
possibleMethods = _calculatePossibleMethods(
|
||||||
|
knownVerificationMethods, payload['methods']);
|
||||||
|
if (possibleMethods.isEmpty) {
|
||||||
|
// reject it outright
|
||||||
|
await cancel('m.unknown_method');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setState(KeyVerificationState.askAccept);
|
setState(KeyVerificationState.askAccept);
|
||||||
break;
|
break;
|
||||||
case 'm.key.verification.ready':
|
case 'm.key.verification.ready':
|
||||||
if (deviceId == '*') {
|
if (deviceId == '*') {
|
||||||
_deviceId = payload['from_device']; // gotta set the real device id
|
_deviceId = payload['from_device']; // gotta set the real device id
|
||||||
|
transactionId ??= eventId ?? payload['transaction_id'];
|
||||||
// and broadcast the cancel to the other devices
|
// and broadcast the cancel to the other devices
|
||||||
final devices = List<DeviceKeys>.from(
|
final devices = List<DeviceKeys>.from(
|
||||||
client.userDeviceKeys[userId]?.deviceKeys.values ??
|
client.userDeviceKeys[userId]?.deviceKeys.values ??
|
||||||
|
|
@ -254,13 +430,6 @@ class KeyVerification {
|
||||||
devices, EventTypes.KeyVerificationCancel, cancelPayload);
|
devices, EventTypes.KeyVerificationCancel, cancelPayload);
|
||||||
}
|
}
|
||||||
_deviceId ??= payload['from_device'];
|
_deviceId ??= payload['from_device'];
|
||||||
possibleMethods =
|
|
||||||
_intersect(knownVerificationMethods, payload['methods']);
|
|
||||||
if (possibleMethods.isEmpty) {
|
|
||||||
// reject it outright
|
|
||||||
await cancel('m.unknown_method');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure we have the other sides keys
|
// ensure we have the other sides keys
|
||||||
if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
|
if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
|
||||||
|
|
@ -271,14 +440,35 @@ class KeyVerification {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oppositePossibleMethods = List<String>.from(payload['methods']);
|
||||||
|
possibleMethods = _calculatePossibleMethods(
|
||||||
|
knownVerificationMethods, payload['methods']);
|
||||||
|
if (possibleMethods.isEmpty) {
|
||||||
|
// reject it outright
|
||||||
|
await cancel('m.unknown_method');
|
||||||
|
return;
|
||||||
|
}
|
||||||
// as both parties can send a start, the last step being "ready" is race-condition prone
|
// as both parties can send a start, the last step being "ready" is race-condition prone
|
||||||
// as such, we better set it *before* we send our start
|
// as such, we better set it *before* we send our start
|
||||||
lastStep = type;
|
lastStep = type;
|
||||||
// TODO: Pick method?
|
|
||||||
final method =
|
// setup QRData from outgoing request (incoming ready)
|
||||||
_method = _makeVerificationMethod(possibleMethods.first, this);
|
qrCode = await generateQrCode();
|
||||||
await method.sendStart();
|
|
||||||
setState(KeyVerificationState.waitingAccept);
|
// play nice with sdks < 0.20.5
|
||||||
|
// https://matrix.to/#/!KBwfdofYJUmnsVoqwn:famedly.de/$wlHXlLQJdfrqKAF5KkuQrXydwOhY_uyqfH4ReasZqnA?via=neko.dev&via=famedly.de&via=lihotzki.de
|
||||||
|
if (!isQrSupported(knownVerificationMethods, payload['methods'])) {
|
||||||
|
if (knownVerificationMethods.contains(EventTypes.Sas)) {
|
||||||
|
final method = _method =
|
||||||
|
_makeVerificationMethod(possibleMethods.first, this);
|
||||||
|
await method.sendStart();
|
||||||
|
setState(KeyVerificationState.waitingAccept);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// allow user to choose
|
||||||
|
setState(KeyVerificationState.askChoice);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case EventTypes.KeyVerificationStart:
|
case EventTypes.KeyVerificationStart:
|
||||||
_deviceId ??= payload['from_device'];
|
_deviceId ??= payload['from_device'];
|
||||||
|
|
@ -306,8 +496,10 @@ class KeyVerification {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!(await verifyLastStep(
|
if (!(await verifyLastStep([
|
||||||
[EventTypes.KeyVerificationRequest, null]))) {
|
EventTypes.KeyVerificationRequest,
|
||||||
|
'm.key.verification.ready',
|
||||||
|
]))) {
|
||||||
return; // abort
|
return; // abort
|
||||||
}
|
}
|
||||||
if (!knownVerificationMethods.contains(payload['method'])) {
|
if (!knownVerificationMethods.contains(payload['method'])) {
|
||||||
|
|
@ -315,6 +507,13 @@ class KeyVerification {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lastStep == EventTypes.KeyVerificationRequest) {
|
||||||
|
if (!possibleMethods.contains(payload['method'])) {
|
||||||
|
await cancel('m.unknown_method');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ensure we have the other sides keys
|
// ensure we have the other sides keys
|
||||||
if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
|
if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
|
||||||
await client.updateUserDeviceKeys(additionalUsers: {userId});
|
await client.updateUserDeviceKeys(additionalUsers: {userId});
|
||||||
|
|
@ -345,7 +544,10 @@ class KeyVerification {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case EventTypes.KeyVerificationDone:
|
case EventTypes.KeyVerificationDone:
|
||||||
// do nothing
|
if (state == KeyVerificationState.showQRSuccess) {
|
||||||
|
await send(EventTypes.KeyVerificationDone, {});
|
||||||
|
setState(KeyVerificationState.done);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case EventTypes.KeyVerificationCancel:
|
case EventTypes.KeyVerificationCancel:
|
||||||
canceled = true;
|
canceled = true;
|
||||||
|
|
@ -387,11 +589,13 @@ class KeyVerification {
|
||||||
bool skip = false}) async {
|
bool skip = false}) async {
|
||||||
Future<void> next() async {
|
Future<void> next() async {
|
||||||
if (_nextAction == 'request') {
|
if (_nextAction == 'request') {
|
||||||
await sendStart();
|
await sendRequest();
|
||||||
} else if (_nextAction == 'done') {
|
} else if (_nextAction == 'done') {
|
||||||
// and now let's sign them all in the background
|
// and now let's sign them all in the background
|
||||||
unawaited(encryption.crossSigning.sign(_verifiedDevices));
|
unawaited(encryption.crossSigning.sign(_verifiedDevices));
|
||||||
setState(KeyVerificationState.done);
|
setState(KeyVerificationState.done);
|
||||||
|
} else if (_nextAction == 'showQRSuccess') {
|
||||||
|
setState(KeyVerificationState.showQRSuccess);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -418,10 +622,32 @@ class KeyVerification {
|
||||||
}
|
}
|
||||||
setState(KeyVerificationState.waitingAccept);
|
setState(KeyVerificationState.waitingAccept);
|
||||||
if (lastStep == EventTypes.KeyVerificationRequest) {
|
if (lastStep == EventTypes.KeyVerificationRequest) {
|
||||||
|
final copyKnownVerificationMethods =
|
||||||
|
List<String>.from(knownVerificationMethods);
|
||||||
|
// qr code only works when atleast one side has verified master key
|
||||||
|
if (userId == client.userID) {
|
||||||
|
if (!(client.userDeviceKeys[client.userID]?.deviceKeys[deviceId]
|
||||||
|
?.hasValidSignatureChain(verifiedByTheirMasterKey: true) ??
|
||||||
|
false) &&
|
||||||
|
!(client.userDeviceKeys[client.userID]?.masterKey?.verified ??
|
||||||
|
false)) {
|
||||||
|
copyKnownVerificationMethods
|
||||||
|
.removeWhere((element) => element.startsWith('m.qr_code'));
|
||||||
|
copyKnownVerificationMethods.remove(EventTypes.Reciprocate);
|
||||||
|
|
||||||
|
// we are removing stuff only using the old possibleMethods should be ok here.
|
||||||
|
final copyPossibleMethods = List<String>.from(possibleMethods);
|
||||||
|
possibleMethods = _calculatePossibleMethods(
|
||||||
|
copyKnownVerificationMethods, copyPossibleMethods);
|
||||||
|
}
|
||||||
|
}
|
||||||
// we need to send a ready event
|
// we need to send a ready event
|
||||||
await send('m.key.verification.ready', {
|
await send('m.key.verification.ready', {
|
||||||
'methods': possibleMethods,
|
'methods': copyKnownVerificationMethods,
|
||||||
});
|
});
|
||||||
|
// setup QRData from incoming request (outgoing ready)
|
||||||
|
qrCode = await generateQrCode();
|
||||||
|
setState(KeyVerificationState.askChoice);
|
||||||
} else {
|
} else {
|
||||||
// we need to send an accept event
|
// we need to send an accept event
|
||||||
await _method!
|
await _method!
|
||||||
|
|
@ -443,6 +669,16 @@ class KeyVerification {
|
||||||
await cancel('m.user');
|
await cancel('m.user');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// call this to confirm that your other device has shown a shield and is in
|
||||||
|
/// `done` state.
|
||||||
|
Future<void> acceptQRScanConfirmation() async {
|
||||||
|
if (_method is _KeyVerificationMethodQRReciprocate &&
|
||||||
|
state == KeyVerificationState.confirmQRScan) {
|
||||||
|
await (_method as _KeyVerificationMethodQRReciprocate)
|
||||||
|
.acceptQRScanConfirmation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> acceptSas() async {
|
Future<void> acceptSas() async {
|
||||||
if (_method is _KeyVerificationMethodSas) {
|
if (_method is _KeyVerificationMethodSas) {
|
||||||
await (_method as _KeyVerificationMethodSas).acceptSas();
|
await (_method as _KeyVerificationMethodSas).acceptSas();
|
||||||
|
|
@ -501,7 +737,7 @@ class KeyVerification {
|
||||||
() => maybeRequestSSSSSecrets(i + 1));
|
() => maybeRequestSSSSSecrets(i + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> verifyKeys(Map<String, String> keys,
|
Future<void> verifyKeysSAS(Map<String, String> keys,
|
||||||
Future<bool> Function(String, SignableKey) verifier) async {
|
Future<bool> Function(String, SignableKey) verifier) async {
|
||||||
_verifiedDevices = <SignableKey>[];
|
_verifiedDevices = <SignableKey>[];
|
||||||
|
|
||||||
|
|
@ -561,6 +797,56 @@ class KeyVerification {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// shower is true only for reciprocated verifications (shower side)
|
||||||
|
Future<void> verifyKeysQR(SignableKey key, {bool shower = true}) async {
|
||||||
|
var verifiedMasterKey = false;
|
||||||
|
final wasUnknownSession = client.isUnknownSession;
|
||||||
|
|
||||||
|
key.setDirectVerified(true);
|
||||||
|
if (key is CrossSigningKey && key.usage.contains('master')) {
|
||||||
|
verifiedMasterKey = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verifiedMasterKey && userId == client.userID) {
|
||||||
|
// it was our own master key, let's request the cross signing keys
|
||||||
|
// we do it in the background, thus no await needed here
|
||||||
|
// ignore: unawaited_futures
|
||||||
|
maybeRequestSSSSSecrets();
|
||||||
|
}
|
||||||
|
if (shower) {
|
||||||
|
await send(EventTypes.KeyVerificationDone, {});
|
||||||
|
}
|
||||||
|
final keyList = List<SignableKey>.from([key]);
|
||||||
|
var askingSSSS = false;
|
||||||
|
if (encryption.crossSigning.enabled &&
|
||||||
|
encryption.crossSigning.signable(keyList)) {
|
||||||
|
// these keys can be signed! Let's do so
|
||||||
|
if (await encryption.crossSigning.isCached()) {
|
||||||
|
// we want to make sure the verification state is correct for the other party after this event is handled.
|
||||||
|
// Otherwise the verification dialog might be stuck in an unverified but done state for a bit.
|
||||||
|
await encryption.crossSigning.sign(keyList);
|
||||||
|
} else if (!wasUnknownSession) {
|
||||||
|
askingSSSS = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (askingSSSS) {
|
||||||
|
// no need to worry about shower/scanner here because if scanner was
|
||||||
|
// verified, ssss is already
|
||||||
|
setState(KeyVerificationState.askSSSS);
|
||||||
|
if (shower) {
|
||||||
|
_nextAction = 'done';
|
||||||
|
} else {
|
||||||
|
_nextAction = 'showQRSuccess';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (shower) {
|
||||||
|
setState(KeyVerificationState.done);
|
||||||
|
} else {
|
||||||
|
setState(KeyVerificationState.showQRSuccess);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> verifyActivity() async {
|
Future<bool> verifyActivity() async {
|
||||||
if (lastActivity.add(Duration(minutes: 10)).isAfter(DateTime.now())) {
|
if (lastActivity.add(Duration(minutes: 10)).isAfter(DateTime.now())) {
|
||||||
lastActivity = DateTime.now();
|
lastActivity = DateTime.now();
|
||||||
|
|
@ -577,6 +863,8 @@ class KeyVerification {
|
||||||
if (checkLastStep.contains(lastStep)) {
|
if (checkLastStep.contains(lastStep)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
Logs().e(
|
||||||
|
'[KeyVerificaton] lastStep mismatch cancelling, expected from ${checkLastStep.toString()} was ${lastStep.toString()}');
|
||||||
await cancel('m.unexpected_message');
|
await cancel('m.unexpected_message');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -668,6 +956,138 @@ class KeyVerification {
|
||||||
|
|
||||||
onUpdate?.call();
|
onUpdate?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const String prefix = 'MATRIX';
|
||||||
|
static const int version = 0x02;
|
||||||
|
|
||||||
|
Future<bool> verifyQrData(Uint8List qrDataRawBytes) async {
|
||||||
|
final data = qrDataRawBytes;
|
||||||
|
// hardcoded stuff + 2 keys + secret
|
||||||
|
if (data.length < 10 + 32 + 32 + 8 + utf8.encode(transactionId!).length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (data[6] != version) return false;
|
||||||
|
final remoteQrMode =
|
||||||
|
QRMode.values.singleWhere((mode) => mode.code == data[7]);
|
||||||
|
if (ascii.decode(data.sublist(0, 6)) != prefix) return false;
|
||||||
|
if (data[6] != version) return false;
|
||||||
|
final tmpBuf = Uint8List.fromList([data[8], data[9]]);
|
||||||
|
final encodedTxnLen = ByteData.view(tmpBuf.buffer).getUint16(0);
|
||||||
|
if (utf8.decode(data.sublist(10, 10 + encodedTxnLen)) != transactionId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final keys = client.userDeviceKeys;
|
||||||
|
|
||||||
|
final ownKeys = keys[client.userID];
|
||||||
|
final otherUserKeys = keys[userId];
|
||||||
|
final ownMasterKey = ownKeys?.getCrossSigningKey('master');
|
||||||
|
final ownDeviceKey = ownKeys?.getKey(client.deviceID!);
|
||||||
|
final ownOtherDeviceKey = ownKeys?.getKey(deviceId!);
|
||||||
|
final otherUserMasterKey = otherUserKeys?.masterKey;
|
||||||
|
|
||||||
|
final secondKey = encodeBase64Unpadded(
|
||||||
|
data.sublist(10 + encodedTxnLen + 32, 10 + encodedTxnLen + 32 + 32));
|
||||||
|
final randomSharedSecret =
|
||||||
|
encodeBase64Unpadded(data.sublist(10 + encodedTxnLen + 32 + 32));
|
||||||
|
|
||||||
|
/// `request.randomSharedSecretForQRCode` is overwritten below to send with `sendStart`
|
||||||
|
if ({QRMode.verifyOtherUser, QRMode.verifySelfUntrusted}
|
||||||
|
.contains(remoteQrMode)) {
|
||||||
|
if (!(ownMasterKey?.verified ?? false)) {
|
||||||
|
Logs().e(
|
||||||
|
'[KeyVerification] verifyQrData because you were in mode 0/2 and had untrusted msk');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteQrMode == QRMode.verifyOtherUser &&
|
||||||
|
otherUserMasterKey != null &&
|
||||||
|
ownMasterKey != null) {
|
||||||
|
if (secondKey == ownMasterKey.ed25519Key) {
|
||||||
|
randomSharedSecretForQRCode = randomSharedSecret;
|
||||||
|
await verifyKeysQR(otherUserMasterKey, shower: false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (remoteQrMode == QRMode.verifySelfTrusted &&
|
||||||
|
ownMasterKey != null &&
|
||||||
|
ownDeviceKey != null) {
|
||||||
|
if (secondKey == ownDeviceKey.ed25519Key) {
|
||||||
|
randomSharedSecretForQRCode = randomSharedSecret;
|
||||||
|
await verifyKeysQR(ownMasterKey, shower: false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (remoteQrMode == QRMode.verifySelfUntrusted &&
|
||||||
|
ownOtherDeviceKey != null &&
|
||||||
|
ownMasterKey != null) {
|
||||||
|
if (secondKey == ownMasterKey.ed25519Key) {
|
||||||
|
randomSharedSecretForQRCode = randomSharedSecret;
|
||||||
|
await verifyKeysQR(ownOtherDeviceKey, shower: false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<(String, String)?> getKeys(QRMode mode) async {
|
||||||
|
final keys = client.userDeviceKeys;
|
||||||
|
|
||||||
|
final ownKeys = keys[client.userID];
|
||||||
|
final otherUserKeys = keys[userId];
|
||||||
|
final ownDeviceKey = ownKeys?.getKey(client.deviceID!);
|
||||||
|
final ownMasterKey = ownKeys?.getCrossSigningKey('master');
|
||||||
|
final otherDeviceKey = otherUserKeys?.getKey(deviceId!);
|
||||||
|
final otherMasterKey = otherUserKeys?.getCrossSigningKey('master');
|
||||||
|
|
||||||
|
if (mode == QRMode.verifyOtherUser &&
|
||||||
|
ownMasterKey != null &&
|
||||||
|
otherMasterKey != null) {
|
||||||
|
// we already have this check when sending `knownVerificationMethods`, but
|
||||||
|
// just to be safe anyway
|
||||||
|
if (ownMasterKey.verified) {
|
||||||
|
return (ownMasterKey.ed25519Key!, otherMasterKey.ed25519Key!);
|
||||||
|
}
|
||||||
|
} else if (mode == QRMode.verifySelfTrusted &&
|
||||||
|
ownMasterKey != null &&
|
||||||
|
otherDeviceKey != null) {
|
||||||
|
if (ownMasterKey.verified) {
|
||||||
|
return (ownMasterKey.ed25519Key!, otherDeviceKey.ed25519Key!);
|
||||||
|
}
|
||||||
|
} else if (mode == QRMode.verifySelfUntrusted &&
|
||||||
|
ownMasterKey != null &&
|
||||||
|
ownDeviceKey != null) {
|
||||||
|
return (ownDeviceKey.ed25519Key!, ownMasterKey.ed25519Key!);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<QRCode?> generateQrCode() async {
|
||||||
|
final data = Uint8Buffer();
|
||||||
|
// why 11? https://github.com/matrix-org/matrix-js-sdk/commit/275ea6aacbfc6623e7559a7649ca5cab207903d9
|
||||||
|
randomSharedSecretForQRCode =
|
||||||
|
encodeBase64Unpadded(uc.secureRandomBytes(11));
|
||||||
|
|
||||||
|
final mode = getOurQRMode();
|
||||||
|
data.addAll(ascii.encode(prefix));
|
||||||
|
data.add(version);
|
||||||
|
data.add(mode.code);
|
||||||
|
final encodedTxnId = utf8.encode(transactionId!);
|
||||||
|
final txnIdLen = encodedTxnId.length;
|
||||||
|
final tmpBuf = Uint8List(2);
|
||||||
|
ByteData.view(tmpBuf.buffer).setUint16(0, txnIdLen);
|
||||||
|
data.addAll(tmpBuf);
|
||||||
|
data.addAll(encodedTxnId);
|
||||||
|
final keys = await getKeys(mode);
|
||||||
|
if (keys != null) {
|
||||||
|
data.addAll(base64decodeUnpadded(keys.$1));
|
||||||
|
data.addAll(base64decodeUnpadded(keys.$2));
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.addAll(base64decodeUnpadded(randomSharedSecretForQRCode!));
|
||||||
|
return QRCode(randomSharedSecretForQRCode!, data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class _KeyVerificationMethod {
|
abstract class _KeyVerificationMethod {
|
||||||
|
|
@ -688,6 +1108,99 @@ abstract class _KeyVerificationMethod {
|
||||||
void dispose() {}
|
void dispose() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _KeyVerificationMethodQRReciprocate extends _KeyVerificationMethod {
|
||||||
|
_KeyVerificationMethodQRReciprocate({required super.request});
|
||||||
|
|
||||||
|
@override
|
||||||
|
// ignore: overridden_fields
|
||||||
|
final _type = EventTypes.Reciprocate;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool validateStart(Map<String, dynamic> payload) {
|
||||||
|
if (payload['method'] != type) return false;
|
||||||
|
if (payload['secret'] != request.randomSharedSecretForQRCode) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> handlePayload(String type, Map<String, dynamic> payload) async {
|
||||||
|
try {
|
||||||
|
switch (type) {
|
||||||
|
case EventTypes.KeyVerificationStart:
|
||||||
|
if (!(await request.verifyLastStep([
|
||||||
|
'm.key.verification.ready',
|
||||||
|
EventTypes.KeyVerificationRequest,
|
||||||
|
]))) {
|
||||||
|
return; // abort
|
||||||
|
}
|
||||||
|
if (!validateStart(payload)) {
|
||||||
|
await request.cancel('m.invalid_message');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
request.setState(KeyVerificationState.confirmQRScan);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs().e('[Key Verification Reciprocate] An error occured', e, s);
|
||||||
|
if (request.deviceId != null) {
|
||||||
|
await request.cancel('m.invalid_message');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> acceptQRScanConfirmation() async {
|
||||||
|
// secret validation already done in validateStart
|
||||||
|
|
||||||
|
final ourQRMode = request.getOurQRMode();
|
||||||
|
SignableKey? keyToVerify;
|
||||||
|
|
||||||
|
if (ourQRMode == QRMode.verifyOtherUser) {
|
||||||
|
keyToVerify = client.userDeviceKeys[request.userId]?.masterKey;
|
||||||
|
} else if (ourQRMode == QRMode.verifySelfTrusted) {
|
||||||
|
keyToVerify =
|
||||||
|
client.userDeviceKeys[client.userID]?.deviceKeys[request.deviceId];
|
||||||
|
} else if (ourQRMode == QRMode.verifySelfUntrusted) {
|
||||||
|
keyToVerify = client.userDeviceKeys[client.userID]?.masterKey;
|
||||||
|
}
|
||||||
|
if (keyToVerify != null) {
|
||||||
|
await request.verifyKeysQR(keyToVerify, shower: true);
|
||||||
|
} else {
|
||||||
|
Logs().e('[KeyVerification], verifying keys failed');
|
||||||
|
await request.cancel('m.invalid_key');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> sendStart() async {
|
||||||
|
final payload = <String, dynamic>{
|
||||||
|
'method': type,
|
||||||
|
'secret': request.randomSharedSecretForQRCode,
|
||||||
|
};
|
||||||
|
request.makePayload(payload);
|
||||||
|
await request.send(EventTypes.KeyVerificationStart, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum QRMode {
|
||||||
|
verifyOtherUser(0x00),
|
||||||
|
verifySelfTrusted(0x01),
|
||||||
|
verifySelfUntrusted(0x02);
|
||||||
|
|
||||||
|
const QRMode(this.code);
|
||||||
|
final int code;
|
||||||
|
}
|
||||||
|
|
||||||
|
class QRCode {
|
||||||
|
/// You actually never need this when implementing in a client, its just to
|
||||||
|
/// make tests easier. Just pass `qrDataRawBytes` in `continueVerifcation()`
|
||||||
|
final String randomSharedSecret;
|
||||||
|
final Uint8Buffer qrDataRawBytes;
|
||||||
|
QRCode(this.randomSharedSecret, this.qrDataRawBytes);
|
||||||
|
}
|
||||||
|
|
||||||
const knownKeyAgreementProtocols = ['curve25519-hkdf-sha256', 'curve25519'];
|
const knownKeyAgreementProtocols = ['curve25519-hkdf-sha256', 'curve25519'];
|
||||||
const knownHashes = ['sha256'];
|
const knownHashes = ['sha256'];
|
||||||
const knownHashesAuthentificationCodes = ['hkdf-hmac-sha256'];
|
const knownHashesAuthentificationCodes = ['hkdf-hmac-sha256'];
|
||||||
|
|
@ -698,7 +1211,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
// ignore: overridden_fields
|
// ignore: overridden_fields
|
||||||
final _type = 'm.sas.v1';
|
final _type = EventTypes.Sas;
|
||||||
|
|
||||||
String? keyAgreementProtocol;
|
String? keyAgreementProtocol;
|
||||||
String? hash;
|
String? hash;
|
||||||
|
|
@ -734,6 +1247,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case EventTypes.KeyVerificationStart:
|
case EventTypes.KeyVerificationStart:
|
||||||
if (!(await request.verifyLastStep([
|
if (!(await request.verifyLastStep([
|
||||||
|
'm.key.verification.ready',
|
||||||
EventTypes.KeyVerificationRequest,
|
EventTypes.KeyVerificationRequest,
|
||||||
EventTypes.KeyVerificationStart
|
EventTypes.KeyVerificationStart
|
||||||
]))) {
|
]))) {
|
||||||
|
|
@ -746,7 +1260,10 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
||||||
await _sendAccept();
|
await _sendAccept();
|
||||||
break;
|
break;
|
||||||
case EventTypes.KeyVerificationAccept:
|
case EventTypes.KeyVerificationAccept:
|
||||||
if (!(await request.verifyLastStep(['m.key.verification.ready']))) {
|
if (!(await request.verifyLastStep([
|
||||||
|
'm.key.verification.ready',
|
||||||
|
EventTypes.KeyVerificationRequest
|
||||||
|
]))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!_handleAccept(payload)) {
|
if (!_handleAccept(payload)) {
|
||||||
|
|
@ -982,7 +1499,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
||||||
mac[entry.key] = entry.value;
|
mac[entry.key] = entry.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await request.verifyKeys(mac, (String mac, SignableKey key) async {
|
await request.verifyKeysSAS(mac, (String mac, SignableKey key) async {
|
||||||
return mac ==
|
return mac ==
|
||||||
_calculateMac(
|
_calculateMac(
|
||||||
key.ed25519Key!, '${baseInfo}ed25519:${key.identifier!}');
|
key.ed25519Key!, '${baseInfo}ed25519:${key.identifier!}');
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ homepage: https://famedly.com
|
||||||
repository: https://gitlab.com/famedly/company/frontend/famedlysdk.git
|
repository: https://gitlab.com/famedly/company/frontend/famedlysdk.git
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.18.0 <4.0.0"
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
async: ^2.8.0
|
async: ^2.8.0
|
||||||
|
|
@ -28,6 +28,7 @@ dependencies:
|
||||||
random_string: ^2.3.1
|
random_string: ^2.3.1
|
||||||
sdp_transform: ^0.3.2
|
sdp_transform: ^0.3.2
|
||||||
slugify: ^2.0.0
|
slugify: ^2.0.0
|
||||||
|
typed_data: ^1.3.2
|
||||||
webrtc_interface: ^1.0.13
|
webrtc_interface: ^1.0.13
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
@ -28,19 +29,6 @@ import '../fake_client.dart';
|
||||||
import '../fake_database.dart';
|
import '../fake_database.dart';
|
||||||
import '../fake_matrix_api.dart';
|
import '../fake_matrix_api.dart';
|
||||||
|
|
||||||
class MockSSSS extends SSSS {
|
|
||||||
MockSSSS(Encryption encryption) : super(encryption);
|
|
||||||
|
|
||||||
bool requestedSecrets = false;
|
|
||||||
@override
|
|
||||||
Future<void> maybeRequestAll([List<DeviceKeys>? devices]) async {
|
|
||||||
requestedSecrets = true;
|
|
||||||
final handle = open();
|
|
||||||
await handle.unlock(recoveryKey: ssssKey);
|
|
||||||
await handle.maybeCacheAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EventUpdate getLastSentEvent(KeyVerification req) {
|
EventUpdate getLastSentEvent(KeyVerification req) {
|
||||||
final entry = FakeMatrixApi.calledEndpoints.entries
|
final entry = FakeMatrixApi.calledEndpoints.entries
|
||||||
.firstWhere((p) => p.key.contains('/send/'));
|
.firstWhere((p) => p.key.contains('/send/'));
|
||||||
|
|
@ -82,7 +70,6 @@ void main() async {
|
||||||
|
|
||||||
late Client client1;
|
late Client client1;
|
||||||
late Client client2;
|
late Client client2;
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
client1 = await getClient();
|
client1 = await getClient();
|
||||||
client2 = Client(
|
client2 = Client(
|
||||||
|
|
@ -103,11 +90,16 @@ void main() async {
|
||||||
await Future.delayed(Duration(milliseconds: 10));
|
await Future.delayed(Duration(milliseconds: 10));
|
||||||
client1.verificationMethods = {
|
client1.verificationMethods = {
|
||||||
KeyVerificationMethod.emoji,
|
KeyVerificationMethod.emoji,
|
||||||
KeyVerificationMethod.numbers
|
KeyVerificationMethod.numbers,
|
||||||
|
KeyVerificationMethod.qrScan,
|
||||||
|
KeyVerificationMethod.qrShow,
|
||||||
|
KeyVerificationMethod.reciprocate
|
||||||
};
|
};
|
||||||
client2.verificationMethods = {
|
client2.verificationMethods = {
|
||||||
KeyVerificationMethod.emoji,
|
KeyVerificationMethod.emoji,
|
||||||
KeyVerificationMethod.numbers
|
KeyVerificationMethod.numbers,
|
||||||
|
KeyVerificationMethod.qrShow,
|
||||||
|
KeyVerificationMethod.reciprocate
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
|
|
@ -143,18 +135,24 @@ void main() async {
|
||||||
client2.encryption!.keyVerificationManager
|
client2.encryption!.keyVerificationManager
|
||||||
.getRequest(req2.transactionId!),
|
.getRequest(req2.transactionId!),
|
||||||
req2);
|
req2);
|
||||||
|
|
||||||
// send ready
|
// send ready
|
||||||
FakeMatrixApi.calledEndpoints.clear();
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
await req2.acceptVerification();
|
await req2.acceptVerification();
|
||||||
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
||||||
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.ready'));
|
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.ready'));
|
||||||
evt = getLastSentEvent(req2);
|
|
||||||
expect(req2.state, KeyVerificationState.waitingAccept);
|
|
||||||
|
|
||||||
// send start
|
evt = getLastSentEvent(req2);
|
||||||
|
expect(req2.state, KeyVerificationState.askChoice);
|
||||||
|
|
||||||
FakeMatrixApi.calledEndpoints.clear();
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
|
||||||
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
|
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
|
||||||
|
|
||||||
|
expect(req1.possibleMethods, [EventTypes.Sas]);
|
||||||
|
expect(req2.possibleMethods, [EventTypes.Sas]);
|
||||||
|
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||||
|
|
||||||
|
// no need for start (continueVerification) because sas only mode override already sent it after ready
|
||||||
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
||||||
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.start'));
|
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.start'));
|
||||||
evt = getLastSentEvent(req1);
|
evt = getLastSentEvent(req1);
|
||||||
|
|
@ -288,11 +286,16 @@ void main() async {
|
||||||
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
||||||
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.ready'));
|
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.ready'));
|
||||||
evt = getLastSentEvent(req2);
|
evt = getLastSentEvent(req2);
|
||||||
expect(req2.state, KeyVerificationState.waitingAccept);
|
expect(req2.state, KeyVerificationState.askChoice);
|
||||||
|
|
||||||
// send start
|
|
||||||
FakeMatrixApi.calledEndpoints.clear();
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
|
||||||
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
|
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
|
||||||
|
|
||||||
|
expect(req1.possibleMethods, [EventTypes.Sas]);
|
||||||
|
expect(req2.possibleMethods, [EventTypes.Sas]);
|
||||||
|
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||||
|
|
||||||
|
// no need for start (continueVerification) because sas only mode override already sent it after ready
|
||||||
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
||||||
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.start'));
|
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.start'));
|
||||||
evt = getLastSentEvent(req1);
|
evt = getLastSentEvent(req1);
|
||||||
|
|
@ -370,13 +373,8 @@ void main() async {
|
||||||
await req1.openSSSS(recoveryKey: ssssKey);
|
await req1.openSSSS(recoveryKey: ssssKey);
|
||||||
expect(req1.state, KeyVerificationState.done);
|
expect(req1.state, KeyVerificationState.done);
|
||||||
|
|
||||||
client1.encryption!.ssss = MockSSSS(client1.encryption!);
|
// let any background box usage from ssss signing finish
|
||||||
(client1.encryption!.ssss as MockSSSS).requestedSecrets = false;
|
await Future.delayed(Duration(seconds: 1));
|
||||||
await client1.encryption!.ssss.clearCache();
|
|
||||||
await req1.maybeRequestSSSSSecrets();
|
|
||||||
await Future.delayed(Duration(milliseconds: 10));
|
|
||||||
expect((client1.encryption!.ssss as MockSSSS).requestedSecrets, true);
|
|
||||||
|
|
||||||
await client1.encryption!.keyVerificationManager.cleanup();
|
await client1.encryption!.keyVerificationManager.cleanup();
|
||||||
await client2.encryption!.keyVerificationManager.cleanup();
|
await client2.encryption!.keyVerificationManager.cleanup();
|
||||||
});
|
});
|
||||||
|
|
@ -440,11 +438,17 @@ void main() async {
|
||||||
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
||||||
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.ready'));
|
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.ready'));
|
||||||
evt = getLastSentEvent(req2);
|
evt = getLastSentEvent(req2);
|
||||||
expect(req2.state, KeyVerificationState.waitingAccept);
|
expect(req2.state, KeyVerificationState.askChoice);
|
||||||
|
|
||||||
// send start
|
|
||||||
FakeMatrixApi.calledEndpoints.clear();
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
|
||||||
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
|
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
|
||||||
|
|
||||||
|
expect(req1.possibleMethods, [EventTypes.Sas]);
|
||||||
|
expect(req2.possibleMethods, [EventTypes.Sas]);
|
||||||
|
|
||||||
|
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||||
|
|
||||||
|
// no need for start (continueVerification) because sas only mode override already sent it after ready
|
||||||
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
||||||
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.start'));
|
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.start'));
|
||||||
evt = getLastSentEvent(req1);
|
evt = getLastSentEvent(req1);
|
||||||
|
|
@ -516,7 +520,7 @@ void main() async {
|
||||||
'event_id': req2.transactionId,
|
'event_id': req2.transactionId,
|
||||||
'type': 'm.key.verification.ready',
|
'type': 'm.key.verification.ready',
|
||||||
'content': {
|
'content': {
|
||||||
'methods': ['m.sas.v1'],
|
'methods': [EventTypes.Sas],
|
||||||
'from_device': 'SOMEOTHERDEVICE',
|
'from_device': 'SOMEOTHERDEVICE',
|
||||||
'm.relates_to': {
|
'm.relates_to': {
|
||||||
'rel_type': 'm.reference',
|
'rel_type': 'm.reference',
|
||||||
|
|
@ -537,5 +541,274 @@ void main() async {
|
||||||
await client1.encryption!.keyVerificationManager.cleanup();
|
await client1.encryption!.keyVerificationManager.cleanup();
|
||||||
await client2.encryption!.keyVerificationManager.cleanup();
|
await client2.encryption!.keyVerificationManager.cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Run qr verification mode 0, ssss start', () async {
|
||||||
|
expect(client1.userDeviceKeys[client2.userID]?.masterKey!.directVerified,
|
||||||
|
false);
|
||||||
|
expect(client2.userDeviceKeys[client1.userID]?.masterKey!.directVerified,
|
||||||
|
false);
|
||||||
|
// for a full run we test in-room verification in a cleartext room
|
||||||
|
// because then we can easily intercept the payloads and inject in the other client
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
// make sure our master key is *not* verified to not triger SSSS for now
|
||||||
|
client1.userDeviceKeys[client1.userID]!.masterKey!
|
||||||
|
.setDirectVerified(true);
|
||||||
|
client2.userDeviceKeys[client2.userID]!.masterKey!
|
||||||
|
.setDirectVerified(true);
|
||||||
|
await client1.encryption!.ssss.clearCache();
|
||||||
|
|
||||||
|
final req1 =
|
||||||
|
await client1.userDeviceKeys[client2.userID]!.startVerification(
|
||||||
|
newDirectChatEnableEncryption: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(req1.state, KeyVerificationState.askSSSS);
|
||||||
|
await req1.openSSSS(recoveryKey: ssssKey);
|
||||||
|
|
||||||
|
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
||||||
|
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.room.message'));
|
||||||
|
|
||||||
|
var evt = getLastSentEvent(req1);
|
||||||
|
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||||
|
|
||||||
|
final comp = Completer<KeyVerification>();
|
||||||
|
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
|
||||||
|
comp.complete(req);
|
||||||
|
});
|
||||||
|
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
|
||||||
|
final req2 = await comp.future;
|
||||||
|
await sub.cancel();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
client2.encryption!.keyVerificationManager
|
||||||
|
.getRequest(req2.transactionId!),
|
||||||
|
req2);
|
||||||
|
|
||||||
|
// send ready
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
await req2.acceptVerification();
|
||||||
|
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
||||||
|
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.ready'));
|
||||||
|
evt = getLastSentEvent(req2);
|
||||||
|
expect(req2.state, KeyVerificationState.askChoice);
|
||||||
|
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
|
||||||
|
expect(req1.state, KeyVerificationState.askChoice);
|
||||||
|
expect(req1.possibleMethods,
|
||||||
|
[EventTypes.Sas, EventTypes.Reciprocate, EventTypes.QRScan]);
|
||||||
|
expect(req2.possibleMethods,
|
||||||
|
[EventTypes.Sas, EventTypes.Reciprocate, EventTypes.QRShow]);
|
||||||
|
|
||||||
|
// send start
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
await req1.continueVerification(EventTypes.Reciprocate,
|
||||||
|
qrDataRawBytes:
|
||||||
|
Uint8List.fromList(req2.qrCode?.qrDataRawBytes ?? []));
|
||||||
|
expect(req2.qrCode!.randomSharedSecret, req1.randomSharedSecretForQRCode);
|
||||||
|
expect(req1.state, KeyVerificationState.showQRSuccess);
|
||||||
|
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
||||||
|
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.start'));
|
||||||
|
evt = getLastSentEvent(req1);
|
||||||
|
|
||||||
|
// send done
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
|
||||||
|
expect(req2.state, KeyVerificationState.confirmQRScan);
|
||||||
|
await req2.acceptQRScanConfirmation();
|
||||||
|
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
||||||
|
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.done'));
|
||||||
|
evt = getLastSentEvent(req2);
|
||||||
|
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
|
||||||
|
|
||||||
|
expect(req1.state, KeyVerificationState.done);
|
||||||
|
expect(req2.state, KeyVerificationState.done);
|
||||||
|
|
||||||
|
expect(client1.userDeviceKeys[client2.userID]?.masterKey!.directVerified,
|
||||||
|
true);
|
||||||
|
expect(client2.userDeviceKeys[client1.userID]?.masterKey!.directVerified,
|
||||||
|
true);
|
||||||
|
|
||||||
|
await client1.encryption!.keyVerificationManager.cleanup();
|
||||||
|
await client2.encryption!.keyVerificationManager.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Run qr verification mode 0, but fail on masterKey unverified client1',
|
||||||
|
() async {
|
||||||
|
// for a full run we test in-room verification in a cleartext room
|
||||||
|
// because then we can easily intercept the payloads and inject in the other client
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
// make sure our master key is *not* verified to not triger SSSS for now
|
||||||
|
client1.userDeviceKeys[client1.userID]!.masterKey!
|
||||||
|
.setDirectVerified(false);
|
||||||
|
|
||||||
|
final req1 =
|
||||||
|
await client1.userDeviceKeys[client2.userID]!.startVerification(
|
||||||
|
newDirectChatEnableEncryption: false,
|
||||||
|
);
|
||||||
|
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
||||||
|
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.room.message'));
|
||||||
|
|
||||||
|
var evt = getLastSentEvent(req1);
|
||||||
|
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||||
|
|
||||||
|
final comp = Completer<KeyVerification>();
|
||||||
|
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
|
||||||
|
comp.complete(req);
|
||||||
|
});
|
||||||
|
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
|
||||||
|
final req2 = await comp.future;
|
||||||
|
await sub.cancel();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
client2.encryption!.keyVerificationManager
|
||||||
|
.getRequest(req2.transactionId!),
|
||||||
|
req2);
|
||||||
|
// send ready
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
await req2.acceptVerification();
|
||||||
|
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
||||||
|
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.ready'));
|
||||||
|
evt = getLastSentEvent(req2);
|
||||||
|
expect(req2.state, KeyVerificationState.askChoice);
|
||||||
|
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
|
||||||
|
|
||||||
|
expect(req1.possibleMethods, [EventTypes.Sas]);
|
||||||
|
expect(req2.possibleMethods, [EventTypes.Sas]);
|
||||||
|
|
||||||
|
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||||
|
|
||||||
|
// send start
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
// qrCode will be null here anyway because masterKey not signed
|
||||||
|
await req1.continueVerification(EventTypes.Reciprocate,
|
||||||
|
qrDataRawBytes:
|
||||||
|
Uint8List.fromList(req2.qrCode?.qrDataRawBytes ?? []));
|
||||||
|
expect(req1.state, KeyVerificationState.error);
|
||||||
|
|
||||||
|
await client1.encryption!.keyVerificationManager.cleanup();
|
||||||
|
await client2.encryption!.keyVerificationManager.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Run qr verification mode 0, but fail on masterKey unverified client2',
|
||||||
|
() async {
|
||||||
|
// for a full run we test in-room verification in a cleartext room
|
||||||
|
// because then we can easily intercept the payloads and inject in the other client
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
// make sure our master key is *not* verified to not triger SSSS for now
|
||||||
|
client1.userDeviceKeys[client1.userID]!.masterKey!
|
||||||
|
.setDirectVerified(false);
|
||||||
|
|
||||||
|
final req1 =
|
||||||
|
await client1.userDeviceKeys[client2.userID]!.startVerification(
|
||||||
|
newDirectChatEnableEncryption: false,
|
||||||
|
);
|
||||||
|
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
||||||
|
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.room.message'));
|
||||||
|
|
||||||
|
var evt = getLastSentEvent(req1);
|
||||||
|
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||||
|
|
||||||
|
final comp = Completer<KeyVerification>();
|
||||||
|
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
|
||||||
|
comp.complete(req);
|
||||||
|
});
|
||||||
|
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
|
||||||
|
final req2 = await comp.future;
|
||||||
|
await sub.cancel();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
client2.encryption!.keyVerificationManager
|
||||||
|
.getRequest(req2.transactionId!),
|
||||||
|
req2);
|
||||||
|
// send ready
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
await req2.acceptVerification();
|
||||||
|
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
||||||
|
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.ready'));
|
||||||
|
evt = getLastSentEvent(req2);
|
||||||
|
expect(req2.state, KeyVerificationState.askChoice);
|
||||||
|
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
|
||||||
|
|
||||||
|
expect(req1.possibleMethods, [EventTypes.Sas]);
|
||||||
|
expect(req2.possibleMethods, [EventTypes.Sas]);
|
||||||
|
|
||||||
|
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
// qrCode will be null here anyway because masterKey not signed
|
||||||
|
await req2.continueVerification(EventTypes.Reciprocate,
|
||||||
|
qrDataRawBytes:
|
||||||
|
Uint8List.fromList(req2.qrCode?.qrDataRawBytes ?? []));
|
||||||
|
expect(req2.state, KeyVerificationState.error);
|
||||||
|
|
||||||
|
await client1.encryption!.keyVerificationManager.cleanup();
|
||||||
|
await client2.encryption!.keyVerificationManager.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Run qr verification mode, but fail because no knownVerificationMethod',
|
||||||
|
() async {
|
||||||
|
client1.verificationMethods = {
|
||||||
|
KeyVerificationMethod.emoji,
|
||||||
|
KeyVerificationMethod.numbers
|
||||||
|
};
|
||||||
|
client2.verificationMethods = {
|
||||||
|
KeyVerificationMethod.emoji,
|
||||||
|
KeyVerificationMethod.numbers
|
||||||
|
};
|
||||||
|
|
||||||
|
// for a full run we test in-room verification in a cleartext room
|
||||||
|
// because then we can easily intercept the payloads and inject in the other client
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
// make sure our master key is *not* verified to not triger SSSS for now
|
||||||
|
client1.userDeviceKeys[client1.userID]!.masterKey!
|
||||||
|
.setDirectVerified(false);
|
||||||
|
|
||||||
|
final req1 =
|
||||||
|
await client1.userDeviceKeys[client2.userID]!.startVerification(
|
||||||
|
newDirectChatEnableEncryption: false,
|
||||||
|
);
|
||||||
|
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
||||||
|
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.room.message'));
|
||||||
|
|
||||||
|
var evt = getLastSentEvent(req1);
|
||||||
|
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||||
|
|
||||||
|
final comp = Completer<KeyVerification>();
|
||||||
|
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
|
||||||
|
comp.complete(req);
|
||||||
|
});
|
||||||
|
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
|
||||||
|
final req2 = await comp.future;
|
||||||
|
await sub.cancel();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
client2.encryption!.keyVerificationManager
|
||||||
|
.getRequest(req2.transactionId!),
|
||||||
|
req2);
|
||||||
|
// send ready
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
await req2.acceptVerification();
|
||||||
|
await FakeMatrixApi.firstWhere((e) => e.startsWith(
|
||||||
|
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.ready'));
|
||||||
|
evt = getLastSentEvent(req2);
|
||||||
|
expect(req2.state, KeyVerificationState.askChoice);
|
||||||
|
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
|
||||||
|
|
||||||
|
expect(req1.possibleMethods, [EventTypes.Sas]);
|
||||||
|
expect(req2.possibleMethods, [EventTypes.Sas]);
|
||||||
|
|
||||||
|
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
|
||||||
|
// qrCode will be null here anyway because qr isn't supported
|
||||||
|
await req1.continueVerification(EventTypes.Reciprocate,
|
||||||
|
qrDataRawBytes:
|
||||||
|
Uint8List.fromList(req2.qrCode?.qrDataRawBytes ?? []));
|
||||||
|
expect(req1.state, KeyVerificationState.error);
|
||||||
|
|
||||||
|
await client1.encryption!.keyVerificationManager.cleanup();
|
||||||
|
await client2.encryption!.keyVerificationManager.cleanup();
|
||||||
|
});
|
||||||
}, skip: skip);
|
}, skip: skip);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,536 @@
|
||||||
|
/*
|
||||||
|
* Famedly Matrix SDK
|
||||||
|
* Copyright (C) 2020 Famedly GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import 'package:matrix/encryption.dart';
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
import '../fake_client.dart';
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
// need to mock to pass correct data to handleToDeviceEvent
|
||||||
|
Future<void> ingestCorrectReadyEvent(
|
||||||
|
KeyVerification req1, KeyVerification req2) async {
|
||||||
|
final copyKnownVerificationMethods =
|
||||||
|
List.from(req2.knownVerificationMethods);
|
||||||
|
|
||||||
|
// this is the same logic from `acceptVerification()` just couldn't find a
|
||||||
|
// easy to to mock it
|
||||||
|
// qr code only works when atleast one side has verified master key
|
||||||
|
if (req2.userId == req2.client.userID) {
|
||||||
|
if (!(req2.client.userDeviceKeys[req2.client.userID]
|
||||||
|
?.deviceKeys[req2.deviceId]
|
||||||
|
?.hasValidSignatureChain(verifiedByTheirMasterKey: true) ??
|
||||||
|
false) &&
|
||||||
|
!(req2.client.userDeviceKeys[req2.client.userID]?.masterKey
|
||||||
|
?.verified ??
|
||||||
|
false)) {
|
||||||
|
copyKnownVerificationMethods
|
||||||
|
.removeWhere((element) => element.startsWith('m.qr_code'));
|
||||||
|
copyKnownVerificationMethods.remove(EventTypes.Reciprocate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await req1.client.encryption!.keyVerificationManager.handleToDeviceEvent(
|
||||||
|
ToDeviceEvent(
|
||||||
|
type: 'm.key.verification.ready',
|
||||||
|
sender: req2.client.userID!,
|
||||||
|
content: {
|
||||||
|
'from_device': req2.client.deviceID,
|
||||||
|
'methods': copyKnownVerificationMethods,
|
||||||
|
'transaction_id': req2.transactionId
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var olmEnabled = true;
|
||||||
|
try {
|
||||||
|
await olm.init();
|
||||||
|
olm.get_library_version();
|
||||||
|
} catch (e) {
|
||||||
|
olmEnabled = false;
|
||||||
|
Logs().w('[LibOlm] Failed to load LibOlm', e);
|
||||||
|
}
|
||||||
|
Logs().i('[LibOlm] Enabled: $olmEnabled');
|
||||||
|
|
||||||
|
final dynamic skip = olmEnabled ? false : 'olm library not available';
|
||||||
|
|
||||||
|
/// All Tests related to the ChatTime
|
||||||
|
group('Key Verification', () {
|
||||||
|
Logs().level = Level.error;
|
||||||
|
|
||||||
|
late Client client1;
|
||||||
|
late Client client2;
|
||||||
|
setUp(() async {
|
||||||
|
client1 = await getClient();
|
||||||
|
client2 = await getOtherClient();
|
||||||
|
|
||||||
|
await Future.delayed(Duration(milliseconds: 10));
|
||||||
|
client1.verificationMethods = {
|
||||||
|
KeyVerificationMethod.emoji,
|
||||||
|
KeyVerificationMethod.numbers,
|
||||||
|
KeyVerificationMethod.qrScan,
|
||||||
|
KeyVerificationMethod.reciprocate
|
||||||
|
};
|
||||||
|
client2.verificationMethods = {
|
||||||
|
KeyVerificationMethod.emoji,
|
||||||
|
KeyVerificationMethod.numbers,
|
||||||
|
KeyVerificationMethod.qrShow,
|
||||||
|
KeyVerificationMethod.reciprocate
|
||||||
|
};
|
||||||
|
});
|
||||||
|
tearDown(() async {
|
||||||
|
await client1.dispose(closeDatabase: true);
|
||||||
|
await client2.dispose(closeDatabase: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Run qr verification mode 1', () async {
|
||||||
|
expect(
|
||||||
|
client1.userDeviceKeys[client2.userID]?.masterKey!.verified, false);
|
||||||
|
expect(
|
||||||
|
client2.userDeviceKeys[client1.userID]?.masterKey!.verified, false);
|
||||||
|
expect(
|
||||||
|
client1.userDeviceKeys[client2.userID]?.deviceKeys[client2.deviceID]
|
||||||
|
?.verified,
|
||||||
|
false);
|
||||||
|
expect(
|
||||||
|
client2.userDeviceKeys[client1.userID]?.deviceKeys[client1.deviceID]
|
||||||
|
?.verified,
|
||||||
|
false);
|
||||||
|
// make sure our master key is *not* verified to not triger SSSS for now
|
||||||
|
client1.userDeviceKeys[client1.userID]!.masterKey!
|
||||||
|
.setDirectVerified(true);
|
||||||
|
client2.userDeviceKeys[client2.userID]!.masterKey!
|
||||||
|
.setDirectVerified(false);
|
||||||
|
|
||||||
|
await client1.encryption!.ssss.clearCache();
|
||||||
|
final req1 = await client1.userDeviceKeys[client2.userID]!
|
||||||
|
.startVerification(newDirectChatEnableEncryption: false);
|
||||||
|
|
||||||
|
expect(req1.state, KeyVerificationState.askSSSS);
|
||||||
|
await req1.openSSSS(recoveryKey: ssssKey);
|
||||||
|
|
||||||
|
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||||
|
|
||||||
|
final comp = Completer<KeyVerification>();
|
||||||
|
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
|
||||||
|
comp.complete(req);
|
||||||
|
});
|
||||||
|
await client2.encryption!.keyVerificationManager.handleToDeviceEvent(
|
||||||
|
ToDeviceEvent(
|
||||||
|
type: EventTypes.KeyVerificationRequest,
|
||||||
|
sender: req1.client.userID!,
|
||||||
|
content: {
|
||||||
|
'from_device': req1.client.deviceID,
|
||||||
|
'methods': req1.knownVerificationMethods,
|
||||||
|
'timestamp': DateTime.now().millisecondsSinceEpoch,
|
||||||
|
'transaction_id': req1.transactionId
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final req2 = await comp.future;
|
||||||
|
await sub.cancel();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
client2.encryption!.keyVerificationManager
|
||||||
|
.getRequest(req2.transactionId!),
|
||||||
|
req2);
|
||||||
|
|
||||||
|
expect(req1.possibleMethods, []);
|
||||||
|
await req2.acceptVerification();
|
||||||
|
|
||||||
|
expect(req2.state, KeyVerificationState.askChoice);
|
||||||
|
await ingestCorrectReadyEvent(req1, req2);
|
||||||
|
expect(req1.possibleMethods,
|
||||||
|
[EventTypes.Sas, EventTypes.Reciprocate, EventTypes.QRScan]);
|
||||||
|
expect(req2.possibleMethods,
|
||||||
|
[EventTypes.Sas, EventTypes.Reciprocate, EventTypes.QRShow]);
|
||||||
|
|
||||||
|
expect(req1.state, KeyVerificationState.askChoice);
|
||||||
|
|
||||||
|
expect(req1.getOurQRMode(), QRMode.verifySelfTrusted);
|
||||||
|
expect(req2.getOurQRMode(), QRMode.verifySelfUntrusted);
|
||||||
|
|
||||||
|
// send start
|
||||||
|
await req1.continueVerification(EventTypes.Reciprocate,
|
||||||
|
qrDataRawBytes:
|
||||||
|
Uint8List.fromList(req2.qrCode?.qrDataRawBytes ?? []));
|
||||||
|
|
||||||
|
expect(req2.qrCode!.randomSharedSecret, req1.randomSharedSecretForQRCode);
|
||||||
|
expect(req1.state, KeyVerificationState.showQRSuccess);
|
||||||
|
|
||||||
|
await client2.encryption!.keyVerificationManager.handleToDeviceEvent(
|
||||||
|
ToDeviceEvent(
|
||||||
|
type: 'm.key.verification.start',
|
||||||
|
sender: req1.client.userID!,
|
||||||
|
content: {
|
||||||
|
'from_device': req1.client.deviceID,
|
||||||
|
'm.relates_to': {
|
||||||
|
'event_id': req1.transactionId,
|
||||||
|
'rel_type': 'm.reference'
|
||||||
|
},
|
||||||
|
'method': EventTypes.Reciprocate,
|
||||||
|
'secret': req1.randomSharedSecretForQRCode,
|
||||||
|
'transaction_id': req1.transactionId,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(req2.state, KeyVerificationState.confirmQRScan);
|
||||||
|
|
||||||
|
await req2.acceptQRScanConfirmation();
|
||||||
|
|
||||||
|
await client1.encryption!.keyVerificationManager.handleToDeviceEvent(
|
||||||
|
ToDeviceEvent(
|
||||||
|
type: 'm.key.verification.done',
|
||||||
|
sender: req2.client.userID!,
|
||||||
|
content: {
|
||||||
|
'transaction_id': req2.transactionId,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(req1.state, KeyVerificationState.done);
|
||||||
|
expect(req2.state, KeyVerificationState.done);
|
||||||
|
|
||||||
|
expect(client1.userDeviceKeys[client2.userID]?.masterKey!.verified, true);
|
||||||
|
expect(client2.userDeviceKeys[client1.userID]?.masterKey!.verified, true);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
client1.userDeviceKeys[client2.userID]?.deviceKeys[client2.deviceID]
|
||||||
|
?.verified,
|
||||||
|
true);
|
||||||
|
expect(
|
||||||
|
client2.userDeviceKeys[client1.userID]?.deviceKeys[client1.deviceID]
|
||||||
|
?.verified,
|
||||||
|
true);
|
||||||
|
|
||||||
|
// let any background box usage from ssss signing finish
|
||||||
|
await Future.delayed(Duration(seconds: 1));
|
||||||
|
await client1.encryption!.keyVerificationManager.cleanup();
|
||||||
|
await client2.encryption!.keyVerificationManager.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Run qr verification mode 2', () async {
|
||||||
|
expect(
|
||||||
|
client1.userDeviceKeys[client2.userID]?.masterKey!.verified, false);
|
||||||
|
expect(
|
||||||
|
client2.userDeviceKeys[client1.userID]?.masterKey!.verified, false);
|
||||||
|
expect(
|
||||||
|
client1.userDeviceKeys[client2.userID]?.deviceKeys[client2.deviceID]
|
||||||
|
?.verified,
|
||||||
|
false);
|
||||||
|
expect(
|
||||||
|
client2.userDeviceKeys[client1.userID]?.deviceKeys[client1.deviceID]
|
||||||
|
?.verified,
|
||||||
|
false);
|
||||||
|
// make sure our master key is *not* verified to not triger SSSS for now
|
||||||
|
client1.userDeviceKeys[client1.userID]!.masterKey!
|
||||||
|
.setDirectVerified(false);
|
||||||
|
client2.userDeviceKeys[client2.userID]!.masterKey!
|
||||||
|
.setDirectVerified(true);
|
||||||
|
// await client1.encryption!.ssss.clearCache();
|
||||||
|
final req1 =
|
||||||
|
await client1.userDeviceKeys[client2.userID]!.startVerification(
|
||||||
|
newDirectChatEnableEncryption: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||||
|
|
||||||
|
final comp = Completer<KeyVerification>();
|
||||||
|
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
|
||||||
|
comp.complete(req);
|
||||||
|
});
|
||||||
|
await client2.encryption!.keyVerificationManager.handleToDeviceEvent(
|
||||||
|
ToDeviceEvent(
|
||||||
|
type: EventTypes.KeyVerificationRequest,
|
||||||
|
sender: req1.client.userID!,
|
||||||
|
content: {
|
||||||
|
'from_device': req1.client.deviceID,
|
||||||
|
'methods': req1.knownVerificationMethods,
|
||||||
|
'timestamp': DateTime.now().millisecondsSinceEpoch,
|
||||||
|
'transaction_id': req1.transactionId
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final req2 = await comp.future;
|
||||||
|
await sub.cancel();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
client2.encryption!.keyVerificationManager
|
||||||
|
.getRequest(req2.transactionId!),
|
||||||
|
req2);
|
||||||
|
|
||||||
|
expect(req1.possibleMethods, []);
|
||||||
|
await req2.acceptVerification();
|
||||||
|
|
||||||
|
expect(req2.state, KeyVerificationState.askChoice);
|
||||||
|
await ingestCorrectReadyEvent(req1, req2);
|
||||||
|
|
||||||
|
expect(req1.possibleMethods,
|
||||||
|
[EventTypes.Sas, EventTypes.Reciprocate, EventTypes.QRScan]);
|
||||||
|
expect(req2.possibleMethods,
|
||||||
|
[EventTypes.Sas, EventTypes.Reciprocate, EventTypes.QRShow]);
|
||||||
|
|
||||||
|
expect(req1.state, KeyVerificationState.askChoice);
|
||||||
|
|
||||||
|
expect(req1.getOurQRMode(), QRMode.verifySelfUntrusted);
|
||||||
|
expect(req2.getOurQRMode(), QRMode.verifySelfTrusted);
|
||||||
|
|
||||||
|
// send start
|
||||||
|
await req1.continueVerification(EventTypes.Reciprocate,
|
||||||
|
qrDataRawBytes:
|
||||||
|
Uint8List.fromList(req2.qrCode?.qrDataRawBytes ?? []));
|
||||||
|
|
||||||
|
expect(req2.qrCode!.randomSharedSecret, req1.randomSharedSecretForQRCode);
|
||||||
|
expect(req1.state, KeyVerificationState.showQRSuccess);
|
||||||
|
|
||||||
|
await client2.encryption!.keyVerificationManager.handleToDeviceEvent(
|
||||||
|
ToDeviceEvent(
|
||||||
|
type: 'm.key.verification.start',
|
||||||
|
sender: req1.client.userID!,
|
||||||
|
content: {
|
||||||
|
'from_device': req1.client.deviceID,
|
||||||
|
'm.relates_to': {
|
||||||
|
'event_id': req1.transactionId,
|
||||||
|
'rel_type': 'm.reference'
|
||||||
|
},
|
||||||
|
'method': EventTypes.Reciprocate,
|
||||||
|
'secret': req1.randomSharedSecretForQRCode,
|
||||||
|
'transaction_id': req1.transactionId,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(req2.state, KeyVerificationState.confirmQRScan);
|
||||||
|
|
||||||
|
await req2.acceptQRScanConfirmation();
|
||||||
|
|
||||||
|
await client1.encryption!.keyVerificationManager.handleToDeviceEvent(
|
||||||
|
ToDeviceEvent(
|
||||||
|
type: 'm.key.verification.done',
|
||||||
|
sender: req2.client.userID!,
|
||||||
|
content: {
|
||||||
|
'transaction_id': req2.transactionId,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(req1.state, KeyVerificationState.done);
|
||||||
|
expect(req2.state, KeyVerificationState.askSSSS);
|
||||||
|
await req2.openSSSS(recoveryKey: ssssKey);
|
||||||
|
expect(req2.state, KeyVerificationState.done);
|
||||||
|
|
||||||
|
expect(client1.userDeviceKeys[client2.userID]?.masterKey!.verified, true);
|
||||||
|
expect(client2.userDeviceKeys[client1.userID]?.masterKey!.verified, true);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
client1.userDeviceKeys[client2.userID]?.deviceKeys[client2.deviceID]
|
||||||
|
?.verified,
|
||||||
|
true);
|
||||||
|
expect(
|
||||||
|
client2.userDeviceKeys[client1.userID]?.deviceKeys[client1.deviceID]
|
||||||
|
?.verified,
|
||||||
|
true);
|
||||||
|
|
||||||
|
await client1.encryption!.keyVerificationManager.cleanup();
|
||||||
|
await client2.encryption!.keyVerificationManager.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Run qr verification mode 1, but fail because incorrect secret',
|
||||||
|
() async {
|
||||||
|
// make sure our master key is *not* verified to not triger SSSS for now
|
||||||
|
client1.userDeviceKeys[client1.userID]!.masterKey!
|
||||||
|
.setDirectVerified(true);
|
||||||
|
client2.userDeviceKeys[client2.userID]!.masterKey!
|
||||||
|
.setDirectVerified(false);
|
||||||
|
|
||||||
|
await client1.encryption!.ssss.clearCache();
|
||||||
|
final req1 =
|
||||||
|
await client1.userDeviceKeys[client2.userID]!.startVerification(
|
||||||
|
newDirectChatEnableEncryption: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(req1.state, KeyVerificationState.askSSSS);
|
||||||
|
await req1.openSSSS(recoveryKey: ssssKey);
|
||||||
|
|
||||||
|
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||||
|
|
||||||
|
final comp = Completer<KeyVerification>();
|
||||||
|
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
|
||||||
|
comp.complete(req);
|
||||||
|
});
|
||||||
|
await client2.encryption!.keyVerificationManager.handleToDeviceEvent(
|
||||||
|
ToDeviceEvent(
|
||||||
|
type: EventTypes.KeyVerificationRequest,
|
||||||
|
sender: req1.client.userID!,
|
||||||
|
content: {
|
||||||
|
'from_device': req1.client.deviceID,
|
||||||
|
'methods': req1.knownVerificationMethods,
|
||||||
|
'timestamp': DateTime.now().millisecondsSinceEpoch,
|
||||||
|
'transaction_id': req1.transactionId
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final req2 = await comp.future;
|
||||||
|
await sub.cancel();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
client2.encryption!.keyVerificationManager
|
||||||
|
.getRequest(req2.transactionId!),
|
||||||
|
req2);
|
||||||
|
|
||||||
|
expect(req1.possibleMethods, []);
|
||||||
|
await req2.acceptVerification();
|
||||||
|
|
||||||
|
expect(req2.possibleMethods,
|
||||||
|
[EventTypes.Sas, EventTypes.Reciprocate, EventTypes.QRShow]);
|
||||||
|
|
||||||
|
expect(req2.state, KeyVerificationState.askChoice);
|
||||||
|
await ingestCorrectReadyEvent(req1, req2);
|
||||||
|
|
||||||
|
expect(req1.possibleMethods,
|
||||||
|
[EventTypes.Sas, EventTypes.Reciprocate, EventTypes.QRScan]);
|
||||||
|
|
||||||
|
expect(req1.state, KeyVerificationState.askChoice);
|
||||||
|
|
||||||
|
expect(req1.getOurQRMode(), QRMode.verifySelfTrusted);
|
||||||
|
expect(req2.getOurQRMode(), QRMode.verifySelfUntrusted);
|
||||||
|
|
||||||
|
// send start
|
||||||
|
await req1.continueVerification(EventTypes.Reciprocate,
|
||||||
|
qrDataRawBytes:
|
||||||
|
Uint8List.fromList(req2.qrCode?.qrDataRawBytes ?? []));
|
||||||
|
|
||||||
|
expect(req2.qrCode!.randomSharedSecret, req1.randomSharedSecretForQRCode);
|
||||||
|
expect(req1.state, KeyVerificationState.showQRSuccess);
|
||||||
|
|
||||||
|
await client2.encryption!.keyVerificationManager.handleToDeviceEvent(
|
||||||
|
ToDeviceEvent(
|
||||||
|
type: 'm.key.verification.start',
|
||||||
|
sender: req1.client.userID!,
|
||||||
|
content: {
|
||||||
|
'from_device': req1.client.deviceID,
|
||||||
|
'm.relates_to': {
|
||||||
|
'event_id': req1.transactionId,
|
||||||
|
'rel_type': 'm.reference'
|
||||||
|
},
|
||||||
|
'method': EventTypes.Reciprocate,
|
||||||
|
'secret': 'fake_secret',
|
||||||
|
'transaction_id': req1.transactionId,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(req1.state, KeyVerificationState.showQRSuccess);
|
||||||
|
expect(req2.state, KeyVerificationState.error);
|
||||||
|
|
||||||
|
await client1.encryption!.keyVerificationManager.cleanup();
|
||||||
|
await client2.encryption!.keyVerificationManager.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Run qr verification mode 2, but both unverified master key',
|
||||||
|
() async {
|
||||||
|
// make sure our master key is *not* verified to not triger SSSS for now
|
||||||
|
await client1.userDeviceKeys[client1.userID]!.masterKey!.setBlocked(true);
|
||||||
|
await client2.userDeviceKeys[client2.userID]!.masterKey!.setBlocked(true);
|
||||||
|
// await client1.encryption!.ssss.clearCache();
|
||||||
|
final req1 =
|
||||||
|
await client1.userDeviceKeys[client2.userID]!.startVerification(
|
||||||
|
newDirectChatEnableEncryption: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||||
|
|
||||||
|
final comp = Completer<KeyVerification>();
|
||||||
|
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
|
||||||
|
comp.complete(req);
|
||||||
|
});
|
||||||
|
await client2.encryption!.keyVerificationManager.handleToDeviceEvent(
|
||||||
|
ToDeviceEvent(
|
||||||
|
type: EventTypes.KeyVerificationRequest,
|
||||||
|
sender: req1.client.userID!,
|
||||||
|
content: {
|
||||||
|
'from_device': req1.client.deviceID,
|
||||||
|
'methods': req1.knownVerificationMethods,
|
||||||
|
'timestamp': DateTime.now().millisecondsSinceEpoch,
|
||||||
|
'transaction_id': req1.transactionId
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final req2 = await comp.future;
|
||||||
|
await sub.cancel();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
client2.encryption!.keyVerificationManager
|
||||||
|
.getRequest(req2.transactionId!),
|
||||||
|
req2);
|
||||||
|
|
||||||
|
await req2.acceptVerification();
|
||||||
|
expect(req2.possibleMethods, [EventTypes.Sas]);
|
||||||
|
expect(req2.state, KeyVerificationState.askChoice);
|
||||||
|
|
||||||
|
await ingestCorrectReadyEvent(req1, req2);
|
||||||
|
|
||||||
|
expect(req1.possibleMethods, [EventTypes.Sas]);
|
||||||
|
|
||||||
|
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||||
|
|
||||||
|
expect(req1.getOurQRMode(), QRMode.verifySelfUntrusted);
|
||||||
|
expect(req2.getOurQRMode(), QRMode.verifySelfUntrusted);
|
||||||
|
|
||||||
|
// send start
|
||||||
|
await req1.continueVerification(EventTypes.Reciprocate,
|
||||||
|
qrDataRawBytes:
|
||||||
|
Uint8List.fromList(req2.qrCode?.qrDataRawBytes ?? []));
|
||||||
|
|
||||||
|
expect(req1.state, KeyVerificationState.error);
|
||||||
|
|
||||||
|
await client2.encryption!.keyVerificationManager.handleToDeviceEvent(
|
||||||
|
ToDeviceEvent(
|
||||||
|
type: 'm.key.verification.start',
|
||||||
|
sender: req1.client.userID!,
|
||||||
|
content: {
|
||||||
|
'from_device': req1.client.deviceID,
|
||||||
|
'm.relates_to': {
|
||||||
|
'event_id': req1.transactionId,
|
||||||
|
'rel_type': 'm.reference'
|
||||||
|
},
|
||||||
|
'method': EventTypes.Reciprocate,
|
||||||
|
'secret': 'stub_incorrect_secret_here',
|
||||||
|
'transaction_id': req1.transactionId,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(req1.state, KeyVerificationState.error);
|
||||||
|
expect(req2.state, KeyVerificationState.error);
|
||||||
|
|
||||||
|
await client1.encryption!.keyVerificationManager.cleanup();
|
||||||
|
await client2.encryption!.keyVerificationManager.cleanup();
|
||||||
|
});
|
||||||
|
}, skip: skip);
|
||||||
|
}
|
||||||
|
|
@ -47,3 +47,23 @@ Future<Client> getClient() async {
|
||||||
await Future.delayed(Duration(milliseconds: 10));
|
await Future.delayed(Duration(milliseconds: 10));
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Client> getOtherClient() async {
|
||||||
|
final client = Client(
|
||||||
|
'othertestclient',
|
||||||
|
httpClient: FakeMatrixApi(),
|
||||||
|
databaseBuilder: getDatabase,
|
||||||
|
);
|
||||||
|
FakeMatrixApi.client = client;
|
||||||
|
await client.checkHomeserver(Uri.parse('https://fakeServer.notExisting'),
|
||||||
|
checkWellKnown: false);
|
||||||
|
await client.init(
|
||||||
|
newToken: '1234',
|
||||||
|
newUserID: '@test:fakeServer.notExisting',
|
||||||
|
newHomeserver: client.homeserver,
|
||||||
|
newDeviceName: 'Text Matrix Client',
|
||||||
|
newDeviceID: 'OTHERDEVICE',
|
||||||
|
);
|
||||||
|
await Future.delayed(Duration(milliseconds: 10));
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2299,7 +2299,8 @@ class FakeMatrixApi extends BaseClient {
|
||||||
'user_id': '@othertest:fakeServer.notExisting',
|
'user_id': '@othertest:fakeServer.notExisting',
|
||||||
'usage': ['master'],
|
'usage': ['master'],
|
||||||
'keys': {
|
'keys': {
|
||||||
'ed25519:master': 'master',
|
'ed25519:92mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8':
|
||||||
|
'92mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8',
|
||||||
},
|
},
|
||||||
'signatures': {},
|
'signatures': {},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue