/*
* 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 .
*/
import 'dart:async';
import 'dart:typed_data';
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 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: EventTypes.KeyVerificationReady,
sender: req2.client.userID!,
content: {
'from_device': req2.client.deviceID,
'methods': copyKnownVerificationMethods,
'transaction_id': req2.transactionId
},
),
);
}
/// All Tests related to the ChatTime
group('Key Verification', tags: 'olm', () {
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();
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();
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();
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();
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();
});
});
}