From f3f3231df6efb53c382f354e9ae4ae05bf3e0a07 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Thu, 4 Jun 2020 17:51:49 +0200 Subject: [PATCH] add some encrypt / decrypt tests --- lib/encryption/encryption.dart | 4 + lib/encryption/olm_manager.dart | 56 +++++--- lib/src/client.dart | 3 + test/client_test.dart | 3 +- .../encrypt_decrypt_room_message_test.dart | 96 +++++++++++++ .../encrypt_decrypt_to_device_test.dart | 127 ++++++++++++++++++ test/encryption/key_request_test.dart | 2 +- test/encryption/key_verification_test.dart | 2 +- test/fake_matrix_api.dart | 52 ++++++- 9 files changed, 317 insertions(+), 28 deletions(-) create mode 100644 test/encryption/encrypt_decrypt_room_message_test.dart create mode 100644 test/encryption/encrypt_decrypt_to_device_test.dart diff --git a/lib/encryption/encryption.dart b/lib/encryption/encryption.dart index add3ff2d..072d0c8f 100644 --- a/lib/encryption/encryption.dart +++ b/lib/encryption/encryption.dart @@ -68,9 +68,13 @@ class Encryption { Future handleToDeviceEvent(ToDeviceEvent event) async { if (['m.room_key', 'm.room_key_request', 'm.forwarded_room_key'] .contains(event.type)) { + // a new room key or thelike. We need to handle this asap, before other + // events in /sync are handled await keyManager.handleToDeviceEvent(event); } if (event.type.startsWith('m.key.verification.')) { + // some key verification event. No need to handle it now, we can easily + // do this in the background unawaited(keyVerificationManager.handleToDeviceEvent(event)); } } diff --git a/lib/encryption/olm_manager.dart b/lib/encryption/olm_manager.dart index 4b5fad27..648f2b72 100644 --- a/lib/encryption/olm_manager.dart +++ b/lib/encryption/olm_manager.dart @@ -123,13 +123,15 @@ class OlmManager { } /// Generates new one time keys, signs everything and upload it to the server. - Future uploadKeys({bool uploadDeviceKeys = false}) async { + Future uploadKeys({bool uploadDeviceKeys = false, int oldKeyCount = 0}) async { if (!enabled) { return true; } // generate one-time keys - final oneTimeKeysCount = _olmAccount.max_number_of_one_time_keys(); + // we generate 2/3rds of max, so that other keys people may still have can + // still be used + final oneTimeKeysCount = (_olmAccount.max_number_of_one_time_keys() * 2 / 3).floor() - oldKeyCount; _olmAccount.generate_one_time_keys(oneTimeKeysCount); final Map oneTimeKeys = json.decode(_olmAccount.one_time_keys()); @@ -194,7 +196,7 @@ class OlmManager { if (countJson.containsKey('signed_curve25519') && countJson['signed_curve25519'] < (_olmAccount.max_number_of_one_time_keys() / 2)) { - uploadKeys(); + uploadKeys(oldKeyCount: countJson['signed_curve25519']); } } @@ -260,11 +262,16 @@ class OlmManager { if (plaintext == null) { var newSession = olm.Session(); - newSession.create_inbound_from(_olmAccount, senderKey, body); - _olmAccount.remove_one_time_keys(newSession); - client.database?.updateClientKeys(pickledOlmAccount, client.id); - plaintext = newSession.decrypt(type, body); - storeOlmSession(senderKey, newSession); + try { + newSession.create_inbound_from(_olmAccount, senderKey, body); + _olmAccount.remove_one_time_keys(newSession); + client.database?.updateClientKeys(pickledOlmAccount, client.id); + plaintext = newSession.decrypt(type, body); + storeOlmSession(senderKey, newSession); + } catch (_) { + newSession?.free(); + rethrow; + } } final Map plainContent = json.decode(plaintext); if (plainContent.containsKey('sender') && @@ -292,22 +299,31 @@ class OlmManager { if (event.type != EventTypes.Encrypted) { return event; } + final senderKey = event.content['sender_key']; + final loadFromDb = () async { + if (client.database == null) { + return false; + } + final sessions = await client.database.getSingleOlmSessions( + client.id, senderKey, client.userID); + if (sessions.isEmpty) { + return false; // okay, can't do anything + } + _olmSessions[senderKey] = sessions; + return true; + }; + if (!_olmSessions.containsKey(senderKey)) { + await loadFromDb(); + } event = _decryptToDeviceEvent(event); - if (event.type != EventTypes.Encrypted || client.database == null) { + if (event.type != EventTypes.Encrypted || !(await loadFromDb())) { return event; } - // load the olm session from the database and re-try to decrypt it - final sessions = await client.database.getSingleOlmSessions( - client.id, event.content['sender_key'], client.userID); - if (sessions.isEmpty) { - return event; // okay, can't do anything - } - _olmSessions[event.content['sender_key']] = sessions; + // retry to decrypt! return _decryptToDeviceEvent(event); } - Future startOutgoingOlmSessions(List deviceKeys, - {bool checkSignature = true}) async { + Future startOutgoingOlmSessions(List deviceKeys) async { var requestingKeysFrom = >{}; for (var device in deviceKeys) { if (requestingKeysFrom[device.userId] == null) { @@ -328,9 +344,7 @@ class OlmManager { final identityKey = client.userDeviceKeys[userId].deviceKeys[deviceId].curve25519Key; for (Map deviceKey in deviceKeysEntry.value.values) { - if (checkSignature && - checkJsonSignature(fingerprintKey, deviceKey, userId, deviceId) == - false) { + if (!checkJsonSignature(fingerprintKey, deviceKey, userId, deviceId)) { continue; } try { diff --git a/lib/src/client.dart b/lib/src/client.dart index 40036d6a..ef6b2a4f 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -1134,6 +1134,9 @@ class Client { for (final rawDeviceKeyListEntry in response.deviceKeys.entries) { final userId = rawDeviceKeyListEntry.key; + if (!userDeviceKeys.containsKey(userId)) { + _userDeviceKeys[userId] = DeviceKeysList(userId); + } final oldKeys = Map.from(_userDeviceKeys[userId].deviceKeys); _userDeviceKeys[userId].deviceKeys = {}; diff --git a/test/client_test.dart b/test/client_test.dart index 09bef104..eaded694 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -38,8 +38,9 @@ void main() { Future> eventUpdateListFuture; Future> toDeviceUpdateListFuture; + // key @test:fakeServer.notExisting const pickledOlmAccount = - 'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuweStA+EKZvvHZO0SnwRp0Hw7sv8UMYvXw'; + 'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtu/BjDjz0C3ioDgrrFdoSrn+GSeF5FGKsNu8OLkQ9Lq5+BrUutK5QSJI19uoZj2sj/OixvIpnun8XxYpXo7cfh9MEtKI8ob7lLM2OpZ8BogU70ORgkwthsPSOtxQGPhx8+y5Sg7B6KGlU'; const identityKey = '7rvl3jORJkBiK4XX1e5TnGnqz068XfYJ0W++Ml63rgk'; const fingerprintKey = 'gjL//fyaFHADt9KBADGag8g7F8Up78B/K1zXeiEPLJo'; diff --git a/test/encryption/encrypt_decrypt_room_message_test.dart b/test/encryption/encrypt_decrypt_room_message_test.dart new file mode 100644 index 00000000..56ace3cd --- /dev/null +++ b/test/encryption/encrypt_decrypt_room_message_test.dart @@ -0,0 +1,96 @@ +/* + * Ansible inventory script used at Famedly GmbH for managing many hosts + * 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 'package:famedlysdk/famedlysdk.dart'; +import 'package:test/test.dart'; +import 'package:olm/olm.dart' as olm; + +import '../fake_matrix_api.dart'; +import '../fake_database.dart'; + +void main() { + group('Encrypt/Decrypt room message', () { + var olmEnabled = true; + try { + olm.init(); + olm.Account(); + } catch (_) { + olmEnabled = false; + print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + } + print('[LibOlm] Enabled: $olmEnabled'); + + if (!olmEnabled) return; + + var client = Client('testclient', debug: true, httpClient: FakeMatrixApi()); + final roomId = '!726s6s6q:example.com'; + Room room; + Map payload; + final now = DateTime.now(); + + test('setupClient', () async { + client.database = getDatabase(); + await client.checkServer('https://fakeServer.notExisting'); + await client.login('test', '1234'); + room = client.getRoomById(roomId); + }); + + test('encrypt payload', () async { + payload = await client.encryption.encryptGroupMessagePayload(roomId, { + 'msgtype': 'm.text', + 'text': 'Hello foxies!', + }); + expect(payload['algorithm'], 'm.megolm.v1.aes-sha2'); + expect(payload['ciphertext'] is String, true); + expect(payload['device_id'], client.deviceID); + expect(payload['sender_key'], client.identityKey); + expect(payload['session_id'] is String, true); + }); + + test('decrypt payload', () async { + final encryptedEvent = Event( + type: EventTypes.Encrypted, + content: payload, + roomId: roomId, + room: room, + originServerTs: now, + eventId: '\$event', + ); + final decryptedEvent = await client.encryption.decryptRoomEvent(roomId, encryptedEvent); + expect(decryptedEvent.type, 'm.room.message'); + expect(decryptedEvent.content['msgtype'], 'm.text'); + expect(decryptedEvent.content['text'], 'Hello foxies!'); + }); + + test('decrypt payload nocache', () async { + client.encryption.keyManager.clearInboundGroupSessions(); + final encryptedEvent = Event( + type: EventTypes.Encrypted, + content: payload, + roomId: roomId, + room: room, + originServerTs: now, + eventId: '\$event', + ); + final decryptedEvent = await client.encryption.decryptRoomEvent(roomId, encryptedEvent); + expect(decryptedEvent.type, 'm.room.message'); + expect(decryptedEvent.content['msgtype'], 'm.text'); + expect(decryptedEvent.content['text'], 'Hello foxies!'); + }); + }); +} diff --git a/test/encryption/encrypt_decrypt_to_device_test.dart b/test/encryption/encrypt_decrypt_to_device_test.dart new file mode 100644 index 00000000..6b082b6e --- /dev/null +++ b/test/encryption/encrypt_decrypt_to_device_test.dart @@ -0,0 +1,127 @@ +/* + * Ansible inventory script used at Famedly GmbH for managing many hosts + * 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 'package:famedlysdk/famedlysdk.dart'; +import 'package:test/test.dart'; +import 'package:olm/olm.dart' as olm; + +import '../fake_matrix_api.dart'; +import '../fake_database.dart'; + +void main() { + // key @test:fakeServer.notExisting + const pickledOlmAccount = + 'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtu/BjDjz0C3ioDgrrFdoSrn+GSeF5FGKsNu8OLkQ9Lq5+BrUutK5QSJI19uoZj2sj/OixvIpnun8XxYpXo7cfh9MEtKI8ob7lLM2OpZ8BogU70ORgkwthsPSOtxQGPhx8+y5Sg7B6KGlU'; + + const otherPickledOlmAccount = 'VWhVApbkcilKAEGppsPDf9nNVjaK8/IxT3asSR0sYg0S5KgbfE8vXEPwoiKBX2cEvwX3OessOBOkk+ZE7TTbjlrh/KEd31p8Wo+47qj0AP+Ky+pabnhi+/rTBvZy+gfzTqUfCxZrkzfXI9Op4JnP6gYmy7dVX2lMYIIs9WCO1jcmIXiXum5jnfXu1WLfc7PZtO2hH+k9CDKosOFaXRBmsu8k/BGXPSoWqUpvu6WpEG9t5STk4FeAzA'; + + group('Encrypt/Decrypt to-device messages', () { + var olmEnabled = true; + try { + olm.init(); + olm.Account(); + } catch (_) { + olmEnabled = false; + print('[LibOlm] Failed to load LibOlm: ' + _.toString()); + } + print('[LibOlm] Enabled: $olmEnabled'); + + if (!olmEnabled) return; + + var client = Client('testclient', debug: true, httpClient: FakeMatrixApi()); + var otherClient = Client('othertestclient', debug: true, httpClient: FakeMatrixApi()); + final roomId = '!726s6s6q:example.com'; + DeviceKeys device; + Map payload; + + test('setupClient', () async { + client.database = getDatabase(); + otherClient.database = client.database; + await client.checkServer('https://fakeServer.notExisting'); + await otherClient.checkServer('https://fakeServer.notExisting'); + final resp = await client.api.login( + type: 'm.login.password', + user: 'test', + password: '1234', + initialDeviceDisplayName: 'Fluffy Matrix Client', + ); + client.connect( + newToken: resp.accessToken, + newUserID: resp.userId, + newHomeserver: client.api.homeserver, + newDeviceName: 'Text Matrix Client', + newDeviceID: resp.deviceId, + newOlmAccount: pickledOlmAccount, + ); + otherClient.connect( + newToken: 'abc', + newUserID: '@othertest:fakeServer.notExisting', + newHomeserver: otherClient.api.homeserver, + newDeviceName: 'Text Matrix Client', + newDeviceID: 'FOXDEVICE', + newOlmAccount: otherPickledOlmAccount, + ); + + await Future.delayed(Duration(milliseconds: 50)); + device = DeviceKeys( + userId: resp.userId, + deviceId: resp.deviceId, + algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'], + keys: { + 'curve25519:${resp.deviceId}': client.identityKey, + 'ed25519:${resp.deviceId}': client.fingerprintKey, + }, + verified: true, + blocked: false, + ); + }); + + test('encryptToDeviceMessage', () async { + payload = await otherClient.encryption.encryptToDeviceMessage([device], 'm.to_device', {'hello': 'foxies'}); + }); + + test('encryptToDeviceMessagePayload', () async { + // just a hard test if nothing errors + await otherClient.encryption.encryptToDeviceMessagePayload(device, 'm.to_device', {'hello': 'foxies'}); + }); + + test('decryptToDeviceEvent', () async { + final encryptedEvent = ToDeviceEvent( + sender: '@othertest:fakeServer.notExisting', + type: EventTypes.Encrypted, + content: payload[client.userID][client.deviceID], + ); + final decryptedEvent = await client.encryption.decryptToDeviceEvent(encryptedEvent); + expect(decryptedEvent.type, 'm.to_device'); + expect(decryptedEvent.content['hello'], 'foxies'); + }); + + test('decryptToDeviceEvent nocache', () async { + client.encryption.olmManager.olmSessions.clear(); + payload = await otherClient.encryption.encryptToDeviceMessage([device], 'm.to_device', {'hello': 'superfoxies'}); + final encryptedEvent = ToDeviceEvent( + sender: '@othertest:fakeServer.notExisting', + type: EventTypes.Encrypted, + content: payload[client.userID][client.deviceID], + ); + final decryptedEvent = await client.encryption.decryptToDeviceEvent(encryptedEvent); + expect(decryptedEvent.type, 'm.to_device'); + expect(decryptedEvent.content['hello'], 'superfoxies'); + }); + }); +} diff --git a/test/encryption/key_request_test.dart b/test/encryption/key_request_test.dart index 243f7765..982a4f0b 100644 --- a/test/encryption/key_request_test.dart +++ b/test/encryption/key_request_test.dart @@ -1,6 +1,6 @@ /* * Ansible inventory script used at Famedly GmbH for managing many hosts - * Copyright (C) 2019, 2020 Famedly GmbH + * 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 diff --git a/test/encryption/key_verification_test.dart b/test/encryption/key_verification_test.dart index 039a38f4..eac31a0b 100644 --- a/test/encryption/key_verification_test.dart +++ b/test/encryption/key_verification_test.dart @@ -1,6 +1,6 @@ /* * Ansible inventory script used at Famedly GmbH for managing many hosts - * Copyright (C) 2019, 2020 Famedly GmbH + * 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 diff --git a/test/fake_matrix_api.dart b/test/fake_matrix_api.dart index 410b7369..e2f85246 100644 --- a/test/fake_matrix_api.dart +++ b/test/fake_matrix_api.dart @@ -1569,7 +1569,19 @@ class FakeMatrixApi extends MockClient { } } } - } + }, + '@test:fakeServer.notExisting': { + 'GHTYAJCE': { + 'signed_curve25519:AAAAAQ': { + 'key': 'qc72ve94cA28iuE0fXa98QO3uls39DHWdQlYyvvhGh0', + 'signatures': { + '@test:fakeServer.notExisting': { + 'ed25519:GHTYAJCE': 'dFwffr5kTKefO7sjnWLMhTzw7oV31nkPIDRxFy5OQT2OP5++Ao0KRbaBZ6qfuT7lW1owKK0Xk3s7QTBvc/eNDA', + }, + }, + }, + }, + }, } }, '/client/r0/rooms/!localpart%3Aexample.com/invite': (var req) => {}, @@ -1586,7 +1598,7 @@ class FakeMatrixApi extends MockClient { '/client/r0/keys/upload': (var req) => { 'one_time_key_counts': { 'curve25519': 10, - 'signed_curve25519': 100, + 'signed_curve25519': 66, } }, '/client/r0/keys/query': (var req) => { @@ -1627,8 +1639,40 @@ class FakeMatrixApi extends MockClient { }, 'signatures': {}, }, - } - } + }, + '@test:fakeServer.notExisting': { + 'GHTYAJCE': { + 'user_id': '@test:fakeServer.notExisting', + 'device_id': 'GHTYAJCE', + 'algorithms': [ + 'm.olm.v1.curve25519-aes-sha2', + 'm.megolm.v1.aes-sha2' + ], + 'keys': { + 'curve25519:GHTYAJCE': + '7rvl3jORJkBiK4XX1e5TnGnqz068XfYJ0W++Ml63rgk', + 'ed25519:GHTYAJCE': + 'gjL//fyaFHADt9KBADGag8g7F8Up78B/K1zXeiEPLJo' + }, + 'signatures': {}, + }, + }, + '@othertest:fakeServer.notExisting': { + 'FOXDEVICE': { + 'user_id': '@othertest:fakeServer.notExisting', + 'device_id': 'FOXDEVICE', + 'algorithms': [ + 'm.olm.v1.curve25519-aes-sha2', + 'm.megolm.v1.aes-sha2' + ], + 'keys': { + 'curve25519:FOXDEVICE': 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg', + 'ed25519:FOXDEVICE': 'R5/p04tticvdlNIxiiBIP0j9OQWv8ep6eEU6/lWKDxw', + }, + 'signatures': {}, + }, + }, + }, }, '/client/r0/register': (var req) => { 'user_id': '@testuser:example.com',