/* * 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:convert'; import 'package:matrix/matrix.dart'; import 'package:matrix/encryption.dart'; import 'package:test/test.dart'; import 'package:olm/olm.dart' as olm; import '../fake_client.dart'; import '../fake_database.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(); await handle.unlock(recoveryKey: ssssKey); 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, }, type: EventUpdateType.timeline, roomID: req.room!.id, ); } void main() { /// All Tests related to the ChatTime group('Key Verification', () { Logs().level = Level.error; var olmEnabled = true; // key @othertest:fakeServer.notExisting const otherPickledOlmAccount = 'VWhVApbkcilKAEGppsPDf9nNVjaK8/IxT3asSR0sYg0S5KgbfE8vXEPwoiKBX2cEvwX3OessOBOkk+ZE7TTbjlrh/KEd31p8Wo+47qj0AP+Ky+pabnhi+/rTBvZy+gfzTqUfCxZrkzfXI9Op4JnP6gYmy7dVX2lMYIIs9WCO1jcmIXiXum5jnfXu1WLfc7PZtO2hH+k9CDKosOFaXRBmsu8k/BGXPSoWqUpvu6WpEG9t5STk4FeAzA'; late Client client1; late Client client2; test('setupClient', () async { 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'); if (!olmEnabled) return; client1 = await getClient(); client2 = Client( 'othertestclient', httpClient: FakeMatrixApi(), databaseBuilder: getDatabase, ); await client2.checkHomeserver('https://fakeserver.notexisting', checkWellKnown: false); await client2.init( newToken: 'abc', newUserID: '@othertest:fakeServer.notExisting', newHomeserver: client2.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('Run emoji / number verification', () async { if (!olmEnabled) return; // 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, ); var evt = getLastSentEvent(req1); expect(req1.state, KeyVerificationState.waitingAccept); late 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( client2.encryption!.keyVerificationManager .getRequest(req2.transactionId!), req2); // 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 { if (!olmEnabled) return; client1.userDeviceKeys[client1.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 Future.delayed(Duration(seconds: 1)); expect(req1.state, KeyVerificationState.waitingAccept); await req1.cancel(); await client1.encryption!.keyVerificationManager.cleanup(); }); test('ask SSSS end', () async { if (!olmEnabled) return; 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(newDirectChatEnableEncryption: false); var evt = getLastSentEvent(req1); expect(req1.state, KeyVerificationState.waitingAccept); late 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(); // 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.encryption!.ssss.clearCache(); // 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: ssssKey); 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.encryption!.ssss.clearCache(); 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 { if (!olmEnabled) return; 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); var evt = getLastSentEvent(req1); expect(req1.state, KeyVerificationState.waitingAccept); late 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('reject sas', () async { if (!olmEnabled) return; 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); var evt = getLastSentEvent(req1); expect(req1.state, KeyVerificationState.waitingAccept); late 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(); // 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('other device accepted', () async { if (!olmEnabled) return; 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); final evt = getLastSentEvent(req1); expect(req1.state, KeyVerificationState.waitingAccept); late 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(); 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, }, type: EventUpdateType.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 { if (!olmEnabled) return; await client1.dispose(closeDatabase: true); await client2.dispose(closeDatabase: true); }); }); }