524 lines
18 KiB
Dart
524 lines
18 KiB
Dart
/*
|
|
* 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: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: 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<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();
|
|
});
|
|
});
|
|
}
|