diff --git a/lib/encryption/key_verification_manager.dart b/lib/encryption/key_verification_manager.dart index ba80770..de82074 100644 --- a/lib/encryption/key_verification_manager.dart +++ b/lib/encryption/key_verification_manager.dart @@ -99,16 +99,16 @@ class KeyVerificationManager { if (_requests.containsKey(transactionId)) { final req = _requests[transactionId]; + final otherDeviceId = event['content']['from_device']; if (event['sender'] != client.userID) { await req.handlePayload(type, event['content'], event['event_id']); - } else if (req.userId == client.userID && req.deviceId == null) { - // okay, maybe another of our devices answered - await req.handlePayload(type, event['content'], event['event_id']); - if (req.deviceId != client.deviceID) { - req.otherDeviceAccepted(); - req.dispose(); - _requests.remove(transactionId); - } + } else if (event['sender'] == client.userID && + otherDeviceId != null && + otherDeviceId != client.deviceID) { + // okay, another of our devices answered + req.otherDeviceAccepted(); + req.dispose(); + _requests.remove(transactionId); } } else if (event['sender'] != client.userID) { final room = client.getRoomById(update.roomID) ?? diff --git a/test/encryption/key_verification_test.dart b/test/encryption/key_verification_test.dart index 1a9ddc9..66ccdee 100644 --- a/test/encryption/key_verification_test.dart +++ b/test/encryption/key_verification_test.dart @@ -16,12 +16,47 @@ * along with this program. If not, see . */ +import 'dart:convert'; + import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/encryption.dart'; import 'package:test/test.dart'; import 'package:olm/olm.dart' as olm; import '../fake_client.dart'; +import '../fake_matrix_api.dart'; + +class MockSSSS extends SSSS { + MockSSSS(Encryption encryption) : super(encryption); + + bool requestedSecrets = false; + @override + Future maybeRequestAll(List devices) async { + requestedSecrets = true; + final handle = open(); + handle.unlock(recoveryKey: SSSS_KEY); + await handle.maybeCacheAll(); + } +} + +EventUpdate getLastSentEvent(KeyVerification req) { + final entry = FakeMatrixApi.calledEndpoints.entries + .firstWhere((p) => p.key.contains('/send/')); + final type = entry.key.split('/')[6]; + final content = json.decode(entry.value.first); + return EventUpdate( + content: { + 'event_id': req.transactionId, + 'type': type, + 'content': content, + 'origin_server_ts': DateTime.now().millisecondsSinceEpoch, + 'sender': req.client.userID, + }, + eventType: type, + type: 'timeline', + roomID: req.room.id, + ); +} void main() { /// All Tests related to the ChatTime @@ -38,70 +73,392 @@ void main() { if (!olmEnabled) return; - Client client; - Room room; - var updateCounter = 0; - KeyVerification keyVerification; + // key @othertest:fakeServer.notExisting + const otherPickledOlmAccount = + 'VWhVApbkcilKAEGppsPDf9nNVjaK8/IxT3asSR0sYg0S5KgbfE8vXEPwoiKBX2cEvwX3OessOBOkk+ZE7TTbjlrh/KEd31p8Wo+47qj0AP+Ky+pabnhi+/rTBvZy+gfzTqUfCxZrkzfXI9Op4JnP6gYmy7dVX2lMYIIs9WCO1jcmIXiXum5jnfXu1WLfc7PZtO2hH+k9CDKosOFaXRBmsu8k/BGXPSoWqUpvu6WpEG9t5STk4FeAzA'; + + Client client1; + Client client2; test('setupClient', () async { - client = await getClient(); - room = Room(id: '!localpart:server.abc', client: client); - keyVerification = KeyVerification( - encryption: client.encryption, - room: room, - userId: '@alice:example.com', - deviceId: 'ABCD', - onUpdate: () => updateCounter++, + client1 = await getClient(); + client2 = + Client('othertestclient', debug: true, httpClient: FakeMatrixApi()); + client2.database = client1.database; + await client2.checkServer('https://fakeServer.notExisting'); + client2.connect( + newToken: 'abc', + newUserID: '@othertest:fakeServer.notExisting', + newHomeserver: client2.api.homeserver, + newDeviceName: 'Text Matrix Client', + newDeviceID: 'FOXDEVICE', + newOlmAccount: otherPickledOlmAccount, ); + await Future.delayed(Duration(milliseconds: 10)); + client1.verificationMethods = { + KeyVerificationMethod.emoji, + KeyVerificationMethod.numbers + }; + client2.verificationMethods = { + KeyVerificationMethod.emoji, + KeyVerificationMethod.numbers + }; }); - test('acceptSas', () async { - await keyVerification.acceptSas(); - }); - test('acceptVerification', () async { - await keyVerification.acceptVerification(); - }); - test('cancel', () async { - await keyVerification.cancel('m.cancelcode'); - expect(keyVerification.canceled, true); - expect(keyVerification.canceledCode, 'm.cancelcode'); - expect(keyVerification.canceledReason, null); - }); - test('handlePayload', () async { - await keyVerification.handlePayload('m.key.verification.request', { - 'from_device': 'AliceDevice2', - 'methods': ['m.sas.v1'], - 'timestamp': 1559598944869, - 'transaction_id': 'S0meUniqueAndOpaqueString' + test('Run emoji / number verification', () 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(); + var evt = getLastSentEvent(req1); + expect(req1.state, KeyVerificationState.waitingAccept); + + KeyVerification req2; + final sub = client2.onKeyVerificationRequest.stream.listen((req) { + req2 = req; }); - await keyVerification.handlePayload('m.key.verification.start', { - 'from_device': 'BobDevice1', - 'method': 'm.sas.v1', - 'transaction_id': 'S0meUniqueAndOpaqueString' + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + await Future.delayed(Duration(milliseconds: 10)); + await sub.cancel(); + expect(req2 != null, true); + + // send ready + FakeMatrixApi.calledEndpoints.clear(); + await req2.acceptVerification(); + evt = getLastSentEvent(req2); + expect(req2.state, KeyVerificationState.waitingAccept); + + // send start + FakeMatrixApi.calledEndpoints.clear(); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req1); + + // send accept + FakeMatrixApi.calledEndpoints.clear(); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req2); + + // send key + FakeMatrixApi.calledEndpoints.clear(); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req1); + + // send key + FakeMatrixApi.calledEndpoints.clear(); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req2); + + // receive last key + FakeMatrixApi.calledEndpoints.clear(); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + + // compare emoji + expect(req1.state, KeyVerificationState.askSas); + expect(req2.state, KeyVerificationState.askSas); + expect(req1.sasTypes[0], 'emoji'); + expect(req1.sasTypes[1], 'decimal'); + expect(req2.sasTypes[0], 'emoji'); + expect(req2.sasTypes[1], 'decimal'); + // compare emoji + final emoji1 = req1.sasEmojis; + final emoji2 = req2.sasEmojis; + for (var i = 0; i < 7; i++) { + expect(emoji1[i].emoji, emoji2[i].emoji); + expect(emoji1[i].name, emoji2[i].name); + } + // compare numbers + final numbers1 = req1.sasNumbers; + final numbers2 = req2.sasNumbers; + for (var i = 0; i < 3; i++) { + expect(numbers1[i], numbers2[i]); + } + + // alright, they match + + // send mac + FakeMatrixApi.calledEndpoints.clear(); + await req1.acceptSas(); + evt = getLastSentEvent(req1); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + expect(req1.state, KeyVerificationState.waitingSas); + + // send mac + FakeMatrixApi.calledEndpoints.clear(); + await req2.acceptSas(); + evt = getLastSentEvent(req2); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + + expect(req1.state, KeyVerificationState.done); + expect(req2.state, KeyVerificationState.done); + expect( + client1.userDeviceKeys[client2.userID].deviceKeys[client2.deviceID] + .directVerified, + true); + expect( + client2.userDeviceKeys[client1.userID].deviceKeys[client1.deviceID] + .directVerified, + true); + await client1.encryption.keyVerificationManager.cleanup(); + await client2.encryption.keyVerificationManager.cleanup(); + }); + + test('ask SSSS start', () async { + client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(true); + await client1.database.clearSSSSCache(client1.id); + final req1 = + await client1.userDeviceKeys[client2.userID].startVerification(); + expect(req1.state, KeyVerificationState.askSSSS); + await req1.openSSSS(recoveryKey: SSSS_KEY); + await Future.delayed(Duration(milliseconds: 10)); + expect(req1.state, KeyVerificationState.waitingAccept); + + await req1.cancel(); + await client1.encryption.keyVerificationManager.cleanup(); + }); + + test('ask SSSS end', () async { + FakeMatrixApi.calledEndpoints.clear(); + // make sure our master key is *not* verified to not triger SSSS for now + client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(false); + // the other one has to have their master key verified to trigger asking for ssss + client2.userDeviceKeys[client2.userID].masterKey.setDirectVerified(true); + final req1 = + await client1.userDeviceKeys[client2.userID].startVerification(); + var evt = getLastSentEvent(req1); + expect(req1.state, KeyVerificationState.waitingAccept); + + KeyVerification req2; + final sub = client2.onKeyVerificationRequest.stream.listen((req) { + req2 = req; }); - await keyVerification.handlePayload('m.key.verification.cancel', { - 'code': 'm.user', - 'reason': 'User rejected the key verification request', - 'transaction_id': 'S0meUniqueAndOpaqueString' + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + await Future.delayed(Duration(milliseconds: 10)); + await sub.cancel(); + expect(req2 != null, true); + + // send ready + FakeMatrixApi.calledEndpoints.clear(); + await req2.acceptVerification(); + evt = getLastSentEvent(req2); + expect(req2.state, KeyVerificationState.waitingAccept); + + // send start + FakeMatrixApi.calledEndpoints.clear(); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req1); + + // send accept + FakeMatrixApi.calledEndpoints.clear(); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req2); + + // send key + FakeMatrixApi.calledEndpoints.clear(); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req1); + + // send key + FakeMatrixApi.calledEndpoints.clear(); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req2); + + // receive last key + FakeMatrixApi.calledEndpoints.clear(); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + + // compare emoji + expect(req1.state, KeyVerificationState.askSas); + expect(req2.state, KeyVerificationState.askSas); + // compare emoji + final emoji1 = req1.sasEmojis; + final emoji2 = req2.sasEmojis; + for (var i = 0; i < 7; i++) { + expect(emoji1[i].emoji, emoji2[i].emoji); + expect(emoji1[i].name, emoji2[i].name); + } + // compare numbers + final numbers1 = req1.sasNumbers; + final numbers2 = req2.sasNumbers; + for (var i = 0; i < 3; i++) { + expect(numbers1[i], numbers2[i]); + } + + // alright, they match + client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(true); + await client1.database.clearSSSSCache(client1.id); + + // send mac + FakeMatrixApi.calledEndpoints.clear(); + await req1.acceptSas(); + evt = getLastSentEvent(req1); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + expect(req1.state, KeyVerificationState.waitingSas); + + // send mac + FakeMatrixApi.calledEndpoints.clear(); + await req2.acceptSas(); + evt = getLastSentEvent(req2); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + + expect(req1.state, KeyVerificationState.askSSSS); + expect(req2.state, KeyVerificationState.done); + + await req1.openSSSS(recoveryKey: SSSS_KEY); + await Future.delayed(Duration(milliseconds: 10)); + expect(req1.state, KeyVerificationState.done); + + client1.encryption.ssss = MockSSSS(client1.encryption); + (client1.encryption.ssss as MockSSSS).requestedSecrets = false; + await client1.database.clearSSSSCache(client1.id); + await req1.maybeRequestSSSSSecrets(); + await Future.delayed(Duration(milliseconds: 10)); + expect((client1.encryption.ssss as MockSSSS).requestedSecrets, true); + // delay for 12 seconds to be sure no other tests clear the ssss cache + await Future.delayed(Duration(seconds: 12)); + + await client1.encryption.keyVerificationManager.cleanup(); + await client2.encryption.keyVerificationManager.cleanup(); + }); + + test('reject verification', () async { + 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(); + var evt = getLastSentEvent(req1); + expect(req1.state, KeyVerificationState.waitingAccept); + + KeyVerification req2; + final sub = client2.onKeyVerificationRequest.stream.listen((req) { + req2 = req; }); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + await Future.delayed(Duration(milliseconds: 10)); + await sub.cancel(); + + FakeMatrixApi.calledEndpoints.clear(); + await req2.rejectVerification(); + evt = getLastSentEvent(req2); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + expect(req1.state, KeyVerificationState.error); + expect(req2.state, KeyVerificationState.error); + + await client1.encryption.keyVerificationManager.cleanup(); + await client2.encryption.keyVerificationManager.cleanup(); }); - test('rejectSas', () async { - await keyVerification.rejectSas(); + + test('reject sas', () async { + 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(); + var evt = getLastSentEvent(req1); + expect(req1.state, KeyVerificationState.waitingAccept); + + KeyVerification req2; + final sub = client2.onKeyVerificationRequest.stream.listen((req) { + req2 = req; + }); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + await Future.delayed(Duration(milliseconds: 10)); + await sub.cancel(); + expect(req2 != null, true); + + // send ready + FakeMatrixApi.calledEndpoints.clear(); + await req2.acceptVerification(); + evt = getLastSentEvent(req2); + expect(req2.state, KeyVerificationState.waitingAccept); + + // send start + FakeMatrixApi.calledEndpoints.clear(); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req1); + + // send accept + FakeMatrixApi.calledEndpoints.clear(); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req2); + + // send key + FakeMatrixApi.calledEndpoints.clear(); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req1); + + // send key + FakeMatrixApi.calledEndpoints.clear(); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + evt = getLastSentEvent(req2); + + // receive last key + FakeMatrixApi.calledEndpoints.clear(); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + + await req1.acceptSas(); + FakeMatrixApi.calledEndpoints.clear(); + await req2.rejectSas(); + evt = getLastSentEvent(req2); + await client1.encryption.keyVerificationManager.handleEventUpdate(evt); + expect(req1.state, KeyVerificationState.error); + expect(req2.state, KeyVerificationState.error); + + await client1.encryption.keyVerificationManager.cleanup(); + await client2.encryption.keyVerificationManager.cleanup(); }); - test('rejectVerification', () async { - await keyVerification.rejectVerification(); - }); - test('start', () async { - await keyVerification.start(); - }); - test('verifyActivity', () async { - final verified = await keyVerification.verifyActivity(); - expect(verified, true); - keyVerification?.dispose(); + + test('other device accepted', () async { + 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(); + var evt = getLastSentEvent(req1); + expect(req1.state, KeyVerificationState.waitingAccept); + + KeyVerification req2; + final sub = client2.onKeyVerificationRequest.stream.listen((req) { + req2 = req; + }); + await client2.encryption.keyVerificationManager.handleEventUpdate(evt); + await Future.delayed(Duration(milliseconds: 10)); + await sub.cancel(); + expect(req2 != null, true); + + await client2.encryption.keyVerificationManager + .handleEventUpdate(EventUpdate( + content: { + 'event_id': req2.transactionId, + 'type': 'm.key.verification.ready', + 'content': { + 'methods': ['m.sas.v1'], + 'from_device': 'SOMEOTHERDEVICE', + 'm.relates_to': { + 'rel_type': 'm.reference', + 'event_id': req2.transactionId, + }, + }, + 'origin_server_ts': DateTime.now().millisecondsSinceEpoch, + 'sender': client2.userID, + }, + eventType: 'm.key.verification.ready', + type: 'timeline', + roomID: req2.room.id, + )); + expect(req2.state, KeyVerificationState.error); + + await req2.cancel(); + await client1.encryption.keyVerificationManager.cleanup(); + await client2.encryption.keyVerificationManager.cleanup(); }); test('dispose client', () async { - await client.dispose(closeDatabase: true); + await client1.dispose(closeDatabase: true); + await client2.dispose(closeDatabase: true); }); }); } diff --git a/test/fake_matrix_api.dart b/test/fake_matrix_api.dart index 4c02258..e51fca5 100644 --- a/test/fake_matrix_api.dart +++ b/test/fake_matrix_api.dart @@ -78,6 +78,11 @@ class FakeMatrixApi extends MockClient { action.contains('/state/m.room.member/')) { res = {'displayname': ''}; return Response(json.encode(res), 200); + } else if (method == 'PUT' && + action.contains( + '/client/r0/rooms/%211234%3AfakeServer.notExisting/send/')) { + res = {'event_id': '\$event${FakeMatrixApi.eventCounter++}'}; + return Response(json.encode(res), 200); } else { res = { 'errcode': 'M_UNRECOGNIZED', @@ -2008,6 +2013,10 @@ class FakeMatrixApi extends MockClient { (var req) => {}, '/client/r0/user/%40alice%3Aexample.com/rooms/1234/account_data/test.account.data': (var req) => {}, + '/client/r0/user/%40test%3AfakeServer.notExisting/account_data/m.direct': + (var req) => {}, + '/client/r0/user/%40othertest%3AfakeServer.notExisting/account_data/m.direct': + (var req) => {}, '/client/r0/profile/%40alice%3Aexample.com/displayname': (var reqI) => {}, '/client/r0/profile/%40alice%3Aexample.com/avatar_url': (var reqI) => {}, '/client/r0/profile/%40test%3AfakeServer.notExisting/avatar_url':