From fcde6a2459fc26d12097e286189f04c3b35f402b Mon Sep 17 00:00:00 2001 From: Sorunome Date: Thu, 4 Jun 2020 13:39:51 +0200 Subject: [PATCH 01/18] split encryption stuff to other library --- lib/encryption.dart | 23 + lib/encryption/encryption.dart | 279 ++++++++++ lib/encryption/key_manager.dart | 512 ++++++++++++++++++ lib/encryption/key_verification_manager.dart | 83 +++ lib/encryption/olm_manager.dart | 418 ++++++++++++++ .../utils/key_verification.dart | 49 +- .../utils/outbound_group_session.dart | 58 ++ lib/encryption/utils/session_key.dart | 76 +++ lib/famedlysdk.dart | 2 - lib/src/client.dart | 512 ++---------------- lib/src/database/database.dart | 16 + lib/src/database/database.g.dart | 13 + lib/src/database/database.moor | 1 + lib/src/event.dart | 31 +- lib/src/key_manager.dart | 214 -------- lib/src/room.dart | 472 ++-------------- lib/src/timeline.dart | 7 +- lib/src/utils/device_keys_list.dart | 14 +- lib/src/utils/event_update.dart | 9 +- lib/src/utils/session_key.dart | 59 -- test/client_test.dart | 152 +----- test/encryption/key_request_test.dart | 342 ++++++++++++ .../key_verification_test.dart | 28 +- test/event_test.dart | 1 + test/room_key_request_test.dart | 367 ------------- test/room_test.dart | 56 +- test/session_key_test.dart | 56 -- test_driver/famedlysdk_test.dart | 193 +++++-- 28 files changed, 2113 insertions(+), 1930 deletions(-) create mode 100644 lib/encryption.dart create mode 100644 lib/encryption/encryption.dart create mode 100644 lib/encryption/key_manager.dart create mode 100644 lib/encryption/key_verification_manager.dart create mode 100644 lib/encryption/olm_manager.dart rename lib/{src => encryption}/utils/key_verification.dart (95%) create mode 100644 lib/encryption/utils/outbound_group_session.dart create mode 100644 lib/encryption/utils/session_key.dart delete mode 100644 lib/src/key_manager.dart delete mode 100644 lib/src/utils/session_key.dart create mode 100644 test/encryption/key_request_test.dart rename test/{ => encryption}/key_verification_test.dart (83%) delete mode 100644 test/room_key_request_test.dart delete mode 100644 test/session_key_test.dart diff --git a/lib/encryption.dart b/lib/encryption.dart new file mode 100644 index 0000000..ef4f347 --- /dev/null +++ b/lib/encryption.dart @@ -0,0 +1,23 @@ +/* + * 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 . + */ + +library encryption; + +export './encryption/encryption.dart'; +export './encryption/key_manager.dart'; +export './encryption/utils/key_verification.dart'; diff --git a/lib/encryption/encryption.dart b/lib/encryption/encryption.dart new file mode 100644 index 0000000..add3ff2 --- /dev/null +++ b/lib/encryption/encryption.dart @@ -0,0 +1,279 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2019, 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:famedlysdk/famedlysdk.dart'; +import 'package:famedlysdk/matrix_api.dart'; +import 'package:pedantic/pedantic.dart'; +import 'key_manager.dart'; +import 'olm_manager.dart'; +import 'key_verification_manager.dart'; + +class Encryption { + final Client client; + final bool debug; + final bool enableE2eeRecovery; + + bool get enabled => olmManager.enabled; + + /// Returns the base64 encoded keys to store them in a store. + /// This String should **never** leave the device! + String get pickledOlmAccount => olmManager.pickledOlmAccount; + + String get fingerprintKey => olmManager.fingerprintKey; + String get identityKey => olmManager.identityKey; + + KeyManager keyManager; + OlmManager olmManager; + KeyVerificationManager keyVerificationManager; + + Encryption({ + this.client, + this.debug, + this.enableE2eeRecovery, + }) { + keyManager = KeyManager(this); + olmManager = OlmManager(this); + keyVerificationManager = KeyVerificationManager(this); + } + + Future init(String olmAccount) async { + await olmManager.init(olmAccount); + } + + void handleDeviceOneTimeKeysCount(Map countJson) { + olmManager.handleDeviceOneTimeKeysCount(countJson); + } + + void onSync() { + keyVerificationManager.cleanup(); + } + + Future handleToDeviceEvent(ToDeviceEvent event) async { + if (['m.room_key', 'm.room_key_request', 'm.forwarded_room_key'] + .contains(event.type)) { + await keyManager.handleToDeviceEvent(event); + } + if (event.type.startsWith('m.key.verification.')) { + unawaited(keyVerificationManager.handleToDeviceEvent(event)); + } + } + + Future decryptToDeviceEvent(ToDeviceEvent event) async { + return await olmManager.decryptToDeviceEvent(event); + } + + Event decryptRoomEventSync(String roomId, Event event) { + if (event.type != EventTypes.Encrypted || + event.content['ciphertext'] == null) return event; + Map decryptedPayload; + try { + if (event.content['algorithm'] != 'm.megolm.v1.aes-sha2') { + throw (DecryptError.UNKNOWN_ALGORITHM); + } + final String sessionId = event.content['session_id']; + final String senderKey = event.content['sender_key']; + final inboundGroupSession = + keyManager.getInboundGroupSession(roomId, sessionId, senderKey); + if (inboundGroupSession == null) { + throw (DecryptError.UNKNOWN_SESSION); + } + final decryptResult = inboundGroupSession.inboundGroupSession + .decrypt(event.content['ciphertext']); + final messageIndexKey = event.eventId + + event.originServerTs.millisecondsSinceEpoch.toString(); + var haveIndex = inboundGroupSession.indexes.containsKey(messageIndexKey); + if (haveIndex && + inboundGroupSession.indexes[messageIndexKey] != + decryptResult.message_index) { + // TODO: maybe clear outbound session, if it is ours + throw (DecryptError.CHANNEL_CORRUPTED); + } + inboundGroupSession.indexes[messageIndexKey] = + decryptResult.message_index; + if (!haveIndex) { + // now we persist the udpated indexes into the database. + // the entry should always exist. In the case it doesn't, the following + // line *could* throw an error. As that is a future, though, and we call + // it un-awaited here, nothing happens, which is exactly the result we want + client.database?.updateInboundGroupSessionIndexes( + json.encode(inboundGroupSession.indexes), + client.id, + roomId, + sessionId); + } + decryptedPayload = json.decode(decryptResult.plaintext); + } catch (exception) { + // alright, if this was actually by our own outbound group session, we might as well clear it + if (client.enableE2eeRecovery && + (keyManager + .getOutboundGroupSession(roomId) + ?.outboundGroupSession + ?.session_id() ?? + '') == + event.content['session_id']) { + keyManager.clearOutboundGroupSession(roomId, wipe: true); + } + if (exception.toString() == DecryptError.UNKNOWN_SESSION) { + decryptedPayload = { + 'content': event.content, + 'type': EventTypes.Encrypted, + }; + decryptedPayload['content']['body'] = exception.toString(); + decryptedPayload['content']['msgtype'] = 'm.bad.encrypted'; + } else { + decryptedPayload = { + 'content': { + 'msgtype': 'm.bad.encrypted', + 'body': exception.toString(), + }, + 'type': EventTypes.Encrypted, + }; + } + } + if (event.content['m.relates_to'] != null) { + decryptedPayload['content']['m.relates_to'] = + event.content['m.relates_to']; + } + return Event( + content: decryptedPayload['content'], + type: decryptedPayload['type'], + senderId: event.senderId, + eventId: event.eventId, + roomId: event.roomId, + room: event.room, + originServerTs: event.originServerTs, + unsigned: event.unsigned, + stateKey: event.stateKey, + prevContent: event.prevContent, + status: event.status, + sortOrder: event.sortOrder, + ); + } + + Future decryptRoomEvent(String roomId, Event event, + {bool store = false, String updateType = 'timeline'}) async { + final doStore = () async { + await client.database?.storeEventUpdate( + client.id, + EventUpdate( + eventType: event.type, + content: event.toJson(), + roomID: event.roomId, + type: updateType, + sortOrder: event.sortOrder, + ), + ); + if (updateType != 'history') { + event.room?.setState(event); + } + }; + if (event.type != EventTypes.Encrypted) { + return event; + } + event = decryptRoomEventSync(roomId, event); + if (event.type != EventTypes.Encrypted) { + if (store) { + await doStore(); + } + return event; + } + if (client.database == null) { + return event; + } + await keyManager.loadInboundGroupSession( + roomId, event.content['session_id'], event.content['sender_key']); + event = decryptRoomEventSync(roomId, event); + if (event.type != EventTypes.Encrypted && store) { + await doStore(); + } + return event; + } + + /// Encrypts the given json payload and creates a send-ready m.room.encrypted + /// payload. This will create a new outgoingGroupSession if necessary. + Future> encryptGroupMessagePayload( + String roomId, Map payload, + {String type = EventTypes.Message}) async { + final room = client.getRoomById(roomId); + if (room == null || !room.encrypted || !enabled) { + return payload; + } + if (room.encryptionAlgorithm != 'm.megolm.v1.aes-sha2') { + throw ('Unknown encryption algorithm'); + } + if (keyManager.getOutboundGroupSession(roomId) == null) { + await keyManager.loadOutboundGroupSession(roomId); + } + await keyManager.clearOutboundGroupSession(roomId); + if (keyManager.getOutboundGroupSession(roomId) == null) { + await keyManager.createOutboundGroupSession(roomId); + } + final sess = keyManager.getOutboundGroupSession(roomId); + if (sess == null) { + throw ('Unable to create new outbound group session'); + } + final Map mRelatesTo = payload.remove('m.relates_to'); + final payloadContent = { + 'content': payload, + 'type': type, + 'room_id': roomId, + }; + var encryptedPayload = { + 'algorithm': 'm.megolm.v1.aes-sha2', + 'ciphertext': + sess.outboundGroupSession.encrypt(json.encode(payloadContent)), + 'device_id': client.deviceID, + 'sender_key': identityKey, + 'session_id': sess.outboundGroupSession.session_id(), + if (mRelatesTo != null) 'm.relates_to': mRelatesTo, + }; + sess.sentMessages++; + await keyManager.storeOutboundGroupSession(roomId, sess); + return encryptedPayload; + } + + Future> encryptToDeviceMessagePayload( + DeviceKeys device, String type, Map payload) async { + return await olmManager.encryptToDeviceMessagePayload( + device, type, payload); + } + + Future> encryptToDeviceMessage( + List deviceKeys, + String type, + Map payload) async { + return await olmManager.encryptToDeviceMessage(deviceKeys, type, payload); + } + + void dispose() { + keyManager.dispose(); + olmManager.dispose(); + keyVerificationManager.dispose(); + } +} + +abstract class DecryptError { + static const String NOT_ENABLED = 'Encryption is not enabled in your client.'; + static const String UNKNOWN_ALGORITHM = 'Unknown encryption algorithm.'; + static const String UNKNOWN_SESSION = + 'The sender has not sent us the session key.'; + static const String CHANNEL_CORRUPTED = + 'The secure channel with the sender was corrupted.'; +} diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart new file mode 100644 index 0000000..4234b67 --- /dev/null +++ b/lib/encryption/key_manager.dart @@ -0,0 +1,512 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2019, 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:pedantic/pedantic.dart'; +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:famedlysdk/matrix_api.dart'; +import 'package:olm/olm.dart' as olm; + +import './encryption.dart'; +import './utils/session_key.dart'; +import './utils/outbound_group_session.dart'; + +class KeyManager { + final Encryption encryption; + Client get client => encryption.client; + final outgoingShareRequests = {}; + final incomingShareRequests = {}; + final _inboundGroupSessions = >{}; + final _outboundGroupSessions = {}; + final Set _loadedOutboundGroupSessions = {}; + final Set _requestedSessionIds = {}; + + KeyManager(this.encryption); + + /// clear all cached inbound group sessions. useful for testing + void clearInboundGroupSessions() { + _inboundGroupSessions.clear(); + } + + void setInboundGroupSession(String roomId, String sessionId, String senderKey, + Map content, + {bool forwarded = false}) { + final oldSession = + getInboundGroupSession(roomId, sessionId, senderKey, otherRooms: false); + if (oldSession != null) { + return; + } + if (content['algorithm'] != 'm.megolm.v1.aes-sha2') { + return; + } + olm.InboundGroupSession inboundGroupSession; + try { + inboundGroupSession = olm.InboundGroupSession(); + if (forwarded) { + inboundGroupSession.import_session(content['session_key']); + } else { + inboundGroupSession.create(content['session_key']); + } + } catch (e) { + inboundGroupSession.free(); + print( + '[LibOlm] Could not create new InboundGroupSession: ' + e.toString()); + return; + } + if (!_inboundGroupSessions.containsKey(roomId)) { + _inboundGroupSessions[roomId] = {}; + } + _inboundGroupSessions[roomId][sessionId] = SessionKey( + content: content, + inboundGroupSession: inboundGroupSession, + indexes: {}, + key: client.userID, + ); + client.database?.storeInboundGroupSession( + client.id, + roomId, + sessionId, + inboundGroupSession.pickle(client.userID), + json.encode(content), + json.encode({}), + ); + // TODO: somehow try to decrypt last message again + final room = client.getRoomById(roomId); + if (room != null) { + room.onSessionKeyReceived.add(sessionId); + } + } + + SessionKey getInboundGroupSession( + String roomId, String sessionId, String senderKey, + {bool otherRooms = true}) { + if (_inboundGroupSessions.containsKey(roomId) && + _inboundGroupSessions[roomId].containsKey(sessionId)) { + return _inboundGroupSessions[roomId][sessionId]; + } + if (!otherRooms) { + return null; + } + // search if this session id is *somehow* found in another room + for (final val in _inboundGroupSessions.values) { + if (val.containsKey(sessionId)) { + return val[sessionId]; + } + } + return null; + } + + /// Loads an inbound group session + Future loadInboundGroupSession( + String roomId, String sessionId, String senderKey) async { + if (roomId == null || sessionId == null || senderKey == null) { + return null; + } + if (_inboundGroupSessions.containsKey(roomId) && + _inboundGroupSessions[roomId].containsKey(sessionId)) { + return _inboundGroupSessions[roomId][sessionId]; // nothing to do + } + final session = await client.database + ?.getDbInboundGroupSession(client.id, roomId, sessionId); + if (session == null) { + final room = client.getRoomById(roomId); + final requestIdent = '$roomId|$sessionId|$senderKey'; + if (client.enableE2eeRecovery && + room != null && + !_requestedSessionIds.contains(requestIdent)) { + // do e2ee recovery + _requestedSessionIds.add(requestIdent); + unawaited(request(room, sessionId, senderKey)); + } + return null; + } + if (!_inboundGroupSessions.containsKey(roomId)) { + _inboundGroupSessions[roomId] = {}; + } + final sess = SessionKey.fromDb(session, client.userID); + if (!sess.isValid) { + return null; + } + _inboundGroupSessions[roomId][sessionId] = sess; + return sess; + } + + /// Clears the existing outboundGroupSession but first checks if the participating + /// devices have been changed. Returns false if the session has not been cleared because + /// it wasn't necessary. + Future clearOutboundGroupSession(String roomId, + {bool wipe = false}) async { + final room = client.getRoomById(roomId); + final sess = getOutboundGroupSession(roomId); + if (room == null || sess == null) { + return true; + } + if (!wipe) { + // first check if the devices in the room changed + final deviceKeys = await room.getUserDeviceKeys(); + deviceKeys.removeWhere((k) => k.blocked); + final deviceKeyIds = deviceKeys.map((k) => k.deviceId).toList(); + deviceKeyIds.sort(); + if (deviceKeyIds.toString() != sess.devices.toString()) { + wipe = true; + } + // next check if it needs to be rotated + final encryptionContent = room.getState(EventTypes.Encryption)?.content; + final maxMessages = encryptionContent != null && + encryptionContent['rotation_period_msgs'] is int + ? encryptionContent['rotation_period_msgs'] + : 100; + final maxAge = encryptionContent != null && + encryptionContent['rotation_period_ms'] is int + ? encryptionContent['rotation_period_ms'] + : 604800000; // default of one week + if (sess.sentMessages >= maxMessages || + sess.creationTime + .add(Duration(milliseconds: maxAge)) + .isBefore(DateTime.now())) { + wipe = true; + } + if (!wipe) { + return false; + } + } + sess.dispose(); + _outboundGroupSessions.remove(roomId); + await client.database?.removeOutboundGroupSession(client.id, roomId); + return true; + } + + Future storeOutboundGroupSession( + String roomId, OutboundGroupSession sess) async { + if (sess == null) { + return; + } + await client.database?.storeOutboundGroupSession( + client.id, + roomId, + sess.outboundGroupSession.pickle(client.userID), + json.encode(sess.devices), + sess.creationTime, + sess.sentMessages); + } + + Future createOutboundGroupSession(String roomId) async { + await clearOutboundGroupSession(roomId, wipe: true); + final room = client.getRoomById(roomId); + if (room == null) { + return null; + } + final deviceKeys = await room.getUserDeviceKeys(); + deviceKeys.removeWhere((k) => k.blocked); + final deviceKeyIds = deviceKeys.map((k) => k.deviceId).toList(); + deviceKeyIds.sort(); + final outboundGroupSession = olm.OutboundGroupSession(); + try { + outboundGroupSession.create(); + } catch (e) { + outboundGroupSession.free(); + print('[LibOlm] Unable to create new outboundGroupSession: ' + + e.toString()); + return null; + } + final rawSession = { + 'algorithm': 'm.megolm.v1.aes-sha2', + 'room_id': room.id, + 'session_id': outboundGroupSession.session_id(), + 'session_key': outboundGroupSession.session_key(), + }; + setInboundGroupSession( + roomId, rawSession['session_id'], encryption.identityKey, rawSession); + final sess = OutboundGroupSession( + devices: deviceKeyIds, + creationTime: DateTime.now(), + outboundGroupSession: outboundGroupSession, + sentMessages: 0, + key: client.userID, + ); + try { + await client.sendToDevice(deviceKeys, 'm.room_key', rawSession); + await storeOutboundGroupSession(roomId, sess); + _outboundGroupSessions[roomId] = sess; + } catch (e, s) { + print( + '[LibOlm] Unable to send the session key to the participating devices: ' + + e.toString()); + print(s); + sess.dispose(); + return null; + } + return sess; + } + + OutboundGroupSession getOutboundGroupSession(String roomId) { + return _outboundGroupSessions[roomId]; + } + + Future loadOutboundGroupSession(String roomId) async { + if (_loadedOutboundGroupSessions.contains(roomId) || + _outboundGroupSessions.containsKey(roomId) || + client.database == null) { + return; // nothing to do + } + _loadedOutboundGroupSessions.add(roomId); + final session = + await client.database.getDbOutboundGroupSession(client.id, roomId); + if (session == null) { + return; + } + final sess = OutboundGroupSession.fromDb(session, client.userID); + if (!sess.isValid) { + return; + } + _outboundGroupSessions[roomId] = sess; + } + + /// Request a certain key from another device + Future request(Room room, String sessionId, String senderKey) async { + // while we just send the to-device event to '*', we still need to save the + // devices themself to know where to send the cancel to after receiving a reply + final devices = await room.getUserDeviceKeys(); + final requestId = client.generateUniqueTransactionId(); + final request = KeyManagerKeyShareRequest( + requestId: requestId, + devices: devices, + room: room, + sessionId: sessionId, + senderKey: senderKey, + ); + await client.sendToDevice( + [], + 'm.room_key_request', + { + 'action': 'request', + 'body': { + 'algorithm': 'm.megolm.v1.aes-sha2', + 'room_id': room.id, + 'sender_key': senderKey, + 'session_id': sessionId, + }, + 'request_id': requestId, + 'requesting_device_id': client.deviceID, + }, + encrypted: false, + toUsers: await room.requestParticipants()); + outgoingShareRequests[request.requestId] = request; + } + + /// Handle an incoming to_device event that is related to key sharing + Future handleToDeviceEvent(ToDeviceEvent event) async { + if (event.type == 'm.room_key_request') { + if (!event.content.containsKey('request_id')) { + return; // invalid event + } + if (event.content['action'] == 'request') { + // we are *receiving* a request + if (!event.content.containsKey('body')) { + return; // no body + } + if (!client.userDeviceKeys.containsKey(event.sender) || + !client.userDeviceKeys[event.sender].deviceKeys + .containsKey(event.content['requesting_device_id'])) { + return; // device not found + } + final device = client.userDeviceKeys[event.sender] + .deviceKeys[event.content['requesting_device_id']]; + if (device.userId == client.userID && + device.deviceId == client.deviceID) { + return; // ignore requests by ourself + } + final room = client.getRoomById(event.content['body']['room_id']); + if (room == null) { + return; // unknown room + } + final sessionId = event.content['body']['session_id']; + final senderKey = event.content['body']['sender_key']; + // okay, let's see if we have this session at all + if ((await loadInboundGroupSession(room.id, sessionId, senderKey)) == + null) { + return; // we don't have this session anyways + } + final request = KeyManagerKeyShareRequest( + requestId: event.content['request_id'], + devices: [device], + room: room, + sessionId: sessionId, + senderKey: senderKey, + ); + if (incomingShareRequests.containsKey(request.requestId)) { + return; // we don't want to process one and the same request multiple times + } + incomingShareRequests[request.requestId] = request; + final roomKeyRequest = + RoomKeyRequest.fromToDeviceEvent(event, this, request); + if (device.userId == client.userID && + device.verified && + !device.blocked) { + // alright, we can forward the key + await roomKeyRequest.forwardKey(); + } else { + client.onRoomKeyRequest + .add(roomKeyRequest); // let the client handle this + } + } else if (event.content['action'] == 'request_cancellation') { + // we got told to cancel an incoming request + if (!incomingShareRequests.containsKey(event.content['request_id'])) { + return; // we don't know this request anyways + } + // alright, let's just cancel this request + final request = incomingShareRequests[event.content['request_id']]; + request.canceled = true; + incomingShareRequests.remove(request.requestId); + } + } else if (event.type == 'm.forwarded_room_key') { + // we *received* an incoming key request + if (event.encryptedContent == null) { + return; // event wasn't encrypted, this is a security risk + } + final request = outgoingShareRequests.values.firstWhere( + (r) => + r.room.id == event.content['room_id'] && + r.sessionId == event.content['session_id'] && + r.senderKey == event.content['sender_key'], + orElse: () => null); + if (request == null || request.canceled) { + return; // no associated request found or it got canceled + } + final device = request.devices.firstWhere( + (d) => + d.userId == event.sender && + d.curve25519Key == event.encryptedContent['sender_key'], + orElse: () => null); + if (device == null) { + return; // someone we didn't send our request to replied....better ignore this + } + // TODO: verify that the keys work to decrypt a message + // alright, all checks out, let's go ahead and store this session + setInboundGroupSession( + request.room.id, request.sessionId, request.senderKey, event.content, + forwarded: true); + request.devices.removeWhere( + (k) => k.userId == device.userId && k.deviceId == device.deviceId); + outgoingShareRequests.remove(request.requestId); + // send cancel to all other devices + if (request.devices.isEmpty) { + return; // no need to send any cancellation + } + await client.sendToDevice( + request.devices, + 'm.room_key_request', + { + 'action': 'request_cancellation', + 'request_id': request.requestId, + 'requesting_device_id': client.deviceID, + }, + encrypted: false); + } else if (event.type == 'm.room_key') { + //if (event.encryptedContent == null) { + // return; // the event wasn't encrypted, this is a security risk; + //} + final String roomId = event.content['room_id']; + final String sessionId = event.content['session_id']; + if (client.userDeviceKeys.containsKey(event.sender) && + client.userDeviceKeys[event.sender].deviceKeys + .containsKey(event.content['requesting_device_id'])) { + event.content['sender_claimed_ed25519_key'] = client + .userDeviceKeys[event.sender] + .deviceKeys[event.content['requesting_device_id']] + .ed25519Key; + } + // event.encryptedContent['sender_key'] + setInboundGroupSession(roomId, sessionId, '', event.content, + forwarded: false); + } + } + + void dispose() { + for (final sess in _outboundGroupSessions.values) { + sess.dispose(); + } + for (final entries in _inboundGroupSessions.values) { + for (final sess in entries.values) { + sess.dispose(); + } + } + } +} + +class KeyManagerKeyShareRequest { + final String requestId; + final List devices; + final Room room; + final String sessionId; + final String senderKey; + bool canceled; + + KeyManagerKeyShareRequest( + {this.requestId, + this.devices, + this.room, + this.sessionId, + this.senderKey, + this.canceled = false}); +} + +class RoomKeyRequest extends ToDeviceEvent { + KeyManager keyManager; + KeyManagerKeyShareRequest request; + RoomKeyRequest.fromToDeviceEvent(ToDeviceEvent toDeviceEvent, + KeyManager keyManager, KeyManagerKeyShareRequest request) { + this.keyManager = keyManager; + this.request = request; + sender = toDeviceEvent.sender; + content = toDeviceEvent.content; + type = toDeviceEvent.type; + } + + Room get room => request.room; + + DeviceKeys get requestingDevice => request.devices.first; + + Future forwardKey() async { + if (request.canceled) { + keyManager.incomingShareRequests.remove(request.requestId); + return; // request is canceled, don't send anything + } + var room = this.room; + final session = await keyManager.loadInboundGroupSession( + room.id, request.sessionId, request.senderKey); + var forwardedKeys = [keyManager.encryption.identityKey]; + for (final key in session.forwardingCurve25519KeyChain) { + forwardedKeys.add(key); + } + await requestingDevice.setVerified(true, keyManager.client); + var message = session.content; + message['forwarding_curve25519_key_chain'] = forwardedKeys; + + message['session_key'] = session.inboundGroupSession + .export_session(session.inboundGroupSession.first_known_index()); + // send the actual reply of the key back to the requester + await keyManager.client.sendToDevice( + [requestingDevice], + 'm.forwarded_room_key', + message, + ); + keyManager.incomingShareRequests.remove(request.requestId); + } +} diff --git a/lib/encryption/key_verification_manager.dart b/lib/encryption/key_verification_manager.dart new file mode 100644 index 0000000..f8720f4 --- /dev/null +++ b/lib/encryption/key_verification_manager.dart @@ -0,0 +1,83 @@ +/* + * 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 'package:famedlysdk/famedlysdk.dart'; +import './encryption.dart'; +import './utils/key_verification.dart'; + +class KeyVerificationManager { + final Encryption encryption; + Client get client => encryption.client; + + KeyVerificationManager(this.encryption); + + final Map _requests = {}; + + Future cleanup() async { + for (final entry in _requests.entries) { + var dispose = entry.value.canceled || + entry.value.state == KeyVerificationState.done || + entry.value.state == KeyVerificationState.error; + if (!dispose) { + dispose = !(await entry.value.verifyActivity()); + } + if (dispose) { + entry.value.dispose(); + _requests.remove(entry.key); + } + } + } + + void addRequest(KeyVerification request) { + if (request.transactionId == null) { + return; + } + _requests[request.transactionId] = request; + } + + Future handleToDeviceEvent(ToDeviceEvent event) async { + if (!event.type.startsWith('m.key.verification')) { + return; + } + // we have key verification going on! + final transactionId = KeyVerification.getTransactionId(event.content); + if (transactionId == null) { + return; // TODO: send cancel with unknown transaction id + } + if (_requests.containsKey(transactionId)) { + await _requests[transactionId].handlePayload(event.type, event.content); + } else { + final newKeyRequest = + KeyVerification(encryption: encryption, userId: event.sender); + await newKeyRequest.handlePayload(event.type, event.content); + if (newKeyRequest.state != KeyVerificationState.askAccept) { + // okay, something went wrong (unknown transaction id?), just dispose it + newKeyRequest.dispose(); + } else { + _requests[transactionId] = newKeyRequest; + client.onKeyVerificationRequest.add(newKeyRequest); + } + } + } + + void dispose() { + for (final req in _requests.values) { + req.dispose(); + } + } +} diff --git a/lib/encryption/olm_manager.dart b/lib/encryption/olm_manager.dart new file mode 100644 index 0000000..2d78342 --- /dev/null +++ b/lib/encryption/olm_manager.dart @@ -0,0 +1,418 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2019, 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:canonical_json/canonical_json.dart'; +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:famedlysdk/matrix_api.dart'; +import 'package:olm/olm.dart' as olm; +import './encryption.dart'; + +class OlmManager { + final Encryption encryption; + Client get client => encryption.client; + olm.Account _olmAccount; + + /// Returns the base64 encoded keys to store them in a store. + /// This String should **never** leave the device! + String get pickledOlmAccount => + enabled ? _olmAccount.pickle(client.userID) : null; + String get fingerprintKey => + enabled ? json.decode(_olmAccount.identity_keys())['ed25519'] : null; + String get identityKey => + enabled ? json.decode(_olmAccount.identity_keys())['curve25519'] : null; + + bool get enabled => _olmAccount != null; + + OlmManager(this.encryption); + + /// A map from Curve25519 identity keys to existing olm sessions. + Map> get olmSessions => _olmSessions; + final Map> _olmSessions = {}; + + Future init(String olmAccount) async { + if (olmAccount == null) { + try { + await olm.init(); + _olmAccount = olm.Account(); + _olmAccount.create(); + if (await uploadKeys(uploadDeviceKeys: true) == false) { + throw ('Upload key failed'); + } + } catch (_) { + _olmAccount.free(); + _olmAccount = null; + } + } else { + try { + await olm.init(); + _olmAccount = olm.Account(); + _olmAccount.unpickle(client.userID, olmAccount); + } catch (_) { + _olmAccount.free(); + _olmAccount = null; + } + } + } + + /// Adds a signature to this json from this olm account. + Map signJson(Map payload) { + if (!enabled) throw ('Encryption is disabled'); + final Map unsigned = payload['unsigned']; + final Map signatures = payload['signatures']; + payload.remove('unsigned'); + payload.remove('signatures'); + final canonical = canonicalJson.encode(payload); + final signature = _olmAccount.sign(String.fromCharCodes(canonical)); + if (signatures != null) { + payload['signatures'] = signatures; + } else { + payload['signatures'] = {}; + } + if (!payload['signatures'].containsKey(client.userID)) { + payload['signatures'][client.userID] = {}; + } + payload['signatures'][client.userID]['ed25519:${client.deviceID}'] = + signature; + if (unsigned != null) { + payload['unsigned'] = unsigned; + } + return payload; + } + + /// Checks the signature of a signed json object. + bool checkJsonSignature(String key, Map signedJson, + String userId, String deviceId) { + if (!enabled) throw ('Encryption is disabled'); + final Map signatures = signedJson['signatures']; + if (signatures == null || !signatures.containsKey(userId)) return false; + signedJson.remove('unsigned'); + signedJson.remove('signatures'); + if (!signatures[userId].containsKey('ed25519:$deviceId')) return false; + final String signature = signatures[userId]['ed25519:$deviceId']; + final canonical = canonicalJson.encode(signedJson); + final message = String.fromCharCodes(canonical); + var isValid = false; + final olmutil = olm.Utility(); + try { + olmutil.ed25519_verify(key, message, signature); + isValid = true; + } catch (e) { + isValid = false; + print('[LibOlm] Signature check failed: ' + e.toString()); + } finally { + olmutil.free(); + } + return isValid; + } + + /// Generates new one time keys, signs everything and upload it to the server. + Future uploadKeys({bool uploadDeviceKeys = false}) async { + if (!enabled) { + return true; + } + + // generate one-time keys + final oneTimeKeysCount = _olmAccount.max_number_of_one_time_keys(); + _olmAccount.generate_one_time_keys(oneTimeKeysCount); + final Map oneTimeKeys = + json.decode(_olmAccount.one_time_keys()); + + // now sign all the one-time keys + final signedOneTimeKeys = {}; + for (final entry in oneTimeKeys['curve25519'].entries) { + final key = entry.key; + final value = entry.value; + signedOneTimeKeys['signed_curve25519:$key'] = {}; + signedOneTimeKeys['signed_curve25519:$key'] = signJson({ + 'key': value, + }); + } + + // and now generate the payload to upload + final keysContent = { + if (uploadDeviceKeys) + 'device_keys': { + 'user_id': client.userID, + 'device_id': client.deviceID, + 'algorithms': [ + 'm.olm.v1.curve25519-aes-sha2', + 'm.megolm.v1.aes-sha2' + ], + 'keys': {}, + }, + }; + if (uploadDeviceKeys) { + final Map keys = + json.decode(_olmAccount.identity_keys()); + for (final entry in keys.entries) { + final algorithm = entry.key; + final value = entry.value; + keysContent['device_keys']['keys']['$algorithm:${client.deviceID}'] = + value; + } + keysContent['device_keys'] = + signJson(keysContent['device_keys'] as Map); + } + + final response = await client.api.uploadDeviceKeys( + deviceKeys: uploadDeviceKeys + ? MatrixDeviceKeys.fromJson(keysContent['device_keys']) + : null, + oneTimeKeys: signedOneTimeKeys, + ); + if (response['signed_curve25519'] != oneTimeKeysCount) { + return false; + } + _olmAccount.mark_keys_as_published(); + await client.database?.updateClientKeys(pickledOlmAccount, client.id); + return true; + } + + void handleDeviceOneTimeKeysCount(Map countJson) { + if (!enabled) { + return; + } + // Check if there are at least half of max_number_of_one_time_keys left on the server + // and generate and upload more if not. + if (countJson.containsKey('signed_curve25519') && + countJson['signed_curve25519'] < + (_olmAccount.max_number_of_one_time_keys() / 2)) { + uploadKeys(); + } + } + + void storeOlmSession(String curve25519IdentityKey, olm.Session session) { + if (client.database == null) { + return; + } + if (!_olmSessions.containsKey(curve25519IdentityKey)) { + _olmSessions[curve25519IdentityKey] = []; + } + final ix = _olmSessions[curve25519IdentityKey] + .indexWhere((s) => s.session_id() == session.session_id()); + if (ix == -1) { + // add a new session + _olmSessions[curve25519IdentityKey].add(session); + } else { + // update an existing session + _olmSessions[curve25519IdentityKey][ix] = session; + } + final pickle = session.pickle(client.userID); + client.database.storeOlmSession( + client.id, curve25519IdentityKey, session.session_id(), pickle); + } + + ToDeviceEvent _decryptToDeviceEvent(ToDeviceEvent event) { + if (event.type != EventTypes.Encrypted) { + return event; + } + if (event.content['algorithm'] != 'm.olm.v1.curve25519-aes-sha2') { + throw ('Unknown algorithm: ${event.content}'); + } + if (!event.content['ciphertext'].containsKey(identityKey)) { + throw ("The message isn't sent for this device"); + } + String plaintext; + final String senderKey = event.content['sender_key']; + final String body = event.content['ciphertext'][identityKey]['body']; + final int type = event.content['ciphertext'][identityKey]['type']; + if (type != 0 && type != 1) { + throw ('Unknown message type'); + } + var existingSessions = olmSessions[senderKey]; + if (existingSessions != null) { + for (var session in existingSessions) { + if (type == 0 && session.matches_inbound(body) == true) { + plaintext = session.decrypt(type, body); + storeOlmSession(senderKey, session); + break; + } else if (type == 1) { + try { + plaintext = session.decrypt(type, body); + storeOlmSession(senderKey, session); + break; + } catch (_) { + plaintext = null; + } + } + } + } + if (plaintext == null && type != 0) { + return event; + } + + 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); + } + final Map plainContent = json.decode(plaintext); + if (plainContent.containsKey('sender') && + plainContent['sender'] != event.sender) { + throw ("Message was decrypted but sender doesn't match"); + } + if (plainContent.containsKey('recipient') && + plainContent['recipient'] != client.userID) { + throw ("Message was decrypted but recipient doesn't match"); + } + if (plainContent['recipient_keys'] is Map && + plainContent['recipient_keys']['ed25519'] is String && + plainContent['recipient_keys']['ed25519'] != fingerprintKey) { + throw ("Message was decrypted but own fingerprint Key doesn't match"); + } + return ToDeviceEvent( + content: plainContent['content'], + encryptedContent: event.content, + type: plainContent['type'], + sender: event.sender, + ); + } + + Future decryptToDeviceEvent(ToDeviceEvent event) async { + if (event.type != EventTypes.Encrypted) { + return event; + } + event = _decryptToDeviceEvent(event); + if (event.type != EventTypes.Encrypted || client.database == null) { + 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; + return _decryptToDeviceEvent(event); + } + + Future startOutgoingOlmSessions(List deviceKeys, + {bool checkSignature = true}) async { + var requestingKeysFrom = >{}; + for (var device in deviceKeys) { + if (requestingKeysFrom[device.userId] == null) { + requestingKeysFrom[device.userId] = {}; + } + requestingKeysFrom[device.userId][device.deviceId] = 'signed_curve25519'; + } + + final response = + await client.api.requestOneTimeKeys(requestingKeysFrom, timeout: 10000); + + for (var userKeysEntry in response.oneTimeKeys.entries) { + final userId = userKeysEntry.key; + for (var deviceKeysEntry in userKeysEntry.value.entries) { + final deviceId = deviceKeysEntry.key; + final fingerprintKey = + client.userDeviceKeys[userId].deviceKeys[deviceId].ed25519Key; + final identityKey = + client.userDeviceKeys[userId].deviceKeys[deviceId].curve25519Key; + for (Map deviceKey in deviceKeysEntry.value.values) { + if (checkSignature && + checkJsonSignature(fingerprintKey, deviceKey, userId, deviceId) == + false) { + continue; + } + try { + var session = olm.Session(); + session.create_outbound(_olmAccount, identityKey, deviceKey['key']); + await storeOlmSession(identityKey, session); + } catch (e) { + print('[LibOlm] Could not create new outbound olm session: ' + + e.toString()); + } + } + } + } + } + + Future> encryptToDeviceMessagePayload( + DeviceKeys device, String type, Map payload) async { + var sess = olmSessions[device.curve25519Key]; + if (sess == null || sess.isEmpty) { + final sessions = await client.database + .getSingleOlmSessions(client.id, device.curve25519Key, client.userID); + if (sessions.isEmpty) { + throw ('No olm session found'); + } + sess = _olmSessions[device.curve25519Key] = sessions; + } + sess.sort((a, b) => a.session_id().compareTo(b.session_id())); + final fullPayload = { + 'type': type, + 'content': payload, + 'sender': client.userID, + 'keys': {'ed25519': fingerprintKey}, + 'recipient': device.userId, + 'recipient_keys': {'ed25519': device.ed25519Key}, + }; + final encryptResult = sess.first.encrypt(json.encode(fullPayload)); + storeOlmSession(device.curve25519Key, sess.first); + final encryptedBody = { + 'algorithm': 'm.olm.v1.curve25519-aes-sha2', + 'sender_key': identityKey, + 'ciphertext': {}, + }; + encryptedBody['ciphertext'][device.curve25519Key] = { + 'type': encryptResult.type, + 'body': encryptResult.body, + }; + return encryptedBody; + } + + Future> encryptToDeviceMessage( + List deviceKeys, + String type, + Map payload) async { + var data = >>{}; + final deviceKeysWithoutSession = List.from(deviceKeys); + deviceKeysWithoutSession.removeWhere((DeviceKeys deviceKeys) => + olmSessions.containsKey(deviceKeys.curve25519Key)); + if (deviceKeysWithoutSession.isNotEmpty) { + await startOutgoingOlmSessions(deviceKeysWithoutSession); + } + for (final device in deviceKeys) { + if (!data.containsKey(device.userId)) { + data[device.userId] = {}; + } + try { + data[device.userId][device.deviceId] = + await encryptToDeviceMessagePayload(device, type, payload); + } catch (e) { + print('[LibOlm] Error encrypting to-device event: ' + e.toString()); + continue; + } + } + return data; + } + + void dispose() { + for (final sessions in olmSessions.values) { + for (final sess in sessions) { + sess.free(); + } + } + _olmAccount?.free(); + _olmAccount = null; + } +} diff --git a/lib/src/utils/key_verification.dart b/lib/encryption/utils/key_verification.dart similarity index 95% rename from lib/src/utils/key_verification.dart rename to lib/encryption/utils/key_verification.dart index b676a58..3698477 100644 --- a/lib/src/utils/key_verification.dart +++ b/lib/encryption/utils/key_verification.dart @@ -1,11 +1,29 @@ +/* + * 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:typed_data'; import 'package:random_string/random_string.dart'; import 'package:canonical_json/canonical_json.dart'; import 'package:olm/olm.dart' as olm; -import '../../matrix_api.dart'; -import 'device_keys_list.dart'; -import '../client.dart'; -import '../room.dart'; +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:famedlysdk/matrix_api.dart'; + +import '../encryption.dart'; /* +-------------+ +-----------+ @@ -53,6 +71,9 @@ enum KeyVerificationState { } List _intersect(List a, List b) { + if (b == null || a == null) { + return []; + } final res = []; for (final v in a) { if (b.contains(v)) { @@ -94,7 +115,8 @@ _KeyVerificationMethod _makeVerificationMethod( class KeyVerification { String transactionId; - final Client client; + final Encryption encryption; + Client get client => encryption.client; final Room room; final String userId; void Function() onUpdate; @@ -114,7 +136,11 @@ class KeyVerification { String canceledReason; KeyVerification( - {this.client, this.room, this.userId, String deviceId, this.onUpdate}) { + {this.encryption, + this.room, + this.userId, + String deviceId, + this.onUpdate}) { lastActivity = DateTime.now(); _deviceId ??= deviceId; } @@ -384,7 +410,7 @@ class KeyVerification { final newTransactionId = await room.sendEvent(payload, type: type); if (transactionId == null) { transactionId = newTransactionId; - client.addKeyVerificationRequest(this); + encryption.keyVerificationManager.addRequest(this); } } else { await client.sendToDevice( @@ -404,10 +430,9 @@ class KeyVerification { abstract class _KeyVerificationMethod { KeyVerification request; - Client client; - _KeyVerificationMethod({this.request}) { - client = request.client; - } + Encryption get encryption => request.encryption; + Client get client => request.client; + _KeyVerificationMethod({this.request}); Future handlePayload(String type, Map payload); bool validateStart(Map payload) { @@ -662,7 +687,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { // we would also add the cross signing key here final deviceKeyId = 'ed25519:${client.deviceID}'; mac[deviceKeyId] = - _calculateMac(client.fingerprintKey, baseInfo + deviceKeyId); + _calculateMac(encryption.fingerprintKey, baseInfo + deviceKeyId); keyList.add(deviceKeyId); keyList.sort(); diff --git a/lib/encryption/utils/outbound_group_session.dart b/lib/encryption/utils/outbound_group_session.dart new file mode 100644 index 0000000..2a1c617 --- /dev/null +++ b/lib/encryption/utils/outbound_group_session.dart @@ -0,0 +1,58 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2019, 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:olm/olm.dart' as olm; +import '../../src/database/database.dart' show DbOutboundGroupSession; + +class OutboundGroupSession { + List devices; + DateTime creationTime; + olm.OutboundGroupSession outboundGroupSession; + int sentMessages; + bool get isValid => outboundGroupSession != null; + final String key; + + OutboundGroupSession( + {this.devices, + this.creationTime, + this.outboundGroupSession, + this.sentMessages, + this.key}); + + OutboundGroupSession.fromDb(DbOutboundGroupSession dbEntry, String key) + : key = key { + outboundGroupSession = olm.OutboundGroupSession(); + try { + outboundGroupSession.unpickle(key, dbEntry.pickle); + devices = List.from(json.decode(dbEntry.deviceIds)); + creationTime = dbEntry.creationTime; + sentMessages = dbEntry.sentMessages; + } catch (e) { + dispose(); + print( + '[LibOlm] Unable to unpickle outboundGroupSession: ' + e.toString()); + } + } + + void dispose() { + outboundGroupSession?.free(); + outboundGroupSession = null; + } +} diff --git a/lib/encryption/utils/session_key.dart b/lib/encryption/utils/session_key.dart new file mode 100644 index 0000000..5c6f0b2 --- /dev/null +++ b/lib/encryption/utils/session_key.dart @@ -0,0 +1,76 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2019, 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:olm/olm.dart' as olm; +import 'package:famedlysdk/famedlysdk.dart'; + +import '../../src/database/database.dart' show DbInboundGroupSession; + +class SessionKey { + Map content; + Map indexes; + olm.InboundGroupSession inboundGroupSession; + final String key; + List get forwardingCurve25519KeyChain => + content['forwarding_curve25519_key_chain'] ?? []; + String get senderClaimedEd25519Key => + content['sender_claimed_ed25519_key'] ?? ''; + String get senderKey => content['sender_key'] ?? ''; + bool get isValid => inboundGroupSession != null; + + SessionKey({this.content, this.inboundGroupSession, this.key, this.indexes}); + + SessionKey.fromDb(DbInboundGroupSession dbEntry, String key) : key = key { + final parsedContent = Event.getMapFromPayload(dbEntry.content); + final parsedIndexes = Event.getMapFromPayload(dbEntry.indexes); + content = + parsedContent != null ? Map.from(parsedContent) : null; + indexes = parsedIndexes != null + ? Map.from(parsedIndexes) + : {}; + inboundGroupSession = olm.InboundGroupSession(); + try { + inboundGroupSession.unpickle(key, dbEntry.pickle); + } catch (e) { + dispose(); + print('[LibOlm] Unable to unpickle inboundGroupSession: ' + e.toString()); + } + } + + Map toJson() { + final data = {}; + if (content != null) { + data['content'] = content; + } + if (indexes != null) { + data['indexes'] = indexes; + } + data['inboundGroupSession'] = inboundGroupSession.pickle(key); + return data; + } + + void dispose() { + inboundGroupSession?.free(); + inboundGroupSession = null; + } + + @override + String toString() => json.encode(toJson()); +} diff --git a/lib/famedlysdk.dart b/lib/famedlysdk.dart index da8813d..39860ab 100644 --- a/lib/famedlysdk.dart +++ b/lib/famedlysdk.dart @@ -22,7 +22,6 @@ export 'matrix_api.dart'; export 'package:famedlysdk/src/utils/room_update.dart'; export 'package:famedlysdk/src/utils/event_update.dart'; export 'package:famedlysdk/src/utils/device_keys_list.dart'; -export 'package:famedlysdk/src/utils/key_verification.dart'; export 'package:famedlysdk/src/utils/matrix_file.dart'; export 'package:famedlysdk/src/utils/matrix_id_string_extension.dart'; export 'package:famedlysdk/src/utils/uri_extension.dart'; @@ -32,7 +31,6 @@ export 'package:famedlysdk/src/utils/states_map.dart'; export 'package:famedlysdk/src/utils/to_device_event.dart'; export 'package:famedlysdk/src/client.dart'; export 'package:famedlysdk/src/event.dart'; -export 'package:famedlysdk/src/key_manager.dart'; export 'package:famedlysdk/src/room.dart'; export 'package:famedlysdk/src/timeline.dart'; export 'package:famedlysdk/src/user.dart'; diff --git a/lib/src/client.dart b/lib/src/client.dart index 114bfca..40036d6 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -20,16 +20,14 @@ import 'dart:async'; import 'dart:convert'; import 'dart:core'; -import 'package:canonical_json/canonical_json.dart'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/matrix_api.dart'; +import 'package:famedlysdk/encryption.dart'; import 'package:famedlysdk/src/room.dart'; import 'package:famedlysdk/src/utils/device_keys_list.dart'; import 'package:famedlysdk/src/utils/matrix_file.dart'; -import 'package:famedlysdk/src/utils/session_key.dart'; import 'package:famedlysdk/src/utils/to_device_event.dart'; import 'package:http/http.dart' as http; -import 'package:olm/olm.dart' as olm; import 'package:pedantic/pedantic.dart'; import 'event.dart'; @@ -38,8 +36,6 @@ import 'utils/event_update.dart'; import 'utils/room_update.dart'; import 'user.dart'; import 'database/database.dart' show Database; -import 'utils/key_verification.dart'; -import 'key_manager.dart'; typedef RoomSorter = int Function(Room a, Room b); @@ -53,12 +49,13 @@ class Client { int get id => _id; Database database; - KeyManager keyManager; bool enableE2eeRecovery; MatrixApi api; + Encryption encryption; + /// Create a client /// clientName = unique identifier of this client /// debug: Print debug output? @@ -70,7 +67,6 @@ class Client { this.enableE2eeRecovery = false, http.Client httpClient}) { api = MatrixApi(debug: debug, httpClient: httpClient); - keyManager = KeyManager(this); onLoginStateChanged.stream.listen((loginState) { if (debug) { print('[LoginState]: ${loginState.toString()}'); @@ -106,18 +102,14 @@ class Client { List get rooms => _rooms; List _rooms = []; - olm.Account _olmAccount; - - /// Returns the base64 encoded keys to store them in a store. - /// This String should **never** leave the device! - String get pickledOlmAccount => - encryptionEnabled ? _olmAccount.pickle(userID) : null; - /// Whether this client supports end-to-end encryption using olm. - bool get encryptionEnabled => _olmAccount != null; + bool get encryptionEnabled => encryption != null && encryption.enabled; /// Whether this client is able to encrypt and decrypt files. - bool get fileEncryptionEnabled => true; + bool get fileEncryptionEnabled => encryptionEnabled && true; + + String get identityKey => encryption?.identityKey ?? ''; + String get fingerprintKey => encryption?.fingerprintKey ?? ''; /// Warning! This endpoint is for testing only! set rooms(List newList) { @@ -529,8 +521,6 @@ class Client { final StreamController onKeyVerificationRequest = StreamController.broadcast(); - final Map _keyVerificationRequests = {}; - /// Matrix synchronisation is done with https long polling. This needs a /// timeout which is usually 30 seconds. int syncTimeoutSec = 30; @@ -604,31 +594,15 @@ class Client { if (api.accessToken == null || api.homeserver == null || _userID == null) { // we aren't logged in + encryption?.dispose(); + encryption = null; onLoginStateChanged.add(LoginState.loggedOut); return; } - // Try to create a new olm account or restore a previous one. - if (olmAccount == null) { - try { - await olm.init(); - _olmAccount = olm.Account(); - _olmAccount.create(); - if (await _uploadKeys(uploadDeviceKeys: true) == false) { - throw ('Upload key failed'); - } - } catch (_) { - _olmAccount = null; - } - } else { - try { - await olm.init(); - _olmAccount = olm.Account(); - _olmAccount.unpickle(userID, olmAccount); - } catch (_) { - _olmAccount = null; - } - } + encryption = Encryption( + debug: debug, client: this, enableE2eeRecovery: enableE2eeRecovery); + await encryption.init(olmAccount); if (database != null) { if (id != null) { @@ -639,7 +613,7 @@ class Client { _deviceID, _deviceName, prevBatch, - pickledOlmAccount, + encryption?.pickledOlmAccount, id, ); } else { @@ -651,11 +625,10 @@ class Client { _deviceID, _deviceName, prevBatch, - pickledOlmAccount, + encryption?.pickledOlmAccount, ); } _userDeviceKeys = await database.getUserDeviceKeys(id); - _olmSessions = await database.getOlmSessions(id, _userID); _rooms = await database.getRoomList(this, onlyLeft: false); _sortRooms(); accountData = await database.getAccountData(id); @@ -674,20 +647,12 @@ class Client { /// Resets all settings and stops the synchronisation. void clear() { - olmSessions.values.forEach((List sessions) { - sessions.forEach((olm.Session session) => session?.free()); - }); - rooms.forEach((Room room) { - room.clearOutboundGroupSession(wipe: true); - room.inboundGroupSessions.values.forEach((SessionKey sessionKey) { - sessionKey.inboundGroupSession?.free(); - }); - }); - _olmAccount?.free(); database?.clear(id); _id = api.accessToken = api.homeserver = _userID = _deviceID = _deviceName = prevBatch = null; _rooms = []; + encryption?.dispose(); + encryption = null; onLoginStateChanged.add(LoginState.loggedOut); } @@ -723,7 +688,9 @@ class Client { } prevBatch = syncResp.nextBatch; await _updateUserDeviceKeys(); - _cleanupKeyVerificationRequests(); + if (encryptionEnabled) { + encryption.onSync(); + } if (hash == _syncRequest.hashCode) unawaited(_sync()); } on MatrixException catch (exception) { onError.add(exception); @@ -740,7 +707,7 @@ class Client { /// Use this method only for testing utilities! Future handleSync(SyncUpdate sync) async { if (sync.toDevice != null) { - _handleToDeviceEvents(sync.toDevice); + await _handleToDeviceEvents(sync.toDevice); } if (sync.rooms != null) { if (sync.rooms.join != null) { @@ -784,31 +751,12 @@ class Client { if (sync.deviceLists != null) { await _handleDeviceListsEvents(sync.deviceLists); } - if (sync.deviceOneTimeKeysCount != null) { - _handleDeviceOneTimeKeysCount(sync.deviceOneTimeKeysCount); - } - while (_pendingToDeviceEvents.isNotEmpty) { - _updateRoomsByToDeviceEvent( - _pendingToDeviceEvents.removeLast(), - addToPendingIfNotFound: false, - ); + if (sync.deviceOneTimeKeysCount != null && encryptionEnabled) { + encryption.handleDeviceOneTimeKeysCount(sync.deviceOneTimeKeysCount); } onSync.add(sync); } - void _handleDeviceOneTimeKeysCount(Map deviceOneTimeKeysCount) { - if (!encryptionEnabled) return; - // Check if there are at least half of max_number_of_one_time_keys left on the server - // and generate and upload more if not. - if (deviceOneTimeKeysCount['signed_curve25519'] != null) { - final oneTimeKeysCount = deviceOneTimeKeysCount['signed_curve25519']; - if (oneTimeKeysCount < (_olmAccount.max_number_of_one_time_keys() / 2)) { - // Generate and upload more one time keys: - _uploadKeys(); - } - } - } - Future _handleDeviceListsEvents(DeviceListsUpdate deviceLists) async { if (deviceLists.changed is List) { for (final userId in deviceLists.changed) { @@ -827,36 +775,12 @@ class Client { } } - void _cleanupKeyVerificationRequests() { - for (final entry in _keyVerificationRequests.entries) { - (() async { - var dispose = entry.value.canceled || - entry.value.state == KeyVerificationState.done || - entry.value.state == KeyVerificationState.error; - if (!dispose) { - dispose = !(await entry.value.verifyActivity()); - } - if (dispose) { - entry.value.dispose(); - _keyVerificationRequests.remove(entry.key); - } - })(); - } - } - - void addKeyVerificationRequest(KeyVerification request) { - if (request.transactionId == null) { - return; - } - _keyVerificationRequests[request.transactionId] = request; - } - - void _handleToDeviceEvents(List events) { + Future _handleToDeviceEvents(List events) async { for (var i = 0; i < events.length; i++) { var toDeviceEvent = ToDeviceEvent.fromJson(events[i].toJson()); - if (toDeviceEvent.type == EventTypes.Encrypted) { + if (toDeviceEvent.type == EventTypes.Encrypted && encryptionEnabled) { try { - toDeviceEvent = decryptToDeviceEvent(toDeviceEvent); + toDeviceEvent = await encryption.decryptToDeviceEvent(toDeviceEvent); } catch (e, s) { print( '[LibOlm] Could not decrypt to device event from ${toDeviceEvent.sender} with content: ${toDeviceEvent.content}'); @@ -872,48 +796,13 @@ class Client { toDeviceEvent = ToDeviceEvent.fromJson(events[i].toJson()); } } - _updateRoomsByToDeviceEvent(toDeviceEvent); - if (toDeviceEvent.type.startsWith('m.key.verification.')) { - _handleToDeviceKeyVerificationRequest(toDeviceEvent); - } - if (['m.room_key_request', 'm.forwarded_room_key'] - .contains(toDeviceEvent.type)) { - keyManager.handleToDeviceEvent(toDeviceEvent); + if (encryptionEnabled) { + await encryption.handleToDeviceEvent(toDeviceEvent); } onToDeviceEvent.add(toDeviceEvent); } } - void _handleToDeviceKeyVerificationRequest(ToDeviceEvent toDeviceEvent) { - if (!toDeviceEvent.type.startsWith('m.key.verification.')) { - return; - } - // we have key verification going on! - final transactionId = - KeyVerification.getTransactionId(toDeviceEvent.content); - if (transactionId != null) { - if (_keyVerificationRequests.containsKey(transactionId)) { - _keyVerificationRequests[transactionId] - .handlePayload(toDeviceEvent.type, toDeviceEvent.content); - } else { - final newKeyRequest = - KeyVerification(client: this, userId: toDeviceEvent.sender); - newKeyRequest - .handlePayload(toDeviceEvent.type, toDeviceEvent.content) - .then((res) { - if (newKeyRequest.state != KeyVerificationState.askAccept) { - // okay, something went wrong (unknown transaction id?), just dispose it - newKeyRequest.dispose(); - } else { - // we have a new request! Let's broadcast it! - _keyVerificationRequests[transactionId] = newKeyRequest; - onKeyVerificationRequest.add(newKeyRequest); - } - }); - } - } - } - Future _handleRooms( Map rooms, Membership membership) async { for (final entry in rooms.entries) { @@ -1056,14 +945,8 @@ class Client { content: event, sortOrder: sortOrder, ); - if (event['type'] == EventTypes.Encrypted) { - update = update.decrypt(room); - } - if (update.eventType == EventTypes.Encrypted && database != null) { - // the event is still encrytped....let's try fetching the keys from the database! - await room.loadInboundGroupSessionKey( - event['content']['session_id'], event['content']['sender_key']); - update = update.decrypt(room); + if (event['type'] == EventTypes.Encrypted && encryptionEnabled) { + update = await update.decrypt(room); } if (type != 'ephemeral' && database != null) { await database.storeEventUpdate(id, update); @@ -1187,42 +1070,6 @@ class Client { if (eventUpdate.type == 'timeline') _sortRooms(); } - final List _pendingToDeviceEvents = []; - - void _updateRoomsByToDeviceEvent(ToDeviceEvent toDeviceEvent, - {addToPendingIfNotFound = true}) async { - try { - switch (toDeviceEvent.type) { - case 'm.room_key': - final roomId = toDeviceEvent.content['room_id']; - var room = getRoomById(roomId); - if (room == null && addToPendingIfNotFound) { - _pendingToDeviceEvents.add(toDeviceEvent); - break; - } - room ??= Room(client: this, id: roomId); - final String sessionId = toDeviceEvent.content['session_id']; - if (userDeviceKeys.containsKey(toDeviceEvent.sender) && - userDeviceKeys[toDeviceEvent.sender] - .deviceKeys - .containsKey(toDeviceEvent.content['requesting_device_id'])) { - toDeviceEvent.content['sender_claimed_ed25519_key'] = - userDeviceKeys[toDeviceEvent.sender] - .deviceKeys[toDeviceEvent.content['requesting_device_id']] - .ed25519Key; - } - room.setInboundGroupSession( - sessionId, - toDeviceEvent.content, - forwarded: false, - ); - break; - } - } catch (e) { - print('[Matrix] Error while processing to-device-event: ' + e.toString()); - } - } - bool _sortLock = false; /// The compare function how the rooms should be sorted internally. By default @@ -1301,7 +1148,7 @@ class Client { if (entry.isValid) { _userDeviceKeys[userId].deviceKeys[deviceId] = entry; if (deviceId == deviceID && - entry.ed25519Key == fingerprintKey) { + entry.ed25519Key == encryption?.fingerprintKey) { // Always trust the own device entry.verified = true; } @@ -1349,213 +1196,6 @@ class Client { } } - String get fingerprintKey => encryptionEnabled - ? json.decode(_olmAccount.identity_keys())['ed25519'] - : null; - String get identityKey => encryptionEnabled - ? json.decode(_olmAccount.identity_keys())['curve25519'] - : null; - - /// Adds a signature to this json from this olm account. - Map signJson(Map payload) { - if (!encryptionEnabled) throw ('Encryption is disabled'); - final Map unsigned = payload['unsigned']; - final Map signatures = payload['signatures']; - payload.remove('unsigned'); - payload.remove('signatures'); - final canonical = canonicalJson.encode(payload); - final signature = _olmAccount.sign(String.fromCharCodes(canonical)); - if (signatures != null) { - payload['signatures'] = signatures; - } else { - payload['signatures'] = {}; - } - payload['signatures'][userID] = {}; - payload['signatures'][userID]['ed25519:$deviceID'] = signature; - if (unsigned != null) { - payload['unsigned'] = unsigned; - } - return payload; - } - - /// Checks the signature of a signed json object. - bool checkJsonSignature(String key, Map signedJson, - String userId, String deviceId) { - if (!encryptionEnabled) throw ('Encryption is disabled'); - final Map signatures = signedJson['signatures']; - if (signatures == null || !signatures.containsKey(userId)) return false; - signedJson.remove('unsigned'); - signedJson.remove('signatures'); - if (!signatures[userId].containsKey('ed25519:$deviceId')) return false; - final String signature = signatures[userId]['ed25519:$deviceId']; - final canonical = canonicalJson.encode(signedJson); - final message = String.fromCharCodes(canonical); - var isValid = true; - try { - olm.Utility() - ..ed25519_verify(key, message, signature) - ..free(); - } catch (e) { - isValid = false; - print('[LibOlm] Signature check failed: ' + e.toString()); - } - return isValid; - } - - DateTime lastTimeKeysUploaded; - - /// Generates new one time keys, signs everything and upload it to the server. - Future _uploadKeys({bool uploadDeviceKeys = false}) async { - if (!encryptionEnabled) return true; - - final oneTimeKeysCount = _olmAccount.max_number_of_one_time_keys(); - _olmAccount.generate_one_time_keys(oneTimeKeysCount); - final Map oneTimeKeys = - json.decode(_olmAccount.one_time_keys()); - - var signedOneTimeKeys = {}; - - for (String key in oneTimeKeys['curve25519'].keys) { - signedOneTimeKeys['signed_curve25519:$key'] = {}; - signedOneTimeKeys['signed_curve25519:$key']['key'] = - oneTimeKeys['curve25519'][key]; - signedOneTimeKeys['signed_curve25519:$key'] = - signJson(signedOneTimeKeys['signed_curve25519:$key']); - } - - var keysContent = { - if (uploadDeviceKeys) - 'device_keys': { - 'user_id': userID, - 'device_id': deviceID, - 'algorithms': [ - 'm.olm.v1.curve25519-aes-sha2', - 'm.megolm.v1.aes-sha2' - ], - 'keys': {}, - }, - }; - if (uploadDeviceKeys) { - final Map keys = - json.decode(_olmAccount.identity_keys()); - for (var algorithm in keys.keys) { - keysContent['device_keys']['keys']['$algorithm:$deviceID'] = - keys[algorithm]; - } - keysContent['device_keys'] = - signJson(keysContent['device_keys'] as Map); - } - - _olmAccount.mark_keys_as_published(); - final response = await api.uploadDeviceKeys( - deviceKeys: uploadDeviceKeys - ? MatrixDeviceKeys.fromJson(keysContent['device_keys']) - : null, - oneTimeKeys: signedOneTimeKeys, - ); - if (response['signed_curve25519'] != oneTimeKeysCount) { - return false; - } - await database?.updateClientKeys(pickledOlmAccount, id); - lastTimeKeysUploaded = DateTime.now(); - return true; - } - - /// Try to decrypt a ToDeviceEvent encrypted with olm. - ToDeviceEvent decryptToDeviceEvent(ToDeviceEvent toDeviceEvent) { - if (toDeviceEvent.type != EventTypes.Encrypted) { - print( - '[LibOlm] Warning! Tried to decrypt a not-encrypted to-device-event'); - return toDeviceEvent; - } - if (toDeviceEvent.content['algorithm'] != 'm.olm.v1.curve25519-aes-sha2') { - throw ('Unknown algorithm: ${toDeviceEvent.content}'); - } - if (!toDeviceEvent.content['ciphertext'].containsKey(identityKey)) { - throw ("The message isn't sent for this device"); - } - String plaintext; - final String senderKey = toDeviceEvent.content['sender_key']; - final String body = - toDeviceEvent.content['ciphertext'][identityKey]['body']; - final int type = toDeviceEvent.content['ciphertext'][identityKey]['type']; - if (type != 0 && type != 1) { - throw ('Unknown message type'); - } - var existingSessions = olmSessions[senderKey]; - if (existingSessions != null) { - for (var session in existingSessions) { - if (type == 0 && session.matches_inbound(body) == true) { - plaintext = session.decrypt(type, body); - storeOlmSession(senderKey, session); - break; - } else if (type == 1) { - try { - plaintext = session.decrypt(type, body); - storeOlmSession(senderKey, session); - break; - } catch (_) { - plaintext = null; - } - } - } - } - if (plaintext == null && type != 0) { - throw ('No existing sessions found'); - } - - if (plaintext == null) { - var newSession = olm.Session(); - newSession.create_inbound_from(_olmAccount, senderKey, body); - _olmAccount.remove_one_time_keys(newSession); - database?.updateClientKeys(pickledOlmAccount, id); - plaintext = newSession.decrypt(type, body); - storeOlmSession(senderKey, newSession); - } - final Map plainContent = json.decode(plaintext); - if (plainContent.containsKey('sender') && - plainContent['sender'] != toDeviceEvent.sender) { - throw ("Message was decrypted but sender doesn't match"); - } - if (plainContent.containsKey('recipient') && - plainContent['recipient'] != userID) { - throw ("Message was decrypted but recipient doesn't match"); - } - if (plainContent['recipient_keys'] is Map && - plainContent['recipient_keys']['ed25519'] is String && - plainContent['recipient_keys']['ed25519'] != fingerprintKey) { - throw ("Message was decrypted but own fingerprint Key doesn't match"); - } - return ToDeviceEvent( - content: plainContent['content'], - encryptedContent: toDeviceEvent.content, - type: plainContent['type'], - sender: toDeviceEvent.sender, - ); - } - - /// A map from Curve25519 identity keys to existing olm sessions. - Map> get olmSessions => _olmSessions; - Map> _olmSessions = {}; - - void storeOlmSession(String curve25519IdentityKey, olm.Session session) { - if (!_olmSessions.containsKey(curve25519IdentityKey)) { - _olmSessions[curve25519IdentityKey] = []; - } - final ix = _olmSessions[curve25519IdentityKey] - .indexWhere((s) => s.session_id() == session.session_id()); - if (ix == -1) { - // add a new session - _olmSessions[curve25519IdentityKey].add(session); - } else { - // update an existing session - _olmSessions[curve25519IdentityKey][ix] = session; - } - final pickle = session.pickle(userID); - database?.storeOlmSession( - id, curve25519IdentityKey, session.session_id(), pickle); - } - /// Sends an encrypted [message] of this [type] to these [deviceKeys]. To send /// the request to all devices of the current user, pass an empty list to [deviceKeys]. Future sendToDevice( @@ -1589,96 +1229,22 @@ class Client { } } else { if (encrypted) { - // Create new sessions with devices if there is no existing session yet. - var deviceKeysWithoutSession = List.from(deviceKeys); - deviceKeysWithoutSession.removeWhere((DeviceKeys deviceKeys) => - olmSessions.containsKey(deviceKeys.curve25519Key)); - if (deviceKeysWithoutSession.isNotEmpty) { - await startOutgoingOlmSessions(deviceKeysWithoutSession); + data = + await encryption.encryptToDeviceMessage(deviceKeys, type, message); + } else { + for (final device in deviceKeys) { + if (!data.containsKey(device.userId)) { + data[device.userId] = {}; + } + data[device.userId][device.deviceId] = sendToDeviceMessage; } } - for (var i = 0; i < deviceKeys.length; i++) { - var device = deviceKeys[i]; - if (!data.containsKey(device.userId)) { - data[device.userId] = {}; - } - - if (encrypted) { - var existingSessions = olmSessions[device.curve25519Key]; - if (existingSessions == null || existingSessions.isEmpty) continue; - existingSessions - .sort((a, b) => a.session_id().compareTo(b.session_id())); - - final payload = { - 'type': type, - 'content': message, - 'sender': userID, - 'keys': {'ed25519': fingerprintKey}, - 'recipient': device.userId, - 'recipient_keys': {'ed25519': device.ed25519Key}, - }; - final encryptResult = - existingSessions.first.encrypt(json.encode(payload)); - storeOlmSession(device.curve25519Key, existingSessions.first); - sendToDeviceMessage = { - 'algorithm': 'm.olm.v1.curve25519-aes-sha2', - 'sender_key': identityKey, - 'ciphertext': {}, - }; - sendToDeviceMessage['ciphertext'][device.curve25519Key] = { - 'type': encryptResult.type, - 'body': encryptResult.body, - }; - } - - data[device.userId][device.deviceId] = sendToDeviceMessage; - } } if (encrypted) type = EventTypes.Encrypted; final messageID = generateUniqueTransactionId(); await api.sendToDevice(type, messageID, data); } - Future startOutgoingOlmSessions(List deviceKeys, - {bool checkSignature = true}) async { - var requestingKeysFrom = >{}; - for (var device in deviceKeys) { - if (requestingKeysFrom[device.userId] == null) { - requestingKeysFrom[device.userId] = {}; - } - requestingKeysFrom[device.userId][device.deviceId] = 'signed_curve25519'; - } - - final response = - await api.requestOneTimeKeys(requestingKeysFrom, timeout: 10000); - - for (var userKeysEntry in response.oneTimeKeys.entries) { - final userId = userKeysEntry.key; - for (var deviceKeysEntry in userKeysEntry.value.entries) { - final deviceId = deviceKeysEntry.key; - final fingerprintKey = - userDeviceKeys[userId].deviceKeys[deviceId].ed25519Key; - final identityKey = - userDeviceKeys[userId].deviceKeys[deviceId].curve25519Key; - for (Map deviceKey in deviceKeysEntry.value.values) { - if (checkSignature && - checkJsonSignature(fingerprintKey, deviceKey, userId, deviceId) == - false) { - continue; - } - try { - var session = olm.Session(); - session.create_outbound(_olmAccount, identityKey, deviceKey['key']); - await storeOlmSession(identityKey, session); - } catch (e) { - print('[LibOlm] Could not create new outbound olm session: ' + - e.toString()); - } - } - } - } - } - /// Whether all push notifications are muted using the [.m.rule.master] /// rule of the push rules: https://matrix.org/docs/spec/client_server/r0.6.0#m-rule-master bool get allPushNotificationsMuted { diff --git a/lib/src/database/database.dart b/lib/src/database/database.dart index 080ab21..eda8c5b 100644 --- a/lib/src/database/database.dart +++ b/lib/src/database/database.dart @@ -91,6 +91,22 @@ class Database extends _$Database { return res; } + Future> getSingleOlmSessions( + int clientId, String identityKey, String userId) async { + final rows = await dbGetOlmSessions(clientId, identityKey).get(); + final res = []; + for (final row in rows) { + try { + var session = olm.Session(); + session.unpickle(userId, row.pickle); + res.add(session); + } catch (e) { + print('[LibOlm] Could not unpickle olm session: ' + e.toString()); + } + } + return res; + } + Future getDbOutboundGroupSession( int clientId, String roomId) async { final res = await dbGetOutboundGroupSession(clientId, roomId).get(); diff --git a/lib/src/database/database.g.dart b/lib/src/database/database.g.dart index 61bc1c7..9b86794 100644 --- a/lib/src/database/database.g.dart +++ b/lib/src/database/database.g.dart @@ -4851,6 +4851,19 @@ abstract class _$Database extends GeneratedDatabase { readsFrom: {olmSessions}).map(_rowToDbOlmSessions); } + Selectable dbGetOlmSessions( + int client_id, String identity_key) { + return customSelect( + 'SELECT * FROM olm_sessions WHERE client_id = :client_id AND identity_key = :identity_key', + variables: [ + Variable.withInt(client_id), + Variable.withString(identity_key) + ], + readsFrom: { + olmSessions + }).map(_rowToDbOlmSessions); + } + Future storeOlmSession( int client_id, String identitiy_key, String session_id, String pickle) { return customInsert( diff --git a/lib/src/database/database.moor b/lib/src/database/database.moor index ebe66ea..073a71f 100644 --- a/lib/src/database/database.moor +++ b/lib/src/database/database.moor @@ -155,6 +155,7 @@ storePrevBatch: UPDATE clients SET prev_batch = :prev_batch WHERE client_id = :c getAllUserDeviceKeys: SELECT * FROM user_device_keys WHERE client_id = :client_id; getAllUserDeviceKeysKeys: SELECT * FROM user_device_keys_key WHERE client_id = :client_id; getAllOlmSessions: SELECT * FROM olm_sessions WHERE client_id = :client_id; +dbGetOlmSessions: SELECT * FROM olm_sessions WHERE client_id = :client_id AND identity_key = :identity_key; storeOlmSession: INSERT OR REPLACE INTO olm_sessions (client_id, identity_key, session_id, pickle) VALUES (:client_id, :identitiy_key, :session_id, :pickle); getAllOutboundGroupSessions: SELECT * FROM outbound_group_sessions WHERE client_id = :client_id; dbGetOutboundGroupSession: SELECT * FROM outbound_group_sessions WHERE client_id = :client_id AND room_id = :room_id; diff --git a/lib/src/event.dart b/lib/src/event.dart index ce48f99..fd2ef27 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -19,6 +19,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:famedlysdk/famedlysdk.dart'; +import 'package:famedlysdk/encryption.dart'; import 'package:famedlysdk/src/utils/receipt.dart'; import 'package:http/http.dart' as http; import 'package:matrix_file_e2ee/matrix_file_e2ee.dart'; @@ -333,36 +334,6 @@ class Event extends MatrixEvent { return await timeline.getEventById(replyEventId); } - Future loadSession() { - return room.loadInboundGroupSessionKeyForEvent(this); - } - - /// Trys to decrypt this event. Returns a m.bad.encrypted event - /// if it fails and does nothing if the event was not encrypted. - Event get decrypted => room.decryptGroupMessage(this); - - /// Trys to decrypt this event and persists it in the database afterwards - Future decryptAndStore([String updateType = 'timeline']) async { - final newEvent = decrypted; - if (newEvent.type == EventTypes.Encrypted) { - return newEvent; // decryption failed - } - await room.client.database?.storeEventUpdate( - room.client.id, - EventUpdate( - eventType: newEvent.type, - content: newEvent.toJson(), - roomID: newEvent.roomId, - type: updateType, - sortOrder: newEvent.sortOrder, - ), - ); - if (updateType != 'history') { - room.setState(newEvent); - } - return newEvent; - } - /// If this event is encrypted and the decryption was not successful because /// the session is unknown, this requests the session key from other devices /// in the room. If the event is not encrypted or the decryption failed because diff --git a/lib/src/key_manager.dart b/lib/src/key_manager.dart deleted file mode 100644 index 0b5401d..0000000 --- a/lib/src/key_manager.dart +++ /dev/null @@ -1,214 +0,0 @@ -import 'client.dart'; -import 'room.dart'; -import 'utils/to_device_event.dart'; -import 'utils/device_keys_list.dart'; - -class KeyManager { - final Client client; - final outgoingShareRequests = {}; - final incomingShareRequests = {}; - - KeyManager(this.client); - - /// Request a certain key from another device - Future request(Room room, String sessionId, String senderKey) async { - // while we just send the to-device event to '*', we still need to save the - // devices themself to know where to send the cancel to after receiving a reply - final devices = await room.getUserDeviceKeys(); - final requestId = client.generateUniqueTransactionId(); - final request = KeyManagerKeyShareRequest( - requestId: requestId, - devices: devices, - room: room, - sessionId: sessionId, - senderKey: senderKey, - ); - await client.sendToDevice( - [], - 'm.room_key_request', - { - 'action': 'request', - 'body': { - 'algorithm': 'm.megolm.v1.aes-sha2', - 'room_id': room.id, - 'sender_key': senderKey, - 'session_id': sessionId, - }, - 'request_id': requestId, - 'requesting_device_id': client.deviceID, - }, - encrypted: false, - toUsers: await room.requestParticipants()); - outgoingShareRequests[request.requestId] = request; - } - - /// Handle an incoming to_device event that is related to key sharing - Future handleToDeviceEvent(ToDeviceEvent event) async { - if (event.type == 'm.room_key_request') { - if (!event.content.containsKey('request_id')) { - return; // invalid event - } - if (event.content['action'] == 'request') { - // we are *receiving* a request - if (!event.content.containsKey('body')) { - return; // no body - } - if (!client.userDeviceKeys.containsKey(event.sender) || - !client.userDeviceKeys[event.sender].deviceKeys - .containsKey(event.content['requesting_device_id'])) { - return; // device not found - } - final device = client.userDeviceKeys[event.sender] - .deviceKeys[event.content['requesting_device_id']]; - if (device.userId == client.userID && - device.deviceId == client.deviceID) { - return; // ignore requests by ourself - } - final room = client.getRoomById(event.content['body']['room_id']); - if (room == null) { - return; // unknown room - } - final sessionId = event.content['body']['session_id']; - // okay, let's see if we have this session at all - await room.loadInboundGroupSessionKey(sessionId); - if (!room.inboundGroupSessions.containsKey(sessionId)) { - return; // we don't have this session anyways - } - final request = KeyManagerKeyShareRequest( - requestId: event.content['request_id'], - devices: [device], - room: room, - sessionId: event.content['body']['session_id'], - senderKey: event.content['body']['sender_key'], - ); - if (incomingShareRequests.containsKey(request.requestId)) { - return; // we don't want to process one and the same request multiple times - } - incomingShareRequests[request.requestId] = request; - final roomKeyRequest = - RoomKeyRequest.fromToDeviceEvent(event, this, request); - if (device.userId == client.userID && - device.verified && - !device.blocked) { - // alright, we can forward the key - await roomKeyRequest.forwardKey(); - } else { - client.onRoomKeyRequest - .add(roomKeyRequest); // let the client handle this - } - } else if (event.content['action'] == 'request_cancellation') { - // we got told to cancel an incoming request - if (!incomingShareRequests.containsKey(event.content['request_id'])) { - return; // we don't know this request anyways - } - // alright, let's just cancel this request - final request = incomingShareRequests[event.content['request_id']]; - request.canceled = true; - incomingShareRequests.remove(request.requestId); - } - } else if (event.type == 'm.forwarded_room_key') { - // we *received* an incoming key request - if (event.encryptedContent == null) { - return; // event wasn't encrypted, this is a security risk - } - final request = outgoingShareRequests.values.firstWhere( - (r) => - r.room.id == event.content['room_id'] && - r.sessionId == event.content['session_id'] && - r.senderKey == event.content['sender_key'], - orElse: () => null); - if (request == null || request.canceled) { - return; // no associated request found or it got canceled - } - final device = request.devices.firstWhere( - (d) => - d.userId == event.sender && - d.curve25519Key == event.encryptedContent['sender_key'], - orElse: () => null); - if (device == null) { - return; // someone we didn't send our request to replied....better ignore this - } - // TODO: verify that the keys work to decrypt a message - // alright, all checks out, let's go ahead and store this session - request.room.setInboundGroupSession(request.sessionId, event.content, - forwarded: true); - request.devices.removeWhere( - (k) => k.userId == device.userId && k.deviceId == device.deviceId); - outgoingShareRequests.remove(request.requestId); - // send cancel to all other devices - if (request.devices.isEmpty) { - return; // no need to send any cancellation - } - await client.sendToDevice( - request.devices, - 'm.room_key_request', - { - 'action': 'request_cancellation', - 'request_id': request.requestId, - 'requesting_device_id': client.deviceID, - }, - encrypted: false); - } - } -} - -class KeyManagerKeyShareRequest { - final String requestId; - final List devices; - final Room room; - final String sessionId; - final String senderKey; - bool canceled; - - KeyManagerKeyShareRequest( - {this.requestId, - this.devices, - this.room, - this.sessionId, - this.senderKey, - this.canceled = false}); -} - -class RoomKeyRequest extends ToDeviceEvent { - KeyManager keyManager; - KeyManagerKeyShareRequest request; - RoomKeyRequest.fromToDeviceEvent(ToDeviceEvent toDeviceEvent, - KeyManager keyManager, KeyManagerKeyShareRequest request) { - this.keyManager = keyManager; - this.request = request; - sender = toDeviceEvent.sender; - content = toDeviceEvent.content; - type = toDeviceEvent.type; - } - - Room get room => request.room; - - DeviceKeys get requestingDevice => request.devices.first; - - Future forwardKey() async { - if (request.canceled) { - keyManager.incomingShareRequests.remove(request.requestId); - return; // request is canceled, don't send anything - } - var room = this.room; - await room.loadInboundGroupSessionKey(request.sessionId); - final session = room.inboundGroupSessions[request.sessionId]; - var forwardedKeys = [keyManager.client.identityKey]; - for (final key in session.forwardingCurve25519KeyChain) { - forwardedKeys.add(key); - } - await requestingDevice.setVerified(true, keyManager.client); - var message = session.content; - message['forwarding_curve25519_key_chain'] = forwardedKeys; - - message['session_key'] = session.inboundGroupSession - .export_session(session.inboundGroupSession.first_known_index()); - // send the actual reply of the key back to the requester - await keyManager.client.sendToDevice( - [requestingDevice], - 'm.forwarded_room_key', - message, - ); - keyManager.incomingShareRequests.remove(request.requestId); - } -} diff --git a/lib/src/room.dart b/lib/src/room.dart index 331d773..d418c90 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -17,21 +17,18 @@ */ import 'dart:async'; -import 'dart:convert'; import 'package:famedlysdk/matrix_api.dart'; -import 'package:pedantic/pedantic.dart'; +import 'package:famedlysdk/encryption.dart'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/src/client.dart'; import 'package:famedlysdk/src/event.dart'; import 'package:famedlysdk/src/utils/event_update.dart'; import 'package:famedlysdk/src/utils/room_update.dart'; import 'package:famedlysdk/src/utils/matrix_file.dart'; -import 'package:famedlysdk/src/utils/session_key.dart'; import 'package:image/image.dart'; import 'package:matrix_file_e2ee/matrix_file_e2ee.dart'; import 'package:mime_type/mime_type.dart'; -import 'package:olm/olm.dart' as olm; import 'package:html_unescape/html_unescape.dart'; import './user.dart'; @@ -81,13 +78,6 @@ class Room { /// Key-Value store for private account data only visible for this user. Map roomAccountData = {}; - olm.OutboundGroupSession get outboundGroupSession => _outboundGroupSession; - olm.OutboundGroupSession _outboundGroupSession; - - List _outboundGroupSessionDevices; - DateTime _outboundGroupSessionCreationTime; - int _outboundGroupSessionSentMessages; - double _newestSortOrder; double _oldestSortOrder; @@ -110,168 +100,6 @@ class Room { _oldestSortOrder, _newestSortOrder, client.id, id); } - /// Clears the existing outboundGroupSession, tries to create a new one and - /// stores it as an ingoingGroupSession in the [sessionKeys]. Then sends the - /// new session encrypted with olm to all non-blocked devices using - /// to-device-messaging. - Future createOutboundGroupSession() async { - await clearOutboundGroupSession(wipe: true); - var deviceKeys = await getUserDeviceKeys(); - olm.OutboundGroupSession outboundGroupSession; - var outboundGroupSessionDevices = []; - for (var keys in deviceKeys) { - if (!keys.blocked) outboundGroupSessionDevices.add(keys.deviceId); - } - outboundGroupSessionDevices.sort(); - try { - outboundGroupSession = olm.OutboundGroupSession(); - outboundGroupSession.create(); - } catch (e) { - outboundGroupSession = null; - print('[LibOlm] Unable to create new outboundGroupSession: ' + - e.toString()); - } - - if (outboundGroupSession == null) return; - // Add as an inboundSession to the [sessionKeys]. - var rawSession = { - 'algorithm': 'm.megolm.v1.aes-sha2', - 'room_id': id, - 'session_id': outboundGroupSession.session_id(), - 'session_key': outboundGroupSession.session_key(), - }; - setInboundGroupSession(rawSession['session_id'], rawSession); - try { - await client.sendToDevice(deviceKeys, 'm.room_key', rawSession); - _outboundGroupSession = outboundGroupSession; - _outboundGroupSessionDevices = outboundGroupSessionDevices; - _outboundGroupSessionCreationTime = DateTime.now(); - _outboundGroupSessionSentMessages = 0; - await _storeOutboundGroupSession(); - } catch (e, s) { - print( - '[LibOlm] Unable to send the session key to the participating devices: ' + - e.toString()); - print(s); - await clearOutboundGroupSession(); - } - return; - } - - Future _storeOutboundGroupSession() async { - if (_outboundGroupSession == null) return; - await client.database?.storeOutboundGroupSession( - client.id, - id, - _outboundGroupSession.pickle(client.userID), - json.encode(_outboundGroupSessionDevices), - _outboundGroupSessionCreationTime, - _outboundGroupSessionSentMessages); - return; - } - - /// Clears the existing outboundGroupSession but first checks if the participating - /// devices have been changed. Returns false if the session has not been cleared because - /// it wasn't necessary. - Future clearOutboundGroupSession({bool wipe = false}) async { - if (!wipe && _outboundGroupSessionDevices != null) { - // first check if the devices in the room changed - var deviceKeys = await getUserDeviceKeys(); - var outboundGroupSessionDevices = []; - for (var keys in deviceKeys) { - if (!keys.blocked) outboundGroupSessionDevices.add(keys.deviceId); - } - outboundGroupSessionDevices.sort(); - if (outboundGroupSessionDevices.toString() != - _outboundGroupSessionDevices.toString()) { - wipe = true; - } - // next check if it needs to be rotated - final encryptionContent = getState(EventTypes.Encryption)?.content; - final maxMessages = encryptionContent != null && - encryptionContent['rotation_period_msgs'] is int - ? encryptionContent['rotation_period_msgs'] - : 100; - final maxAge = encryptionContent != null && - encryptionContent['rotation_period_ms'] is int - ? encryptionContent['rotation_period_ms'] - : 604800000; // default of one week - if (_outboundGroupSessionSentMessages >= maxMessages || - _outboundGroupSessionCreationTime - .add(Duration(milliseconds: maxAge)) - .isBefore(DateTime.now())) { - wipe = true; - } - if (!wipe) { - return false; - } - } - if (!wipe && - _outboundGroupSessionDevices == null && - _outboundGroupSession == null) { - return true; // let's just short-circuit out of here, no need to do DB stuff - } - _outboundGroupSessionDevices = null; - await client.database?.removeOutboundGroupSession(client.id, id); - _outboundGroupSession?.free(); - _outboundGroupSession = null; - return true; - } - - /// Key-Value store of session ids to the session keys. Only m.megolm.v1.aes-sha2 - /// session keys are supported. They are stored as a Map with the following keys: - /// { - /// "algorithm": "m.megolm.v1.aes-sha2", - /// "room_id": "!Cuyf34gef24t:localhost", - /// "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ", - /// "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY..." - /// } - Map get inboundGroupSessions => _inboundGroupSessions; - final _inboundGroupSessions = {}; - - /// Add a new session key to the [sessionKeys]. - void setInboundGroupSession(String sessionId, Map content, - {bool forwarded = false}) { - if (inboundGroupSessions.containsKey(sessionId)) return; - olm.InboundGroupSession inboundGroupSession; - if (content['algorithm'] == 'm.megolm.v1.aes-sha2') { - try { - inboundGroupSession = olm.InboundGroupSession(); - if (forwarded) { - inboundGroupSession.import_session(content['session_key']); - } else { - inboundGroupSession.create(content['session_key']); - } - } catch (e) { - inboundGroupSession = null; - print('[LibOlm] Could not create new InboundGroupSession: ' + - e.toString()); - } - } - _inboundGroupSessions[sessionId] = SessionKey( - content: content, - inboundGroupSession: inboundGroupSession, - indexes: {}, - key: client.userID, - ); - client.database?.storeInboundGroupSession( - client.id, - id, - sessionId, - inboundGroupSession.pickle(client.userID), - json.encode(content), - json.encode({}), - ); - _tryAgainDecryptLastMessage(); - onSessionKeyReceived.add(sessionId); - } - - Future _tryAgainDecryptLastMessage() async { - if (getState(EventTypes.Encrypted) != null) { - await getState(EventTypes.Encrypted).decryptAndStore(); - } - } - /// Returns the [Event] for the given [typeKey] and optional [stateKey]. /// If no [stateKey] is provided, it defaults to an empty string. Event getState(String typeKey, [String stateKey = '']) => @@ -281,23 +109,13 @@ class Room { /// typeKey/stateKey key pair if there is one. void setState(Event state) { // Decrypt if necessary - if (state.type == EventTypes.Encrypted) { + if (state.type == EventTypes.Encrypted && client.encryptionEnabled) { try { - state = decryptGroupMessage(state); + state = client.encryption.decryptRoomEventSync(id, state); } catch (e) { print('[LibOlm] Could not decrypt room state: ' + e.toString()); } } - // Check if this is a member change and we need to clear the outboundGroupSession. - if (encrypted && - outboundGroupSession != null && - state.type == EventTypes.RoomMember) { - var newUser = state.asUser; - var oldUser = getState(EventTypes.RoomMember, newUser.id)?.asUser; - if (oldUser == null || oldUser.membership != newUser.membership) { - clearOutboundGroupSession(); - } - } if ((getState(state.type)?.originServerTs?.millisecondsSinceEpoch ?? 0) > (state.originServerTs?.millisecondsSinceEpoch ?? 1)) { return; @@ -882,7 +700,8 @@ class Room { // Send the text and on success, store and display a *sent* event. try { final sendMessageContent = encrypted && client.encryptionEnabled - ? await encryptGroupMessagePayload(content, type: type) + ? await client.encryption + .encryptGroupMessagePayload(id, content, type: type) : content; final res = await client.api.sendMessage( id, @@ -998,55 +817,42 @@ class Room { if (onHistoryReceived != null) onHistoryReceived(); prev_batch = resp.end; - final dbActions = Function()>[]; - if (client.database != null) { - dbActions.add( - () => client.database.setRoomPrevBatch(prev_batch, client.id, id)); - } + final loadFn = () async { + if (!((resp.chunk?.isNotEmpty ?? false) && resp.end != null)) return; - if (!((resp.chunk?.isNotEmpty ?? false) && resp.end != null)) return; - - if (resp.state != null) { - for (final state in resp.state) { - var eventUpdate = EventUpdate( - type: 'state', - roomID: id, - eventType: state.type, - content: state.toJson(), - sortOrder: oldSortOrder, - ).decrypt(this); - client.onEvent.add(eventUpdate); - if (client.database != null) { - dbActions.add( - () => client.database.storeEventUpdate(client.id, eventUpdate)); + if (resp.state != null) { + for (final state in resp.state) { + await EventUpdate( + type: 'state', + roomID: id, + eventType: state.type, + content: state.toJson(), + sortOrder: oldSortOrder, + ).decrypt(this, store: true); } } - } - for (final hist in resp.chunk) { - var eventUpdate = EventUpdate( - type: 'history', - roomID: id, - eventType: hist.type, - content: hist.toJson(), - sortOrder: oldSortOrder, - ).decrypt(this); - client.onEvent.add(eventUpdate); - if (client.database != null) { - dbActions.add( - () => client.database.storeEventUpdate(client.id, eventUpdate)); + for (final hist in resp.chunk) { + final eventUpdate = await EventUpdate( + type: 'history', + roomID: id, + eventType: hist.type, + content: hist.toJson(), + sortOrder: oldSortOrder, + ).decrypt(this, store: true); + client.onEvent.add(eventUpdate); } - } + }; + if (client.database != null) { - dbActions - .add(() => client.database.setRoomPrevBatch(resp.end, client.id, id)); + await client.database.transaction(() async { + await client.database.setRoomPrevBatch(resp.end, client.id, id); + await loadFn(); + await updateSortOrder(); + }); + } else { + await loadFn(); } - await client.database?.transaction(() async { - for (final f in dbActions) { - await f(); - } - await updateSortOrder(); - }); client.onRoomUpdate.add( RoomUpdate( id: id, @@ -1146,7 +952,6 @@ class Room { } for (final rawState in rawStates) { final newState = Event.fromDb(rawState, newRoom); - ; newRoom.setState(newState); } } @@ -1186,13 +991,13 @@ class Room { } // Try again to decrypt encrypted events and update the database. - if (encrypted && client.database != null) { + if (encrypted && client.database != null && client.encryptionEnabled) { await client.database.transaction(() async { for (var i = 0; i < events.length; i++) { if (events[i].type == EventTypes.Encrypted && events[i].content['body'] == DecryptError.UNKNOWN_SESSION) { - await events[i].loadSession(); - events[i] = await events[i].decryptAndStore(); + events[i] = await client.encryption + .decryptRoomEvent(id, events[i], store: true); } } }); @@ -1745,209 +1550,10 @@ class Room { return deviceKeys; } - bool _restoredOutboundGroupSession = false; - - Future restoreOutboundGroupSession() async { - if (_restoredOutboundGroupSession || client.database == null) { - return; - } - final outboundSession = - await client.database.getDbOutboundGroupSession(client.id, id); - if (outboundSession != null) { - try { - _outboundGroupSession = olm.OutboundGroupSession(); - _outboundGroupSession.unpickle(client.userID, outboundSession.pickle); - _outboundGroupSessionDevices = - List.from(json.decode(outboundSession.deviceIds)); - _outboundGroupSessionCreationTime = outboundSession.creationTime; - _outboundGroupSessionSentMessages = outboundSession.sentMessages; - } catch (e) { - _outboundGroupSession = null; - _outboundGroupSessionDevices = null; - print('[LibOlm] Unable to unpickle outboundGroupSession: ' + - e.toString()); - } - } - _restoredOutboundGroupSession = true; - } - - /// Encrypts the given json payload and creates a send-ready m.room.encrypted - /// payload. This will create a new outgoingGroupSession if necessary. - Future> encryptGroupMessagePayload( - Map payload, - {String type = EventTypes.Message}) async { - if (!encrypted || !client.encryptionEnabled) return payload; - if (encryptionAlgorithm != 'm.megolm.v1.aes-sha2') { - throw ('Unknown encryption algorithm'); - } - if (!_restoredOutboundGroupSession && client.database != null) { - // try to restore an outbound group session from the database - await restoreOutboundGroupSession(); - } - // and clear the outbound session, if it needs clearing - await clearOutboundGroupSession(); - // create a new one if none exists... - if (_outboundGroupSession == null) { - await createOutboundGroupSession(); - } - final Map mRelatesTo = payload.remove('m.relates_to'); - final payloadContent = { - 'content': payload, - 'type': type, - 'room_id': id, - }; - var encryptedPayload = { - 'algorithm': 'm.megolm.v1.aes-sha2', - 'ciphertext': _outboundGroupSession.encrypt(json.encode(payloadContent)), - 'device_id': client.deviceID, - 'sender_key': client.identityKey, - 'session_id': _outboundGroupSession.session_id(), - if (mRelatesTo != null) 'm.relates_to': mRelatesTo, - }; - _outboundGroupSessionSentMessages++; - await _storeOutboundGroupSession(); - return encryptedPayload; - } - - final Set _requestedSessionIds = {}; - Future requestSessionKey(String sessionId, String senderKey) async { - await client.keyManager.request(this, sessionId, senderKey); - } - - Future loadInboundGroupSessionKey(String sessionId, - [String senderKey]) async { - if (sessionId == null || inboundGroupSessions.containsKey(sessionId)) { - return; - } // nothing to do - final session = await client.database - .getDbInboundGroupSession(client.id, id, sessionId); - if (session == null) { - // no session found, let's request it! - if (client.enableE2eeRecovery && - !_requestedSessionIds.contains(sessionId) && - senderKey != null) { - unawaited(requestSessionKey(sessionId, senderKey)); - _requestedSessionIds.add(sessionId); - } - return; - } - try { - _inboundGroupSessions[sessionId] = - SessionKey.fromDb(session, client.userID); - } catch (e) { - print('[LibOlm] Could not unpickle inboundGroupSession: ' + e.toString()); - } - } - - Future loadInboundGroupSessionKeyForEvent(Event event) async { - if (client.database == null) return; // nothing to do, no database - if (event.type != EventTypes.Encrypted) return; if (!client.encryptionEnabled) { - throw (DecryptError.NOT_ENABLED); + return; } - if (event.content['algorithm'] != 'm.megolm.v1.aes-sha2') { - throw (DecryptError.UNKNOWN_ALGORITHM); - } - final String sessionId = event.content['session_id']; - return loadInboundGroupSessionKey(sessionId, event.content['sender_key']); - } - - /// Decrypts the given [event] with one of the available ingoingGroupSessions. - /// Returns a m.bad.encrypted event if it fails and does nothing if the event - /// was not encrypted. - Event decryptGroupMessage(Event event) { - if (event.type != EventTypes.Encrypted || - event.content['ciphertext'] == null) return event; - Map decryptedPayload; - try { - if (!client.encryptionEnabled) { - throw (DecryptError.NOT_ENABLED); - } - if (event.content['algorithm'] != 'm.megolm.v1.aes-sha2') { - throw (DecryptError.UNKNOWN_ALGORITHM); - } - final String sessionId = event.content['session_id']; - if (!inboundGroupSessions.containsKey(sessionId)) { - throw (DecryptError.UNKNOWN_SESSION); - } - final decryptResult = inboundGroupSessions[sessionId] - .inboundGroupSession - .decrypt(event.content['ciphertext']); - final messageIndexKey = event.eventId + - event.originServerTs.millisecondsSinceEpoch.toString(); - if (inboundGroupSessions[sessionId] - .indexes - .containsKey(messageIndexKey) && - inboundGroupSessions[sessionId].indexes[messageIndexKey] != - decryptResult.message_index) { - if ((_outboundGroupSession?.session_id() ?? '') == sessionId) { - clearOutboundGroupSession(); - } - throw (DecryptError.CHANNEL_CORRUPTED); - } - inboundGroupSessions[sessionId].indexes[messageIndexKey] = - decryptResult.message_index; - // now we persist the udpated indexes into the database. - // the entry should always exist. In the case it doesn't, the following - // line *could* throw an error. As that is a future, though, and we call - // it un-awaited here, nothing happens, which is exactly the result we want - client.database?.updateInboundGroupSessionIndexes( - json.encode(inboundGroupSessions[sessionId].indexes), - client.id, - id, - sessionId); - decryptedPayload = json.decode(decryptResult.plaintext); - } catch (exception) { - // alright, if this was actually by our own outbound group session, we might as well clear it - if (client.enableE2eeRecovery && - (_outboundGroupSession?.session_id() ?? '') == - event.content['session_id']) { - clearOutboundGroupSession(wipe: true); - } - if (exception.toString() == DecryptError.UNKNOWN_SESSION) { - decryptedPayload = { - 'content': event.content, - 'type': EventTypes.Encrypted, - }; - decryptedPayload['content']['body'] = exception.toString(); - decryptedPayload['content']['msgtype'] = 'm.bad.encrypted'; - } else { - decryptedPayload = { - 'content': { - 'msgtype': 'm.bad.encrypted', - 'body': exception.toString(), - }, - 'type': EventTypes.Encrypted, - }; - } - } - if (event.content['m.relates_to'] != null) { - decryptedPayload['content']['m.relates_to'] = - event.content['m.relates_to']; - } - return Event( - content: decryptedPayload['content'], - type: decryptedPayload['type'], - senderId: event.senderId, - eventId: event.eventId, - roomId: event.roomId, - room: event.room, - originServerTs: event.originServerTs, - unsigned: event.unsigned, - stateKey: event.stateKey, - prevContent: event.prevContent, - status: event.status, - sortOrder: event.sortOrder, - ); + await client.encryption.keyManager.request(this, sessionId, senderKey); } } - -abstract class DecryptError { - static const String NOT_ENABLED = 'Encryption is not enabled in your client.'; - static const String UNKNOWN_ALGORITHM = 'Unknown encryption algorithm.'; - static const String UNKNOWN_SESSION = - 'The sender has not sent us the session key.'; - static const String CHANNEL_CORRUPTED = - 'The secure channel with the sender was corrupted.'; -} diff --git a/lib/src/timeline.dart b/lib/src/timeline.dart index b428665..e7497c1 100644 --- a/lib/src/timeline.dart +++ b/lib/src/timeline.dart @@ -19,6 +19,7 @@ import 'dart:async'; import 'package:famedlysdk/matrix_api.dart'; +import 'package:famedlysdk/encryption.dart'; import 'event.dart'; import 'room.dart'; @@ -97,12 +98,16 @@ class Timeline { void _sessionKeyReceived(String sessionId) async { var decryptAtLeastOneEvent = false; final decryptFn = () async { + if (!room.client.encryptionEnabled) { + return; + } for (var i = 0; i < events.length; i++) { if (events[i].type == EventTypes.Encrypted && events[i].messageType == MessageTypes.BadEncrypted && events[i].content['body'] == DecryptError.UNKNOWN_SESSION && events[i].content['session_id'] == sessionId) { - events[i] = await events[i].decryptAndStore(); + events[i] = await room.client.encryption + .decryptRoomEvent(room.id, events[i], store: true); if (events[i].type != EventTypes.Encrypted) { decryptAtLeastOneEvent = true; } diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index 29754b8..5fc0c85 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -1,11 +1,11 @@ import 'dart:convert'; import 'package:famedlysdk/matrix_api.dart'; +import 'package:famedlysdk/encryption.dart'; import '../client.dart'; import '../database/database.dart' show DbUserDeviceKey, DbUserDeviceKeysKey; import '../event.dart'; -import 'key_verification.dart'; class DeviceKeysList { String userId; @@ -78,12 +78,6 @@ class DeviceKeys extends MatrixDeviceKeys { Future setBlocked(bool newBlocked, Client client) { blocked = newBlocked; - for (var room in client.rooms) { - if (!room.encrypted) continue; - if (room.getParticipants().indexWhere((u) => u.id == userId) != -1) { - room.clearOutboundGroupSession(); - } - } return client.database ?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId); } @@ -157,10 +151,10 @@ class DeviceKeys extends MatrixDeviceKeys { } KeyVerification startVerification(Client client) { - final request = - KeyVerification(client: client, userId: userId, deviceId: deviceId); + final request = KeyVerification( + encryption: client.encryption, userId: userId, deviceId: deviceId); request.start(); - client.addKeyVerificationRequest(request); + client.encryption.keyVerificationManager.addRequest(request); return request; } } diff --git a/lib/src/utils/event_update.dart b/lib/src/utils/event_update.dart index 758fa92..0be1c4e 100644 --- a/lib/src/utils/event_update.dart +++ b/lib/src/utils/event_update.dart @@ -42,13 +42,14 @@ class EventUpdate { EventUpdate( {this.eventType, this.roomID, this.type, this.content, this.sortOrder}); - EventUpdate decrypt(Room room) { - if (eventType != EventTypes.Encrypted) { + Future decrypt(Room room, {bool store = false}) async { + if (eventType != EventTypes.Encrypted || !room.client.encryptionEnabled) { return this; } try { - var decrpytedEvent = - room.decryptGroupMessage(Event.fromJson(content, room, sortOrder)); + var decrpytedEvent = await room.client.encryption.decryptRoomEvent( + room.id, Event.fromJson(content, room, sortOrder), + store: store, updateType: type); return EventUpdate( eventType: decrpytedEvent.type, roomID: roomID, diff --git a/lib/src/utils/session_key.dart b/lib/src/utils/session_key.dart deleted file mode 100644 index 2155b21..0000000 --- a/lib/src/utils/session_key.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'dart:convert'; - -import 'package:olm/olm.dart'; - -import '../database/database.dart' show DbInboundGroupSession; -import '../event.dart'; - -class SessionKey { - Map content; - Map indexes; - InboundGroupSession inboundGroupSession; - final String key; - List get forwardingCurve25519KeyChain => - content['forwarding_curve25519_key_chain'] ?? []; - String get senderClaimedEd25519Key => - content['sender_claimed_ed25519_key'] ?? ''; - - SessionKey({this.content, this.inboundGroupSession, this.key, this.indexes}); - - SessionKey.fromDb(DbInboundGroupSession dbEntry, String key) : key = key { - final parsedContent = Event.getMapFromPayload(dbEntry.content); - final parsedIndexes = Event.getMapFromPayload(dbEntry.indexes); - content = - parsedContent != null ? Map.from(parsedContent) : null; - indexes = parsedIndexes != null - ? Map.from(parsedIndexes) - : {}; - var newInboundGroupSession = InboundGroupSession(); - newInboundGroupSession.unpickle(key, dbEntry.pickle); - inboundGroupSession = newInboundGroupSession; - } - - SessionKey.fromJson(Map json, String key) : key = key { - content = json['content'] != null - ? Map.from(json['content']) - : null; - indexes = json['indexes'] != null - ? Map.from(json['indexes']) - : {}; - var newInboundGroupSession = InboundGroupSession(); - newInboundGroupSession.unpickle(key, json['inboundGroupSession']); - inboundGroupSession = newInboundGroupSession; - } - - Map toJson() { - final data = {}; - if (content != null) { - data['content'] = content; - } - if (indexes != null) { - data['indexes'] = indexes; - } - data['inboundGroupSession'] = inboundGroupSession.pickle(key); - return data; - } - - @override - String toString() => json.encode(toJson()); -} diff --git a/test/client_test.dart b/test/client_test.dart index 2232c15..09bef10 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -17,7 +17,6 @@ */ import 'dart:async'; -import 'dart:convert'; import 'dart:typed_data'; import 'package:famedlysdk/famedlysdk.dart'; @@ -134,24 +133,6 @@ void main() { expect(matrix.directChats, matrix.accountData['m.direct'].content); expect(matrix.presences.length, 1); expect(matrix.rooms[1].ephemerals.length, 2); - expect(matrix.rooms[1].inboundGroupSessions.length, 1); - expect( - matrix - .rooms[1] - .inboundGroupSessions[ - 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU'] - .content['session_key'], - 'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw'); - if (olmEnabled) { - expect( - matrix - .rooms[1] - .inboundGroupSessions[ - 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU'] - .inboundGroupSession != - null, - true); - } expect(matrix.rooms[1].typingUsers.length, 1); expect(matrix.rooms[1].typingUsers[0].id, '@alice:example.com'); expect(matrix.rooms[1].roomAccountData.length, 3); @@ -388,115 +369,6 @@ void main() { 'mxc://example.org/SEsfnsuifSDFSSEF'); expect(aliceProfile.displayname, 'Alice Margatroid'); }); - - test('signJson', () { - if (matrix.encryptionEnabled) { - expect(matrix.fingerprintKey.isNotEmpty, true); - expect(matrix.identityKey.isNotEmpty, true); - var payload = { - 'unsigned': { - 'foo': 'bar', - }, - 'auth': { - 'success': true, - 'mxid': '@john.doe:example.com', - 'profile': { - 'display_name': 'John Doe', - 'three_pids': [ - {'medium': 'email', 'address': 'john.doe@example.org'}, - {'medium': 'msisdn', 'address': '123456789'} - ] - } - } - }; - var payloadWithoutUnsigned = Map.from(payload); - payloadWithoutUnsigned.remove('unsigned'); - - expect( - matrix.checkJsonSignature( - matrix.fingerprintKey, payload, matrix.userID, matrix.deviceID), - false); - expect( - matrix.checkJsonSignature(matrix.fingerprintKey, - payloadWithoutUnsigned, matrix.userID, matrix.deviceID), - false); - payload = matrix.signJson(payload); - payloadWithoutUnsigned = matrix.signJson(payloadWithoutUnsigned); - expect(payload['signatures'], payloadWithoutUnsigned['signatures']); - print(payload['signatures']); - expect( - matrix.checkJsonSignature( - matrix.fingerprintKey, payload, matrix.userID, matrix.deviceID), - true); - expect( - matrix.checkJsonSignature(matrix.fingerprintKey, - payloadWithoutUnsigned, matrix.userID, matrix.deviceID), - true); - } - }); - test('Track oneTimeKeys', () async { - if (matrix.encryptionEnabled) { - var last = matrix.lastTimeKeysUploaded ?? DateTime.now(); - await matrix.handleSync(SyncUpdate.fromJson({ - 'device_one_time_keys_count': {'signed_curve25519': 49} - })); - await Future.delayed(Duration(milliseconds: 50)); - expect( - matrix.lastTimeKeysUploaded.millisecondsSinceEpoch > - last.millisecondsSinceEpoch, - true); - } - }); - test('Test invalidate outboundGroupSessions', () async { - if (matrix.encryptionEnabled) { - expect(matrix.rooms[1].outboundGroupSession == null, true); - await matrix.rooms[1].createOutboundGroupSession(); - expect(matrix.rooms[1].outboundGroupSession != null, true); - await matrix.handleSync(SyncUpdate.fromJson({ - 'device_lists': { - 'changed': [ - '@alice:example.com', - ], - 'left': [ - '@bob:example.com', - ], - } - })); - await Future.delayed(Duration(milliseconds: 50)); - expect(matrix.rooms[1].outboundGroupSession != null, true); - } - }); - test('Test invalidate outboundGroupSessions', () async { - if (matrix.encryptionEnabled) { - await matrix.rooms[1].clearOutboundGroupSession(wipe: true); - expect(matrix.rooms[1].outboundGroupSession == null, true); - await matrix.rooms[1].createOutboundGroupSession(); - expect(matrix.rooms[1].outboundGroupSession != null, true); - await matrix.handleSync(SyncUpdate.fromJson({ - 'rooms': { - 'join': { - '!726s6s6q:example.com': { - 'state': { - 'events': [ - { - 'content': {'membership': 'leave'}, - 'event_id': '143273582443PhrSn:example.org', - 'origin_server_ts': 1432735824653, - 'room_id': '!726s6s6q:example.com', - 'sender': '@alice:example.com', - 'state_key': '@alice:example.com', - 'type': 'm.room.member' - } - ] - } - } - } - } - })); - await Future.delayed(Duration(milliseconds: 50)); - expect(matrix.rooms[1].outboundGroupSession != null, true); - } - }); var deviceKeys = DeviceKeys.fromJson({ 'user_id': '@alice:example.com', 'device_id': 'JLAFKJWSCS', @@ -512,16 +384,6 @@ void main() { } } }); - test('startOutgoingOlmSessions', () async { - expect(matrix.olmSessions.length, 0); - if (olmEnabled) { - await matrix - .startOutgoingOlmSessions([deviceKeys], checkSignature: false); - expect(matrix.olmSessions.length, 1); - expect(matrix.olmSessions.entries.first.key, - '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI'); - } - }); test('sendToDevice', () async { await matrix.sendToDevice( [deviceKeys], @@ -547,13 +409,6 @@ void main() { await Future.delayed(Duration(milliseconds: 50)); - String sessionKey; - if (client1.encryptionEnabled) { - await client1.rooms[1].createOutboundGroupSession(); - - sessionKey = client1.rooms[1].outboundGroupSession.session_key(); - } - expect(client1.isLogged(), true); expect(client1.rooms.length, 2); @@ -571,12 +426,9 @@ void main() { expect(client2.deviceID, client1.deviceID); expect(client2.deviceName, client1.deviceName); if (client2.encryptionEnabled) { - await client2.rooms[1].restoreOutboundGroupSession(); - expect(client2.pickledOlmAccount, client1.pickledOlmAccount); - expect(json.encode(client2.rooms[1].inboundGroupSessions[sessionKey]), - json.encode(client1.rooms[1].inboundGroupSessions[sessionKey])); + expect(client2.encryption.pickledOlmAccount, + client1.encryption.pickledOlmAccount); expect(client2.rooms[1].id, client1.rooms[1].id); - expect(client2.rooms[1].outboundGroupSession.session_key(), sessionKey); } await client1.logout(); diff --git a/test/encryption/key_request_test.dart b/test/encryption/key_request_test.dart new file mode 100644 index 0000000..243f776 --- /dev/null +++ b/test/encryption/key_request_test.dart @@ -0,0 +1,342 @@ +/* + * Ansible inventory script used at Famedly GmbH for managing many hosts + * Copyright (C) 2019, 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:famedlysdk/famedlysdk.dart'; +import 'package:test/test.dart'; + +import '../fake_matrix_api.dart'; +import '../fake_database.dart'; + +Map jsonDecode(dynamic payload) { + if (payload is String) { + try { + return json.decode(payload); + } catch (e) { + return {}; + } + } + if (payload is Map) return payload; + return {}; +} + +void main() { + /// All Tests related to device keys + group('Key Request', () { + final validSessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU'; + final validSenderKey = '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI'; + test('Create Request', () async { + var matrix = + Client('testclient', debug: true, httpClient: FakeMatrixApi()); + matrix.database = getDatabase(); + await matrix.checkServer('https://fakeServer.notExisting'); + await matrix.login('test', '1234'); + if (!matrix.encryptionEnabled) { + await matrix.dispose(closeDatabase: true); + return; + } + final requestRoom = matrix.getRoomById('!726s6s6q:example.com'); + await matrix.encryption.keyManager + .request(requestRoom, 'sessionId', validSenderKey); + var foundEvent = false; + for (var entry in FakeMatrixApi.calledEndpoints.entries) { + final payload = jsonDecode(entry.value.first); + if (entry.key + .startsWith('/client/r0/sendToDevice/m.room_key_request') && + (payload['messages'] is Map) && + (payload['messages']['@alice:example.com'] is Map) && + (payload['messages']['@alice:example.com']['*'] is Map)) { + final content = payload['messages']['@alice:example.com']['*']; + if (content['action'] == 'request' && + content['body']['room_id'] == '!726s6s6q:example.com' && + content['body']['sender_key'] == validSenderKey && + content['body']['session_id'] == 'sessionId') { + foundEvent = true; + break; + } + } + } + expect(foundEvent, true); + await matrix.dispose(closeDatabase: true); + }); + test('Reply To Request', () async { + var matrix = + Client('testclient', debug: true, httpClient: FakeMatrixApi()); + matrix.database = getDatabase(); + await matrix.checkServer('https://fakeServer.notExisting'); + await matrix.login('test', '1234'); + if (!matrix.encryptionEnabled) { + await matrix.dispose(closeDatabase: true); + return; + } + matrix.setUserId('@alice:example.com'); // we need to pretend to be alice + FakeMatrixApi.calledEndpoints.clear(); + await matrix + .userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE'] + .setBlocked(false, matrix); + await matrix + .userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE'] + .setVerified(true, matrix); + // test a successful share + var event = ToDeviceEvent( + sender: '@alice:example.com', + type: 'm.room_key_request', + content: { + 'action': 'request', + 'body': { + 'algorithm': 'm.megolm.v1.aes-sha2', + 'room_id': '!726s6s6q:example.com', + 'sender_key': validSenderKey, + 'session_id': validSessionId, + }, + 'request_id': 'request_1', + 'requesting_device_id': 'OTHERDEVICE', + }); + await matrix.encryption.keyManager.handleToDeviceEvent(event); + print(FakeMatrixApi.calledEndpoints.keys.toString()); + expect( + FakeMatrixApi.calledEndpoints.keys.any( + (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), + true); + + // test various fail scenarios + + // no body + FakeMatrixApi.calledEndpoints.clear(); + event = ToDeviceEvent( + sender: '@alice:example.com', + type: 'm.room_key_request', + content: { + 'action': 'request', + 'request_id': 'request_2', + 'requesting_device_id': 'OTHERDEVICE', + }); + await matrix.encryption.keyManager.handleToDeviceEvent(event); + expect( + FakeMatrixApi.calledEndpoints.keys.any( + (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), + false); + + // request by ourself + FakeMatrixApi.calledEndpoints.clear(); + event = ToDeviceEvent( + sender: '@alice:example.com', + type: 'm.room_key_request', + content: { + 'action': 'request', + 'body': { + 'algorithm': 'm.megolm.v1.aes-sha2', + 'room_id': '!726s6s6q:example.com', + 'sender_key': validSenderKey, + 'session_id': validSessionId, + }, + 'request_id': 'request_3', + 'requesting_device_id': 'JLAFKJWSCS', + }); + await matrix.encryption.keyManager.handleToDeviceEvent(event); + expect( + FakeMatrixApi.calledEndpoints.keys.any( + (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), + false); + + // device not found + FakeMatrixApi.calledEndpoints.clear(); + event = ToDeviceEvent( + sender: '@alice:example.com', + type: 'm.room_key_request', + content: { + 'action': 'request', + 'body': { + 'algorithm': 'm.megolm.v1.aes-sha2', + 'room_id': '!726s6s6q:example.com', + 'sender_key': validSenderKey, + 'session_id': validSessionId, + }, + 'request_id': 'request_4', + 'requesting_device_id': 'blubb', + }); + await matrix.encryption.keyManager.handleToDeviceEvent(event); + expect( + FakeMatrixApi.calledEndpoints.keys.any( + (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), + false); + + // unknown room + FakeMatrixApi.calledEndpoints.clear(); + event = ToDeviceEvent( + sender: '@alice:example.com', + type: 'm.room_key_request', + content: { + 'action': 'request', + 'body': { + 'algorithm': 'm.megolm.v1.aes-sha2', + 'room_id': '!invalid:example.com', + 'sender_key': validSenderKey, + 'session_id': validSessionId, + }, + 'request_id': 'request_5', + 'requesting_device_id': 'OTHERDEVICE', + }); + await matrix.encryption.keyManager.handleToDeviceEvent(event); + expect( + FakeMatrixApi.calledEndpoints.keys.any( + (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), + false); + + // unknwon session + FakeMatrixApi.calledEndpoints.clear(); + event = ToDeviceEvent( + sender: '@alice:example.com', + type: 'm.room_key_request', + content: { + 'action': 'request', + 'body': { + 'algorithm': 'm.megolm.v1.aes-sha2', + 'room_id': '!726s6s6q:example.com', + 'sender_key': validSenderKey, + 'session_id': 'invalid', + }, + 'request_id': 'request_6', + 'requesting_device_id': 'OTHERDEVICE', + }); + await matrix.encryption.keyManager.handleToDeviceEvent(event); + expect( + FakeMatrixApi.calledEndpoints.keys.any( + (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), + false); + + FakeMatrixApi.calledEndpoints.clear(); + await matrix.dispose(closeDatabase: true); + }); + test('Receive shared keys', () async { + var matrix = + Client('testclient', debug: true, httpClient: FakeMatrixApi()); + matrix.database = getDatabase(); + await matrix.checkServer('https://fakeServer.notExisting'); + await matrix.login('test', '1234'); + if (!matrix.encryptionEnabled) { + await matrix.dispose(closeDatabase: true); + return; + } + final requestRoom = matrix.getRoomById('!726s6s6q:example.com'); + await matrix.encryption.keyManager + .request(requestRoom, validSessionId, validSenderKey); + + final session = await matrix.encryption.keyManager + .loadInboundGroupSession( + requestRoom.id, validSessionId, validSenderKey); + final sessionKey = session.inboundGroupSession + .export_session(session.inboundGroupSession.first_known_index()); + matrix.encryption.keyManager.clearInboundGroupSessions(); + var event = ToDeviceEvent( + sender: '@alice:example.com', + type: 'm.forwarded_room_key', + content: { + 'algorithm': 'm.megolm.v1.aes-sha2', + 'room_id': '!726s6s6q:example.com', + 'session_id': validSessionId, + 'session_key': sessionKey, + 'sender_key': validSenderKey, + 'forwarding_curve25519_key_chain': [], + }, + encryptedContent: { + 'sender_key': '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI', + }); + await matrix.encryption.keyManager.handleToDeviceEvent(event); + expect( + matrix.encryption.keyManager.getInboundGroupSession( + requestRoom.id, validSessionId, validSenderKey) != + null, + true); + + // now test a few invalid scenarios + + // request not found + matrix.encryption.keyManager.clearInboundGroupSessions(); + event = ToDeviceEvent( + sender: '@alice:example.com', + type: 'm.forwarded_room_key', + content: { + 'algorithm': 'm.megolm.v1.aes-sha2', + 'room_id': '!726s6s6q:example.com', + 'session_id': validSessionId, + 'session_key': sessionKey, + 'sender_key': validSenderKey, + 'forwarding_curve25519_key_chain': [], + }, + encryptedContent: { + 'sender_key': '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI', + }); + await matrix.encryption.keyManager.handleToDeviceEvent(event); + expect( + matrix.encryption.keyManager.getInboundGroupSession( + requestRoom.id, validSessionId, validSenderKey) != + null, + false); + + // unknown device + await matrix.encryption.keyManager + .request(requestRoom, validSessionId, validSenderKey); + matrix.encryption.keyManager.clearInboundGroupSessions(); + event = ToDeviceEvent( + sender: '@alice:example.com', + type: 'm.forwarded_room_key', + content: { + 'algorithm': 'm.megolm.v1.aes-sha2', + 'room_id': '!726s6s6q:example.com', + 'session_id': validSessionId, + 'session_key': sessionKey, + 'sender_key': validSenderKey, + 'forwarding_curve25519_key_chain': [], + }, + encryptedContent: { + 'sender_key': 'invalid', + }); + await matrix.encryption.keyManager.handleToDeviceEvent(event); + expect( + matrix.encryption.keyManager.getInboundGroupSession( + requestRoom.id, validSessionId, validSenderKey) != + null, + false); + + // no encrypted content + await matrix.encryption.keyManager + .request(requestRoom, validSessionId, validSenderKey); + matrix.encryption.keyManager.clearInboundGroupSessions(); + event = ToDeviceEvent( + sender: '@alice:example.com', + type: 'm.forwarded_room_key', + content: { + 'algorithm': 'm.megolm.v1.aes-sha2', + 'room_id': '!726s6s6q:example.com', + 'session_id': validSessionId, + 'session_key': sessionKey, + 'sender_key': validSenderKey, + 'forwarding_curve25519_key_chain': [], + }); + await matrix.encryption.keyManager.handleToDeviceEvent(event); + expect( + matrix.encryption.keyManager.getInboundGroupSession( + requestRoom.id, validSessionId, validSenderKey) != + null, + false); + + await matrix.dispose(closeDatabase: true); + }); + }); +} diff --git a/test/key_verification_test.dart b/test/encryption/key_verification_test.dart similarity index 83% rename from test/key_verification_test.dart rename to test/encryption/key_verification_test.dart index 0594bd2..039a38f 100644 --- a/test/key_verification_test.dart +++ b/test/encryption/key_verification_test.dart @@ -17,10 +17,12 @@ */ import 'package:famedlysdk/famedlysdk.dart'; +import 'package:famedlysdk/encryption.dart'; import 'package:test/test.dart'; import 'package:olm/olm.dart' as olm; -import 'fake_matrix_api.dart'; +import '../fake_matrix_api.dart'; +import '../fake_database.dart'; void main() { /// All Tests related to the ChatTime @@ -36,19 +38,25 @@ void main() { print('[LibOlm] Enabled: $olmEnabled'); var client = Client('testclient', debug: true, httpClient: FakeMatrixApi()); - client.api.homeserver = Uri.parse('https://fakeserver.notexisting'); var room = Room(id: '!localpart:server.abc', client: client); var updateCounter = 0; - final keyVerification = KeyVerification( - client: client, - room: room, - userId: '@alice:example.com', - deviceId: 'ABCD', - onUpdate: () => updateCounter++, - ); + KeyVerification keyVerification; if (!olmEnabled) return; + test('setupClient', () async { + client.database = getDatabase(); + await client.checkServer('https://fakeServer.notExisting'); + await client.login('test', '1234'); + keyVerification = KeyVerification( + encryption: client.encryption, + room: room, + userId: '@alice:example.com', + deviceId: 'ABCD', + onUpdate: () => updateCounter++, + ); + }); + test('acceptSas', () async { await keyVerification.acceptSas(); }); @@ -91,7 +99,7 @@ void main() { test('verifyActivity', () async { final verified = await keyVerification.verifyActivity(); expect(verified, true); + keyVerification?.dispose(); }); - keyVerification.dispose(); }); } diff --git a/test/event_test.dart b/test/event_test.dart index ea1539b..26028a2 100644 --- a/test/event_test.dart +++ b/test/event_test.dart @@ -20,6 +20,7 @@ import 'dart:convert'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/matrix_api.dart'; +import 'package:famedlysdk/encryption.dart'; import 'package:famedlysdk/src/event.dart'; import 'package:test/test.dart'; diff --git a/test/room_key_request_test.dart b/test/room_key_request_test.dart deleted file mode 100644 index 6801c62..0000000 --- a/test/room_key_request_test.dart +++ /dev/null @@ -1,367 +0,0 @@ -/* - * Ansible inventory script used at Famedly GmbH for managing many hosts - * Copyright (C) 2019, 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:famedlysdk/famedlysdk.dart'; -import 'package:test/test.dart'; - -import 'fake_matrix_api.dart'; -import 'fake_database.dart'; - -Map jsonDecode(dynamic payload) { - if (payload is String) { - try { - return json.decode(payload); - } catch (e) { - return {}; - } - } - if (payload is Map) return payload; - return {}; -} - -void main() { - /// All Tests related to device keys - test('fromJson', () async { - var rawJson = { - 'content': { - 'action': 'request', - 'body': { - 'algorithm': 'm.megolm.v1.aes-sha2', - 'room_id': '!726s6s6q:example.com', - 'sender_key': 'RF3s+E7RkTQTGF2d8Deol0FkQvgII2aJDf3/Jp5mxVU', - 'session_id': 'X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ' - }, - 'request_id': '1495474790150.19', - 'requesting_device_id': 'JLAFKJWSCS' - }, - 'type': 'm.room_key_request', - 'sender': '@alice:example.com' - }; - var toDeviceEvent = ToDeviceEvent.fromJson(rawJson); - expect(toDeviceEvent.content, rawJson['content']); - expect(toDeviceEvent.sender, rawJson['sender']); - expect(toDeviceEvent.type, rawJson['type']); - expect( - ToDeviceEventDecryptionError( - exception: Exception('test'), - stackTrace: null, - toDeviceEvent: toDeviceEvent) - .sender, - rawJson['sender'], - ); - - var matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi()); - matrix.database = getDatabase(); - await matrix.checkServer('https://fakeServer.notExisting'); - await matrix.login('test', '1234'); - var room = matrix.getRoomById('!726s6s6q:example.com'); - if (matrix.encryptionEnabled) { - await room.createOutboundGroupSession(); - rawJson['content']['body']['session_id'] = - room.inboundGroupSessions.keys.first; - - var roomKeyRequest = RoomKeyRequest.fromToDeviceEvent( - ToDeviceEvent.fromJson(rawJson), - matrix.keyManager, - KeyManagerKeyShareRequest( - room: room, - sessionId: rawJson['content']['body']['session_id'], - senderKey: rawJson['content']['body']['sender_key'], - devices: [ - matrix.userDeviceKeys[rawJson['sender']] - .deviceKeys[rawJson['content']['requesting_device_id']] - ], - )); - await roomKeyRequest.forwardKey(); - } - await matrix.dispose(closeDatabase: true); - }); - test('Create Request', () async { - var matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi()); - matrix.database = getDatabase(); - await matrix.checkServer('https://fakeServer.notExisting'); - await matrix.login('test', '1234'); - if (!matrix.encryptionEnabled) { - await matrix.dispose(closeDatabase: true); - return; - } - final requestRoom = matrix.getRoomById('!726s6s6q:example.com'); - await matrix.keyManager.request(requestRoom, 'sessionId', 'senderKey'); - var foundEvent = false; - for (var entry in FakeMatrixApi.calledEndpoints.entries) { - final payload = jsonDecode(entry.value.first); - if (entry.key.startsWith('/client/r0/sendToDevice/m.room_key_request') && - (payload['messages'] is Map) && - (payload['messages']['@alice:example.com'] is Map) && - (payload['messages']['@alice:example.com']['*'] is Map)) { - final content = payload['messages']['@alice:example.com']['*']; - if (content['action'] == 'request' && - content['body']['room_id'] == '!726s6s6q:example.com' && - content['body']['sender_key'] == 'senderKey' && - content['body']['session_id'] == 'sessionId') { - foundEvent = true; - break; - } - } - } - expect(foundEvent, true); - await matrix.dispose(closeDatabase: true); - }); - final validSessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU'; - test('Reply To Request', () async { - var matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi()); - matrix.database = getDatabase(); - await matrix.checkServer('https://fakeServer.notExisting'); - await matrix.login('test', '1234'); - if (!matrix.encryptionEnabled) { - await matrix.dispose(closeDatabase: true); - return; - } - matrix.setUserId('@alice:example.com'); // we need to pretend to be alice - FakeMatrixApi.calledEndpoints.clear(); - await matrix.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE'] - .setBlocked(false, matrix); - await matrix.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE'] - .setVerified(true, matrix); - await matrix.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE'] - .startVerification(matrix); - // test a successful share - var event = ToDeviceEvent( - sender: '@alice:example.com', - type: 'm.room_key_request', - content: { - 'action': 'request', - 'body': { - 'algorithm': 'm.megolm.v1.aes-sha2', - 'room_id': '!726s6s6q:example.com', - 'sender_key': 'senderKey', - 'session_id': validSessionId, - }, - 'request_id': 'request_1', - 'requesting_device_id': 'OTHERDEVICE', - }); - await matrix.keyManager.handleToDeviceEvent(event); - expect( - FakeMatrixApi.calledEndpoints.keys.any( - (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), - true); - - // test various fail scenarios - - // no body - FakeMatrixApi.calledEndpoints.clear(); - event = ToDeviceEvent( - sender: '@alice:example.com', - type: 'm.room_key_request', - content: { - 'action': 'request', - 'request_id': 'request_2', - 'requesting_device_id': 'OTHERDEVICE', - }); - await matrix.keyManager.handleToDeviceEvent(event); - expect( - FakeMatrixApi.calledEndpoints.keys.any( - (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), - false); - - // request by ourself - FakeMatrixApi.calledEndpoints.clear(); - event = ToDeviceEvent( - sender: '@alice:example.com', - type: 'm.room_key_request', - content: { - 'action': 'request', - 'body': { - 'algorithm': 'm.megolm.v1.aes-sha2', - 'room_id': '!726s6s6q:example.com', - 'sender_key': 'senderKey', - 'session_id': validSessionId, - }, - 'request_id': 'request_3', - 'requesting_device_id': 'JLAFKJWSCS', - }); - await matrix.keyManager.handleToDeviceEvent(event); - expect( - FakeMatrixApi.calledEndpoints.keys.any( - (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), - false); - - // device not found - FakeMatrixApi.calledEndpoints.clear(); - event = ToDeviceEvent( - sender: '@alice:example.com', - type: 'm.room_key_request', - content: { - 'action': 'request', - 'body': { - 'algorithm': 'm.megolm.v1.aes-sha2', - 'room_id': '!726s6s6q:example.com', - 'sender_key': 'senderKey', - 'session_id': validSessionId, - }, - 'request_id': 'request_4', - 'requesting_device_id': 'blubb', - }); - await matrix.keyManager.handleToDeviceEvent(event); - expect( - FakeMatrixApi.calledEndpoints.keys.any( - (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), - false); - - // unknown room - FakeMatrixApi.calledEndpoints.clear(); - event = ToDeviceEvent( - sender: '@alice:example.com', - type: 'm.room_key_request', - content: { - 'action': 'request', - 'body': { - 'algorithm': 'm.megolm.v1.aes-sha2', - 'room_id': '!invalid:example.com', - 'sender_key': 'senderKey', - 'session_id': validSessionId, - }, - 'request_id': 'request_5', - 'requesting_device_id': 'OTHERDEVICE', - }); - await matrix.keyManager.handleToDeviceEvent(event); - expect( - FakeMatrixApi.calledEndpoints.keys.any( - (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), - false); - - // unknwon session - FakeMatrixApi.calledEndpoints.clear(); - event = ToDeviceEvent( - sender: '@alice:example.com', - type: 'm.room_key_request', - content: { - 'action': 'request', - 'body': { - 'algorithm': 'm.megolm.v1.aes-sha2', - 'room_id': '!726s6s6q:example.com', - 'sender_key': 'senderKey', - 'session_id': 'invalid', - }, - 'request_id': 'request_6', - 'requesting_device_id': 'OTHERDEVICE', - }); - await matrix.keyManager.handleToDeviceEvent(event); - expect( - FakeMatrixApi.calledEndpoints.keys.any( - (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')), - false); - - FakeMatrixApi.calledEndpoints.clear(); - await matrix.dispose(closeDatabase: true); - }); - test('Receive shared keys', () async { - var matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi()); - matrix.database = getDatabase(); - await matrix.checkServer('https://fakeServer.notExisting'); - await matrix.login('test', '1234'); - if (!matrix.encryptionEnabled) { - await matrix.dispose(closeDatabase: true); - return; - } - final requestRoom = matrix.getRoomById('!726s6s6q:example.com'); - await matrix.keyManager.request(requestRoom, validSessionId, 'senderKey'); - - final session = requestRoom.inboundGroupSessions[validSessionId]; - final sessionKey = session.inboundGroupSession - .export_session(session.inboundGroupSession.first_known_index()); - requestRoom.inboundGroupSessions.clear(); - var event = ToDeviceEvent( - sender: '@alice:example.com', - type: 'm.forwarded_room_key', - content: { - 'algorithm': 'm.megolm.v1.aes-sha2', - 'room_id': '!726s6s6q:example.com', - 'session_id': validSessionId, - 'session_key': sessionKey, - 'sender_key': 'senderKey', - 'forwarding_curve25519_key_chain': [], - }, - encryptedContent: { - 'sender_key': '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI', - }); - await matrix.keyManager.handleToDeviceEvent(event); - expect(requestRoom.inboundGroupSessions.containsKey(validSessionId), true); - - // now test a few invalid scenarios - - // request not found - requestRoom.inboundGroupSessions.clear(); - event = ToDeviceEvent( - sender: '@alice:example.com', - type: 'm.forwarded_room_key', - content: { - 'algorithm': 'm.megolm.v1.aes-sha2', - 'room_id': '!726s6s6q:example.com', - 'session_id': validSessionId, - 'session_key': sessionKey, - 'sender_key': 'senderKey', - 'forwarding_curve25519_key_chain': [], - }, - encryptedContent: { - 'sender_key': '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI', - }); - await matrix.keyManager.handleToDeviceEvent(event); - expect(requestRoom.inboundGroupSessions.containsKey(validSessionId), false); - - // unknown device - await matrix.keyManager.request(requestRoom, validSessionId, 'senderKey'); - requestRoom.inboundGroupSessions.clear(); - event = ToDeviceEvent( - sender: '@alice:example.com', - type: 'm.forwarded_room_key', - content: { - 'algorithm': 'm.megolm.v1.aes-sha2', - 'room_id': '!726s6s6q:example.com', - 'session_id': validSessionId, - 'session_key': sessionKey, - 'sender_key': 'senderKey', - 'forwarding_curve25519_key_chain': [], - }, - encryptedContent: { - 'sender_key': 'invalid', - }); - await matrix.keyManager.handleToDeviceEvent(event); - expect(requestRoom.inboundGroupSessions.containsKey(validSessionId), false); - - // no encrypted content - await matrix.keyManager.request(requestRoom, validSessionId, 'senderKey'); - requestRoom.inboundGroupSessions.clear(); - event = ToDeviceEvent( - sender: '@alice:example.com', - type: 'm.forwarded_room_key', - content: { - 'algorithm': 'm.megolm.v1.aes-sha2', - 'room_id': '!726s6s6q:example.com', - 'session_id': validSessionId, - 'session_key': sessionKey, - 'sender_key': 'senderKey', - 'forwarding_curve25519_key_chain': [], - }); - await matrix.keyManager.handleToDeviceEvent(event); - expect(requestRoom.inboundGroupSessions.containsKey(validSessionId), false); - - await matrix.dispose(closeDatabase: true); - }); -} diff --git a/test/room_test.dart b/test/room_test.dart index 2adcb98..11300c6 100644 --- a/test/room_test.dart +++ b/test/room_test.dart @@ -315,7 +315,7 @@ void main() { test('getTimeline', () async { final timeline = await room.getTimeline(); - expect(timeline.events, []); + expect(timeline.events.length, 1); }); test('getUserByMXID', () async { @@ -388,60 +388,6 @@ void main() { ); expect(room.encrypted, true); expect(room.encryptionAlgorithm, 'm.megolm.v1.aes-sha2'); - expect(room.outboundGroupSession, null); - }); - - test('createOutboundGroupSession', () async { - if (!room.client.encryptionEnabled) return; - await room.createOutboundGroupSession(); - expect(room.outboundGroupSession != null, true); - expect(room.outboundGroupSession.session_id().isNotEmpty, true); - expect( - room.inboundGroupSessions - .containsKey(room.outboundGroupSession.session_id()), - true); - expect( - room.inboundGroupSessions[room.outboundGroupSession.session_id()] - .content['session_key'], - room.outboundGroupSession.session_key()); - expect( - room.inboundGroupSessions[room.outboundGroupSession.session_id()] - .indexes.length, - 0); - }); - - test('clearOutboundGroupSession', () async { - if (!room.client.encryptionEnabled) return; - await room.clearOutboundGroupSession(wipe: true); - expect(room.outboundGroupSession == null, true); - }); - - test('encryptGroupMessagePayload and decryptGroupMessage', () async { - if (!room.client.encryptionEnabled) return; - final payload = { - 'msgtype': 'm.text', - 'body': 'Hello world', - }; - final encryptedPayload = await room.encryptGroupMessagePayload(payload); - expect(encryptedPayload['algorithm'], 'm.megolm.v1.aes-sha2'); - expect(encryptedPayload['ciphertext'].isNotEmpty, true); - expect(encryptedPayload['device_id'], room.client.deviceID); - expect(encryptedPayload['sender_key'], room.client.identityKey); - expect(encryptedPayload['session_id'], - room.outboundGroupSession.session_id()); - - var encryptedEvent = Event( - content: encryptedPayload, - type: 'm.room.encrypted', - senderId: room.client.userID, - eventId: '1234', - roomId: room.id, - room: room, - originServerTs: DateTime.now(), - ); - var decryptedEvent = room.decryptGroupMessage(encryptedEvent); - expect(decryptedEvent.type, 'm.room.message'); - expect(decryptedEvent.content, payload); }); test('setPushRuleState', () async { diff --git a/test/session_key_test.dart b/test/session_key_test.dart deleted file mode 100644 index 9540cae..0000000 --- a/test/session_key_test.dart +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Ansible inventory script used at Famedly GmbH for managing many hosts - * Copyright (C) 2019, 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:famedlysdk/src/utils/session_key.dart'; -import 'package:olm/olm.dart' as olm; -import 'package:test/test.dart'; - -void main() { - /// All Tests related to the ChatTime - group('SessionKey', () { - var olmEnabled = true; - try { - olm.init(); - olm.Account(); - } catch (_) { - olmEnabled = false; - print('[LibOlm] Failed to load LibOlm: ' + _.toString()); - } - print('[LibOlm] Enabled: $olmEnabled'); - test('SessionKey test', () { - if (olmEnabled) { - final sessionKey = SessionKey( - content: { - 'algorithm': 'm.megolm.v1.aes-sha2', - 'room_id': '!Cuyf34gef24t:localhost', - 'session_id': 'X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ', - 'session_key': - 'AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY...' - }, - inboundGroupSession: olm.InboundGroupSession(), - key: '1234', - indexes: {}, - ); - expect(sessionKey.senderClaimedEd25519Key, ''); - expect(sessionKey.toJson(), - SessionKey.fromJson(sessionKey.toJson(), '1234').toJson()); - expect(sessionKey.toString(), json.encode(sessionKey.toJson())); - } - }); - }); -} diff --git a/test_driver/famedlysdk_test.dart b/test_driver/famedlysdk_test.dart index 6ac0d09..d230747 100644 --- a/test_driver/famedlysdk_test.dart +++ b/test_driver/famedlysdk_test.dart @@ -88,7 +88,8 @@ void test() async { await room.enableEncryption(); await Future.delayed(Duration(seconds: 5)); assert(room.encrypted == true); - assert(room.outboundGroupSession == null); + assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) == + null); print('++++ ($testUserA) Check known olm devices ++++'); assert(testClientA.userDeviceKeys.containsKey(testUserB)); @@ -123,16 +124,30 @@ void test() async { print("++++ ($testUserA) Send encrypted message: '$testMessage' ++++"); await room.sendTextEvent(testMessage); await Future.delayed(Duration(seconds: 5)); - assert(room.outboundGroupSession != null); - var currentSessionIdA = room.outboundGroupSession.session_id(); - assert(room.inboundGroupSessions - .containsKey(room.outboundGroupSession.session_id())); - assert(testClientA.olmSessions[testClientB.identityKey].length == 1); - assert(testClientB.olmSessions[testClientA.identityKey].length == 1); - assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() == - testClientB.olmSessions[testClientA.identityKey].first.session_id()); - assert(inviteRoom.inboundGroupSessions - .containsKey(room.outboundGroupSession.session_id())); + assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) != + null); + var currentSessionIdA = room.client.encryption.keyManager + .getOutboundGroupSession(room.id) + .outboundGroupSession + .session_id(); + assert(room.client.encryption.keyManager + .getInboundGroupSession(room.id, currentSessionIdA, '') != + null); + assert(testClientA + .encryption.olmManager.olmSessions[testClientB.identityKey].length == + 1); + assert(testClientB + .encryption.olmManager.olmSessions[testClientA.identityKey].length == + 1); + assert(testClientA + .encryption.olmManager.olmSessions[testClientB.identityKey].first + .session_id() == + testClientB + .encryption.olmManager.olmSessions[testClientA.identityKey].first + .session_id()); + assert(inviteRoom.client.encryption.keyManager + .getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') != + null); assert(room.lastMessage == testMessage); assert(inviteRoom.lastMessage == testMessage); print( @@ -141,14 +156,27 @@ void test() async { print("++++ ($testUserA) Send again encrypted message: '$testMessage2' ++++"); await room.sendTextEvent(testMessage2); await Future.delayed(Duration(seconds: 5)); - assert(testClientA.olmSessions[testClientB.identityKey].length == 1); - assert(testClientB.olmSessions[testClientA.identityKey].length == 1); - assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() == - testClientB.olmSessions[testClientA.identityKey].first.session_id()); + assert(testClientA + .encryption.olmManager.olmSessions[testClientB.identityKey].length == + 1); + assert(testClientB + .encryption.olmManager.olmSessions[testClientA.identityKey].length == + 1); + assert(testClientA + .encryption.olmManager.olmSessions[testClientB.identityKey].first + .session_id() == + testClientB + .encryption.olmManager.olmSessions[testClientA.identityKey].first + .session_id()); - assert(room.outboundGroupSession.session_id() == currentSessionIdA); - assert(inviteRoom.inboundGroupSessions - .containsKey(room.outboundGroupSession.session_id())); + assert(room.client.encryption.keyManager + .getOutboundGroupSession(room.id) + .outboundGroupSession + .session_id() == + currentSessionIdA); + assert(room.client.encryption.keyManager + .getInboundGroupSession(room.id, currentSessionIdA, '') != + null); assert(room.lastMessage == testMessage2); assert(inviteRoom.lastMessage == testMessage2); print( @@ -157,14 +185,31 @@ void test() async { print("++++ ($testUserB) Send again encrypted message: '$testMessage3' ++++"); await inviteRoom.sendTextEvent(testMessage3); await Future.delayed(Duration(seconds: 5)); - assert(testClientA.olmSessions[testClientB.identityKey].length == 1); - assert(testClientB.olmSessions[testClientA.identityKey].length == 1); - assert(room.outboundGroupSession.session_id() == currentSessionIdA); - assert(inviteRoom.outboundGroupSession != null); - assert(inviteRoom.inboundGroupSessions - .containsKey(inviteRoom.outboundGroupSession.session_id())); - assert(room.inboundGroupSessions - .containsKey(inviteRoom.outboundGroupSession.session_id())); + assert(testClientA + .encryption.olmManager.olmSessions[testClientB.identityKey].length == + 1); + assert(testClientB + .encryption.olmManager.olmSessions[testClientA.identityKey].length == + 1); + assert(room.client.encryption.keyManager + .getOutboundGroupSession(room.id) + .outboundGroupSession + .session_id() == + currentSessionIdA); + var inviteRoomOutboundGroupSession = inviteRoom.client.encryption.keyManager + .getOutboundGroupSession(inviteRoom.id); + + assert(inviteRoomOutboundGroupSession != null); + assert(inviteRoom.client.encryption.keyManager.getInboundGroupSession( + inviteRoom.id, + inviteRoomOutboundGroupSession.outboundGroupSession.session_id(), + '') != + null); + assert(room.client.encryption.keyManager.getInboundGroupSession( + room.id, + inviteRoomOutboundGroupSession.outboundGroupSession.session_id(), + '') != + null); assert(inviteRoom.lastMessage == testMessage3); assert(room.lastMessage == testMessage3); print( @@ -180,18 +225,42 @@ void test() async { print("++++ ($testUserA) Send again encrypted message: '$testMessage4' ++++"); await room.sendTextEvent(testMessage4); await Future.delayed(Duration(seconds: 5)); - assert(testClientA.olmSessions[testClientB.identityKey].length == 1); - assert(testClientB.olmSessions[testClientA.identityKey].length == 1); - assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() == - testClientB.olmSessions[testClientA.identityKey].first.session_id()); - assert(testClientA.olmSessions[testClientC.identityKey].length == 1); - assert(testClientC.olmSessions[testClientA.identityKey].length == 1); - assert(testClientA.olmSessions[testClientC.identityKey].first.session_id() == - testClientC.olmSessions[testClientA.identityKey].first.session_id()); - assert(room.outboundGroupSession.session_id() != currentSessionIdA); - currentSessionIdA = room.outboundGroupSession.session_id(); - assert(inviteRoom.inboundGroupSessions - .containsKey(room.outboundGroupSession.session_id())); + assert(testClientA + .encryption.olmManager.olmSessions[testClientB.identityKey].length == + 1); + assert(testClientB + .encryption.olmManager.olmSessions[testClientA.identityKey].length == + 1); + assert(testClientA + .encryption.olmManager.olmSessions[testClientB.identityKey].first + .session_id() == + testClientB + .encryption.olmManager.olmSessions[testClientA.identityKey].first + .session_id()); + assert(testClientA + .encryption.olmManager.olmSessions[testClientC.identityKey].length == + 1); + assert(testClientC + .encryption.olmManager.olmSessions[testClientA.identityKey].length == + 1); + assert(testClientA + .encryption.olmManager.olmSessions[testClientC.identityKey].first + .session_id() == + testClientC + .encryption.olmManager.olmSessions[testClientA.identityKey].first + .session_id()); + assert(room.client.encryption.keyManager + .getOutboundGroupSession(room.id) + .outboundGroupSession + .session_id() != + currentSessionIdA); + currentSessionIdA = room.client.encryption.keyManager + .getOutboundGroupSession(room.id) + .outboundGroupSession + .session_id(); + assert(inviteRoom.client.encryption.keyManager + .getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') != + null); assert(room.lastMessage == testMessage4); assert(inviteRoom.lastMessage == testMessage4); print( @@ -206,14 +275,30 @@ void test() async { print("++++ ($testUserA) Send again encrypted message: '$testMessage6' ++++"); await room.sendTextEvent(testMessage6); await Future.delayed(Duration(seconds: 5)); - assert(testClientA.olmSessions[testClientB.identityKey].length == 1); - assert(testClientB.olmSessions[testClientA.identityKey].length == 1); - assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() == - testClientB.olmSessions[testClientA.identityKey].first.session_id()); - assert(room.outboundGroupSession.session_id() != currentSessionIdA); - currentSessionIdA = room.outboundGroupSession.session_id(); - assert(inviteRoom.inboundGroupSessions - .containsKey(room.outboundGroupSession.session_id())); + assert(testClientA + .encryption.olmManager.olmSessions[testClientB.identityKey].length == + 1); + assert(testClientB + .encryption.olmManager.olmSessions[testClientA.identityKey].length == + 1); + assert(testClientA + .encryption.olmManager.olmSessions[testClientB.identityKey].first + .session_id() == + testClientB + .encryption.olmManager.olmSessions[testClientA.identityKey].first + .session_id()); + assert(room.client.encryption.keyManager + .getOutboundGroupSession(room.id) + .outboundGroupSession + .session_id() != + currentSessionIdA); + currentSessionIdA = room.client.encryption.keyManager + .getOutboundGroupSession(room.id) + .outboundGroupSession + .session_id(); + assert(inviteRoom.client.encryption.keyManager + .getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') != + null); assert(room.lastMessage == testMessage6); assert(inviteRoom.lastMessage == testMessage6); print( @@ -241,18 +326,18 @@ void test() async { assert(restoredRoom.inboundGroupSessions.keys.toList()[i] == room.inboundGroupSessions.keys.toList()[i]); } - assert(testClientA.olmSessions[testClientB.identityKey].length == 1); - assert(testClientB.olmSessions[testClientA.identityKey].length == 1); - assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() == - testClientB.olmSessions[testClientA.identityKey].first.session_id()); + assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].length == 1); + assert(testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].length == 1); + assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].first.session_id() == + testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].first.session_id()); print("++++ ($testUserA) Send again encrypted message: '$testMessage5' ++++"); await restoredRoom.sendTextEvent(testMessage5); await Future.delayed(Duration(seconds: 5)); - assert(testClientA.olmSessions[testClientB.identityKey].length == 1); - assert(testClientB.olmSessions[testClientA.identityKey].length == 1); - assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() == - testClientB.olmSessions[testClientA.identityKey].first.session_id()); + assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].length == 1); + assert(testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].length == 1); + assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].first.session_id() == + testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].first.session_id()); assert(restoredRoom.lastMessage == testMessage5); assert(inviteRoom.lastMessage == testMessage5); assert(testClientB.getRoomById(roomId).lastMessage == testMessage5); From 2e46155f47d3f621c513ba828ca552da25568a15 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Thu, 4 Jun 2020 14:26:35 +0200 Subject: [PATCH 02/18] fix tests without olm --- lib/encryption/olm_manager.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/encryption/olm_manager.dart b/lib/encryption/olm_manager.dart index 2d78342..4b5fad2 100644 --- a/lib/encryption/olm_manager.dart +++ b/lib/encryption/olm_manager.dart @@ -56,7 +56,7 @@ class OlmManager { throw ('Upload key failed'); } } catch (_) { - _olmAccount.free(); + _olmAccount?.free(); _olmAccount = null; } } else { @@ -65,7 +65,7 @@ class OlmManager { _olmAccount = olm.Account(); _olmAccount.unpickle(client.userID, olmAccount); } catch (_) { - _olmAccount.free(); + _olmAccount?.free(); _olmAccount = null; } } From f3f3231df6efb53c382f354e9ae4ae05bf3e0a07 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Thu, 4 Jun 2020 17:51:49 +0200 Subject: [PATCH 03/18] 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 add3ff2..072d0c8 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 4b5fad2..648f2b7 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 40036d6..ef6b2a4 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 09bef10..eaded69 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 0000000..56ace3c --- /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 0000000..6b082b6 --- /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 243f776..982a4f0 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 039a38f..eac31a0 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 410b736..e2f8524 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', From c94e41d393c5ccf366d05e4009640aa7598d82f0 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Thu, 4 Jun 2020 18:16:22 +0200 Subject: [PATCH 04/18] fix tests for real --- lib/encryption/olm_manager.dart | 5 +---- test/client_test.dart | 4 ++-- test/fake_matrix_api.dart | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/encryption/olm_manager.dart b/lib/encryption/olm_manager.dart index 648f2b7..218162f 100644 --- a/lib/encryption/olm_manager.dart +++ b/lib/encryption/olm_manager.dart @@ -179,12 +179,9 @@ class OlmManager { : null, oneTimeKeys: signedOneTimeKeys, ); - if (response['signed_curve25519'] != oneTimeKeysCount) { - return false; - } _olmAccount.mark_keys_as_published(); await client.database?.updateClientKeys(pickledOlmAccount, client.id); - return true; + return response['signed_curve25519'] == oneTimeKeysCount; } void handleDeviceOneTimeKeysCount(Map countJson) { diff --git a/test/client_test.dart b/test/client_test.dart index eaded69..47ae597 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -159,7 +159,7 @@ void main() { expect(presenceCounter, 1); expect(accountDataCounter, 3); await Future.delayed(Duration(milliseconds: 50)); - expect(matrix.userDeviceKeys.length, 3); + expect(matrix.userDeviceKeys.length, 4); expect(matrix.userDeviceKeys['@alice:example.com'].outdated, false); expect(matrix.userDeviceKeys['@alice:example.com'].deviceKeys.length, 2); expect( @@ -178,7 +178,7 @@ void main() { } })); await Future.delayed(Duration(milliseconds: 50)); - expect(matrix.userDeviceKeys.length, 2); + expect(matrix.userDeviceKeys.length, 3); expect(matrix.userDeviceKeys['@alice:example.com'].outdated, true); await matrix.handleSync(SyncUpdate.fromJson({ diff --git a/test/fake_matrix_api.dart b/test/fake_matrix_api.dart index e2f8524..2fec6b9 100644 --- a/test/fake_matrix_api.dart +++ b/test/fake_matrix_api.dart @@ -1598,7 +1598,7 @@ class FakeMatrixApi extends MockClient { '/client/r0/keys/upload': (var req) => { 'one_time_key_counts': { 'curve25519': 10, - 'signed_curve25519': 66, + 'signed_curve25519': json.decode(req)['one_time_keys']?.keys?.length ?? 0, } }, '/client/r0/keys/query': (var req) => { From 8748545f67a6045cb4ef1729f6bc4ebe2845c3d6 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Thu, 4 Jun 2020 18:36:07 +0200 Subject: [PATCH 05/18] add olm manager tests --- test/encryption/olm_manager_test.dart | 88 +++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 test/encryption/olm_manager_test.dart diff --git a/test/encryption/olm_manager_test.dart b/test/encryption/olm_manager_test.dart new file mode 100644 index 0000000..39a321e --- /dev/null +++ b/test/encryption/olm_manager_test.dart @@ -0,0 +1,88 @@ +/* + * 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 'dart:convert'; +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('Olm Manager', () { + 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()); + + test('setupClient', () async { + client.database = getDatabase(); + await client.checkServer('https://fakeServer.notExisting'); + await client.login('test', '1234'); + }); + + test('signatures', () async { + final payload = { + 'fox': 'floof', + }; + final signedPayload = client.encryption.olmManager.signJson(payload); + expect(client.encryption.olmManager.checkJsonSignature(client.fingerprintKey, signedPayload, client.userID, client.deviceID), true); + expect(client.encryption.olmManager.checkJsonSignature(client.fingerprintKey, payload, client.userID, client.deviceID), false); + }); + + test('uploadKeys', () async { + FakeMatrixApi.calledEndpoints.clear(); + final res = await client.encryption.olmManager.uploadKeys(uploadDeviceKeys: true); + expect(res, true); + var sent = json.decode(FakeMatrixApi.calledEndpoints['/client/r0/keys/upload'].first); + expect(sent['device_keys'] != null, true); + expect(sent['one_time_keys'] != null, true); + expect(sent['one_time_keys'].keys.length, 66); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.olmManager.uploadKeys(); + sent = json.decode(FakeMatrixApi.calledEndpoints['/client/r0/keys/upload'].first); + expect(sent['device_keys'] != null, false); + FakeMatrixApi.calledEndpoints.clear(); + await client.encryption.olmManager.uploadKeys(oldKeyCount: 20); + sent = json.decode(FakeMatrixApi.calledEndpoints['/client/r0/keys/upload'].first); + expect(sent['one_time_keys'].keys.length, 46); + }); + + test('handleDeviceOneTimeKeysCount', () async { + FakeMatrixApi.calledEndpoints.clear(); + client.encryption.olmManager.handleDeviceOneTimeKeysCount({'signed_curve25519': 20}); + await Future.delayed(Duration(milliseconds: 50)); + expect(FakeMatrixApi.calledEndpoints.containsKey('/client/r0/keys/upload'), true); + + FakeMatrixApi.calledEndpoints.clear(); + client.encryption.olmManager.handleDeviceOneTimeKeysCount({'signed_curve25519': 70}); + await Future.delayed(Duration(milliseconds: 50)); + expect(FakeMatrixApi.calledEndpoints.containsKey('/client/r0/keys/upload'), false); + }); + }); +} From 05c799e6a5da2adf4d075419387945a1c5208bc6 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Thu, 4 Jun 2020 20:16:18 +0200 Subject: [PATCH 06/18] format --- lib/encryption/olm_manager.dart | 14 +++++--- .../encrypt_decrypt_room_message_test.dart | 6 ++-- .../encrypt_decrypt_to_device_test.dart | 21 +++++++---- test/encryption/olm_manager_test.dart | 36 +++++++++++++------ test/fake_matrix_api.dart | 12 ++++--- 5 files changed, 61 insertions(+), 28 deletions(-) diff --git a/lib/encryption/olm_manager.dart b/lib/encryption/olm_manager.dart index 218162f..e116dc7 100644 --- a/lib/encryption/olm_manager.dart +++ b/lib/encryption/olm_manager.dart @@ -123,7 +123,8 @@ class OlmManager { } /// Generates new one time keys, signs everything and upload it to the server. - Future uploadKeys({bool uploadDeviceKeys = false, int oldKeyCount = 0}) async { + Future uploadKeys( + {bool uploadDeviceKeys = false, int oldKeyCount = 0}) async { if (!enabled) { return true; } @@ -131,7 +132,9 @@ class OlmManager { // generate 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; + 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()); @@ -301,8 +304,8 @@ class OlmManager { if (client.database == null) { return false; } - final sessions = await client.database.getSingleOlmSessions( - client.id, senderKey, client.userID); + final sessions = await client.database + .getSingleOlmSessions(client.id, senderKey, client.userID); if (sessions.isEmpty) { return false; // okay, can't do anything } @@ -341,7 +344,8 @@ class OlmManager { final identityKey = client.userDeviceKeys[userId].deviceKeys[deviceId].curve25519Key; for (Map deviceKey in deviceKeysEntry.value.values) { - if (!checkJsonSignature(fingerprintKey, deviceKey, userId, deviceId)) { + if (!checkJsonSignature( + fingerprintKey, deviceKey, userId, deviceId)) { continue; } try { diff --git a/test/encryption/encrypt_decrypt_room_message_test.dart b/test/encryption/encrypt_decrypt_room_message_test.dart index 56ace3c..20df768 100644 --- a/test/encryption/encrypt_decrypt_room_message_test.dart +++ b/test/encryption/encrypt_decrypt_room_message_test.dart @@ -71,7 +71,8 @@ void main() { originServerTs: now, eventId: '\$event', ); - final decryptedEvent = await client.encryption.decryptRoomEvent(roomId, encryptedEvent); + 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!'); @@ -87,7 +88,8 @@ void main() { originServerTs: now, eventId: '\$event', ); - final decryptedEvent = await client.encryption.decryptRoomEvent(roomId, encryptedEvent); + 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 index 6b082b6..a416ce4 100644 --- a/test/encryption/encrypt_decrypt_to_device_test.dart +++ b/test/encryption/encrypt_decrypt_to_device_test.dart @@ -28,7 +28,8 @@ void main() { 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'; + const otherPickledOlmAccount = + 'VWhVApbkcilKAEGppsPDf9nNVjaK8/IxT3asSR0sYg0S5KgbfE8vXEPwoiKBX2cEvwX3OessOBOkk+ZE7TTbjlrh/KEd31p8Wo+47qj0AP+Ky+pabnhi+/rTBvZy+gfzTqUfCxZrkzfXI9Op4JnP6gYmy7dVX2lMYIIs9WCO1jcmIXiXum5jnfXu1WLfc7PZtO2hH+k9CDKosOFaXRBmsu8k/BGXPSoWqUpvu6WpEG9t5STk4FeAzA'; group('Encrypt/Decrypt to-device messages', () { var olmEnabled = true; @@ -44,7 +45,8 @@ void main() { if (!olmEnabled) return; var client = Client('testclient', debug: true, httpClient: FakeMatrixApi()); - var otherClient = Client('othertestclient', debug: true, httpClient: FakeMatrixApi()); + var otherClient = + Client('othertestclient', debug: true, httpClient: FakeMatrixApi()); final roomId = '!726s6s6q:example.com'; DeviceKeys device; Map payload; @@ -92,12 +94,14 @@ void main() { }); test('encryptToDeviceMessage', () async { - payload = await otherClient.encryption.encryptToDeviceMessage([device], 'm.to_device', {'hello': 'foxies'}); + 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'}); + await otherClient.encryption.encryptToDeviceMessagePayload( + device, 'm.to_device', {'hello': 'foxies'}); }); test('decryptToDeviceEvent', () async { @@ -106,20 +110,23 @@ void main() { type: EventTypes.Encrypted, content: payload[client.userID][client.deviceID], ); - final decryptedEvent = await client.encryption.decryptToDeviceEvent(encryptedEvent); + 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'}); + 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); + final decryptedEvent = + await client.encryption.decryptToDeviceEvent(encryptedEvent); expect(decryptedEvent.type, 'm.to_device'); expect(decryptedEvent.content['hello'], 'superfoxies'); }); diff --git a/test/encryption/olm_manager_test.dart b/test/encryption/olm_manager_test.dart index 39a321e..450544c 100644 --- a/test/encryption/olm_manager_test.dart +++ b/test/encryption/olm_manager_test.dart @@ -51,38 +51,54 @@ void main() { 'fox': 'floof', }; final signedPayload = client.encryption.olmManager.signJson(payload); - expect(client.encryption.olmManager.checkJsonSignature(client.fingerprintKey, signedPayload, client.userID, client.deviceID), true); - expect(client.encryption.olmManager.checkJsonSignature(client.fingerprintKey, payload, client.userID, client.deviceID), false); + expect( + client.encryption.olmManager.checkJsonSignature(client.fingerprintKey, + signedPayload, client.userID, client.deviceID), + true); + expect( + client.encryption.olmManager.checkJsonSignature( + client.fingerprintKey, payload, client.userID, client.deviceID), + false); }); test('uploadKeys', () async { FakeMatrixApi.calledEndpoints.clear(); - final res = await client.encryption.olmManager.uploadKeys(uploadDeviceKeys: true); + final res = + await client.encryption.olmManager.uploadKeys(uploadDeviceKeys: true); expect(res, true); - var sent = json.decode(FakeMatrixApi.calledEndpoints['/client/r0/keys/upload'].first); + var sent = json.decode( + FakeMatrixApi.calledEndpoints['/client/r0/keys/upload'].first); expect(sent['device_keys'] != null, true); expect(sent['one_time_keys'] != null, true); expect(sent['one_time_keys'].keys.length, 66); FakeMatrixApi.calledEndpoints.clear(); await client.encryption.olmManager.uploadKeys(); - sent = json.decode(FakeMatrixApi.calledEndpoints['/client/r0/keys/upload'].first); + sent = json.decode( + FakeMatrixApi.calledEndpoints['/client/r0/keys/upload'].first); expect(sent['device_keys'] != null, false); FakeMatrixApi.calledEndpoints.clear(); await client.encryption.olmManager.uploadKeys(oldKeyCount: 20); - sent = json.decode(FakeMatrixApi.calledEndpoints['/client/r0/keys/upload'].first); + sent = json.decode( + FakeMatrixApi.calledEndpoints['/client/r0/keys/upload'].first); expect(sent['one_time_keys'].keys.length, 46); }); test('handleDeviceOneTimeKeysCount', () async { FakeMatrixApi.calledEndpoints.clear(); - client.encryption.olmManager.handleDeviceOneTimeKeysCount({'signed_curve25519': 20}); + client.encryption.olmManager + .handleDeviceOneTimeKeysCount({'signed_curve25519': 20}); await Future.delayed(Duration(milliseconds: 50)); - expect(FakeMatrixApi.calledEndpoints.containsKey('/client/r0/keys/upload'), true); + expect( + FakeMatrixApi.calledEndpoints.containsKey('/client/r0/keys/upload'), + true); FakeMatrixApi.calledEndpoints.clear(); - client.encryption.olmManager.handleDeviceOneTimeKeysCount({'signed_curve25519': 70}); + client.encryption.olmManager + .handleDeviceOneTimeKeysCount({'signed_curve25519': 70}); await Future.delayed(Duration(milliseconds: 50)); - expect(FakeMatrixApi.calledEndpoints.containsKey('/client/r0/keys/upload'), false); + expect( + FakeMatrixApi.calledEndpoints.containsKey('/client/r0/keys/upload'), + false); }); }); } diff --git a/test/fake_matrix_api.dart b/test/fake_matrix_api.dart index 2fec6b9..2fb25af 100644 --- a/test/fake_matrix_api.dart +++ b/test/fake_matrix_api.dart @@ -1576,7 +1576,8 @@ class FakeMatrixApi extends MockClient { 'key': 'qc72ve94cA28iuE0fXa98QO3uls39DHWdQlYyvvhGh0', 'signatures': { '@test:fakeServer.notExisting': { - 'ed25519:GHTYAJCE': 'dFwffr5kTKefO7sjnWLMhTzw7oV31nkPIDRxFy5OQT2OP5++Ao0KRbaBZ6qfuT7lW1owKK0Xk3s7QTBvc/eNDA', + 'ed25519:GHTYAJCE': + 'dFwffr5kTKefO7sjnWLMhTzw7oV31nkPIDRxFy5OQT2OP5++Ao0KRbaBZ6qfuT7lW1owKK0Xk3s7QTBvc/eNDA', }, }, }, @@ -1598,7 +1599,8 @@ class FakeMatrixApi extends MockClient { '/client/r0/keys/upload': (var req) => { 'one_time_key_counts': { 'curve25519': 10, - 'signed_curve25519': json.decode(req)['one_time_keys']?.keys?.length ?? 0, + 'signed_curve25519': + json.decode(req)['one_time_keys']?.keys?.length ?? 0, } }, '/client/r0/keys/query': (var req) => { @@ -1666,8 +1668,10 @@ class FakeMatrixApi extends MockClient { 'm.megolm.v1.aes-sha2' ], 'keys': { - 'curve25519:FOXDEVICE': 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg', - 'ed25519:FOXDEVICE': 'R5/p04tticvdlNIxiiBIP0j9OQWv8ep6eEU6/lWKDxw', + 'curve25519:FOXDEVICE': + 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg', + 'ed25519:FOXDEVICE': + 'R5/p04tticvdlNIxiiBIP0j9OQWv8ep6eEU6/lWKDxw', }, 'signatures': {}, }, From e14cd61d6d723c08918151316057b908c7d878fa Mon Sep 17 00:00:00 2001 From: Sorunome Date: Thu, 4 Jun 2020 20:30:55 +0200 Subject: [PATCH 07/18] flutter analyze --- test/encryption/encrypt_decrypt_to_device_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/test/encryption/encrypt_decrypt_to_device_test.dart b/test/encryption/encrypt_decrypt_to_device_test.dart index a416ce4..cd57212 100644 --- a/test/encryption/encrypt_decrypt_to_device_test.dart +++ b/test/encryption/encrypt_decrypt_to_device_test.dart @@ -47,7 +47,6 @@ void main() { 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; From 0b1d6ae8dd8b22957aeb6f5f8cf21f640b5c825b Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 5 Jun 2020 09:59:37 +0200 Subject: [PATCH 08/18] split off into fake client --- .../encrypt_decrypt_room_message_test.dart | 9 ++-- .../encrypt_decrypt_to_device_test.dart | 36 ++++---------- test/encryption/key_request_test.dart | 45 +++++++---------- test/encryption/key_verification_test.dart | 15 +++--- test/encryption/olm_manager_test.dart | 8 ++-- test/event_test.dart | 2 +- test/fake_client.dart | 48 +++++++++++++++++++ test/fake_matrix_api.dart | 5 +- test/room_test.dart | 18 ++----- test/timeline_test.dart | 6 +-- 10 files changed, 99 insertions(+), 93 deletions(-) create mode 100644 test/fake_client.dart diff --git a/test/encryption/encrypt_decrypt_room_message_test.dart b/test/encryption/encrypt_decrypt_room_message_test.dart index 20df768..f2a46da 100644 --- a/test/encryption/encrypt_decrypt_room_message_test.dart +++ b/test/encryption/encrypt_decrypt_room_message_test.dart @@ -20,8 +20,7 @@ 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'; +import '../fake_client.dart'; void main() { group('Encrypt/Decrypt room message', () { @@ -37,16 +36,14 @@ void main() { if (!olmEnabled) return; - var client = Client('testclient', debug: true, httpClient: FakeMatrixApi()); + Client client; 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'); + client = await getClient(); room = client.getRoomById(roomId); }); diff --git a/test/encryption/encrypt_decrypt_to_device_test.dart b/test/encryption/encrypt_decrypt_to_device_test.dart index cd57212..7a35d6f 100644 --- a/test/encryption/encrypt_decrypt_to_device_test.dart +++ b/test/encryption/encrypt_decrypt_to_device_test.dart @@ -20,14 +20,11 @@ import 'package:famedlysdk/famedlysdk.dart'; import 'package:test/test.dart'; import 'package:olm/olm.dart' as olm; +import '../fake_client.dart'; 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'; - + // key @othertest:fakeServer.notExisting const otherPickledOlmAccount = 'VWhVApbkcilKAEGppsPDf9nNVjaK8/IxT3asSR0sYg0S5KgbfE8vXEPwoiKBX2cEvwX3OessOBOkk+ZE7TTbjlrh/KEd31p8Wo+47qj0AP+Ky+pabnhi+/rTBvZy+gfzTqUfCxZrkzfXI9Op4JnP6gYmy7dVX2lMYIIs9WCO1jcmIXiXum5jnfXu1WLfc7PZtO2hH+k9CDKosOFaXRBmsu8k/BGXPSoWqUpvu6WpEG9t5STk4FeAzA'; @@ -44,31 +41,16 @@ void main() { if (!olmEnabled) return; - var client = Client('testclient', debug: true, httpClient: FakeMatrixApi()); + Client client; var otherClient = Client('othertestclient', debug: true, httpClient: FakeMatrixApi()); DeviceKeys device; Map payload; test('setupClient', () async { - client.database = getDatabase(); + client = await getClient(); 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', @@ -78,14 +60,14 @@ void main() { newOlmAccount: otherPickledOlmAccount, ); - await Future.delayed(Duration(milliseconds: 50)); + await Future.delayed(Duration(milliseconds: 10)); device = DeviceKeys( - userId: resp.userId, - deviceId: resp.deviceId, + userId: client.userID, + deviceId: client.deviceID, algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'], keys: { - 'curve25519:${resp.deviceId}': client.identityKey, - 'ed25519:${resp.deviceId}': client.fingerprintKey, + 'curve25519:${client.deviceID}': client.identityKey, + 'ed25519:${client.deviceID}': client.fingerprintKey, }, verified: true, blocked: false, diff --git a/test/encryption/key_request_test.dart b/test/encryption/key_request_test.dart index 982a4f0..1ccf510 100644 --- a/test/encryption/key_request_test.dart +++ b/test/encryption/key_request_test.dart @@ -19,9 +19,10 @@ import 'dart:convert'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:test/test.dart'; +import 'package:olm/olm.dart' as olm; +import '../fake_client.dart'; import '../fake_matrix_api.dart'; -import '../fake_database.dart'; Map jsonDecode(dynamic payload) { if (payload is String) { @@ -38,18 +39,22 @@ Map jsonDecode(dynamic payload) { void main() { /// All Tests related to device keys group('Key Request', () { + 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; + final validSessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU'; final validSenderKey = '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI'; test('Create Request', () async { - var matrix = - Client('testclient', debug: true, httpClient: FakeMatrixApi()); - matrix.database = getDatabase(); - await matrix.checkServer('https://fakeServer.notExisting'); - await matrix.login('test', '1234'); - if (!matrix.encryptionEnabled) { - await matrix.dispose(closeDatabase: true); - return; - } + var matrix = await getClient(); final requestRoom = matrix.getRoomById('!726s6s6q:example.com'); await matrix.encryption.keyManager .request(requestRoom, 'sessionId', validSenderKey); @@ -75,15 +80,7 @@ void main() { await matrix.dispose(closeDatabase: true); }); test('Reply To Request', () async { - var matrix = - Client('testclient', debug: true, httpClient: FakeMatrixApi()); - matrix.database = getDatabase(); - await matrix.checkServer('https://fakeServer.notExisting'); - await matrix.login('test', '1234'); - if (!matrix.encryptionEnabled) { - await matrix.dispose(closeDatabase: true); - return; - } + var matrix = await getClient(); matrix.setUserId('@alice:example.com'); // we need to pretend to be alice FakeMatrixApi.calledEndpoints.clear(); await matrix @@ -224,15 +221,7 @@ void main() { await matrix.dispose(closeDatabase: true); }); test('Receive shared keys', () async { - var matrix = - Client('testclient', debug: true, httpClient: FakeMatrixApi()); - matrix.database = getDatabase(); - await matrix.checkServer('https://fakeServer.notExisting'); - await matrix.login('test', '1234'); - if (!matrix.encryptionEnabled) { - await matrix.dispose(closeDatabase: true); - return; - } + var matrix = await getClient(); final requestRoom = matrix.getRoomById('!726s6s6q:example.com'); await matrix.encryption.keyManager .request(requestRoom, validSessionId, validSenderKey); diff --git a/test/encryption/key_verification_test.dart b/test/encryption/key_verification_test.dart index eac31a0..78a24d2 100644 --- a/test/encryption/key_verification_test.dart +++ b/test/encryption/key_verification_test.dart @@ -21,8 +21,7 @@ import 'package:famedlysdk/encryption.dart'; import 'package:test/test.dart'; import 'package:olm/olm.dart' as olm; -import '../fake_matrix_api.dart'; -import '../fake_database.dart'; +import '../fake_client.dart'; void main() { /// All Tests related to the ChatTime @@ -37,17 +36,17 @@ void main() { } print('[LibOlm] Enabled: $olmEnabled'); - var client = Client('testclient', debug: true, httpClient: FakeMatrixApi()); - var room = Room(id: '!localpart:server.abc', client: client); + if (!olmEnabled) return; + + Client client; + Room room; var updateCounter = 0; KeyVerification keyVerification; - if (!olmEnabled) return; test('setupClient', () async { - client.database = getDatabase(); - await client.checkServer('https://fakeServer.notExisting'); - await client.login('test', '1234'); + client = await getClient(); + room = Room(id: '!localpart:server.abc', client: client); keyVerification = KeyVerification( encryption: client.encryption, room: room, diff --git a/test/encryption/olm_manager_test.dart b/test/encryption/olm_manager_test.dart index 450544c..5c2d2f1 100644 --- a/test/encryption/olm_manager_test.dart +++ b/test/encryption/olm_manager_test.dart @@ -21,8 +21,8 @@ import 'package:famedlysdk/famedlysdk.dart'; import 'package:test/test.dart'; import 'package:olm/olm.dart' as olm; +import '../fake_client.dart'; import '../fake_matrix_api.dart'; -import '../fake_database.dart'; void main() { group('Olm Manager', () { @@ -38,12 +38,10 @@ void main() { if (!olmEnabled) return; - var client = Client('testclient', debug: true, httpClient: FakeMatrixApi()); + Client client; test('setupClient', () async { - client.database = getDatabase(); - await client.checkServer('https://fakeServer.notExisting'); - await client.login('test', '1234'); + client = await getClient(); }); test('signatures', () async { diff --git a/test/event_test.dart b/test/event_test.dart index 26028a2..849141d 100644 --- a/test/event_test.dart +++ b/test/event_test.dart @@ -220,7 +220,7 @@ void main() { event.status = -1; final resp2 = await event.sendAgain(txid: '1234'); expect(resp1, null); - expect(resp2, '42'); + expect(resp2, '\$event0'); await matrix.dispose(closeDatabase: true); }); diff --git a/test/fake_client.dart b/test/fake_client.dart new file mode 100644 index 0000000..119a851 --- /dev/null +++ b/test/fake_client.dart @@ -0,0 +1,48 @@ +/* + * 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 'fake_matrix_api.dart'; +import 'fake_database.dart'; + +// key @test:fakeServer.notExisting +const pickledOlmAccount = + 'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtu/BjDjz0C3ioDgrrFdoSrn+GSeF5FGKsNu8OLkQ9Lq5+BrUutK5QSJI19uoZj2sj/OixvIpnun8XxYpXo7cfh9MEtKI8ob7lLM2OpZ8BogU70ORgkwthsPSOtxQGPhx8+y5Sg7B6KGlU'; + +Future getClient() async { + final client = Client('testclient', debug: true, httpClient: FakeMatrixApi()); + client.database = getDatabase(); + await client.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, + ); + await Future.delayed(Duration(milliseconds: 10)); + return client; +} diff --git a/test/fake_matrix_api.dart b/test/fake_matrix_api.dart index 2fb25af..ae7fea7 100644 --- a/test/fake_matrix_api.dart +++ b/test/fake_matrix_api.dart @@ -25,6 +25,7 @@ import 'package:http/testing.dart'; class FakeMatrixApi extends MockClient { static final calledEndpoints = >{}; + static int eventCounter = 0; FakeMatrixApi() : super((request) async { @@ -1754,13 +1755,13 @@ class FakeMatrixApi extends MockClient { '/client/r0/directory/room/%23testalias%3Aexample.com': (var reqI) => {}, '/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.message/testtxid': (var reqI) => { - 'event_id': '42', + 'event_id': '\$event${FakeMatrixApi.eventCounter++}', }, '/client/r0/rooms/!localpart%3Aexample.com/typing/%40alice%3Aexample.com': (var req) => {}, '/client/r0/rooms/%211234%3Aexample.com/send/m.room.message/1234': (var reqI) => { - 'event_id': '42', + 'event_id': '\$event${FakeMatrixApi.eventCounter++}', }, '/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags/testtag': (var req) => {}, diff --git a/test/room_test.dart b/test/room_test.dart index 11300c6..69000d6 100644 --- a/test/room_test.dart +++ b/test/room_test.dart @@ -26,7 +26,7 @@ import 'package:famedlysdk/src/database/database.dart' show DbRoom, DbRoomState, DbRoomAccountData; import 'package:test/test.dart'; -import 'fake_matrix_api.dart'; +import 'fake_client.dart'; import 'dart:typed_data'; @@ -37,15 +37,7 @@ void main() { /// All Tests related to the Event group('Room', () { test('Login', () async { - matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi()); - - final checkResp = - await matrix.checkServer('https://fakeServer.notExisting'); - - final loginResp = await matrix.login('test', '1234'); - - expect(checkResp, true); - expect(loginResp, true); + matrix = await getClient(); }); test('Create from json', () async { @@ -315,7 +307,7 @@ void main() { test('getTimeline', () async { final timeline = await room.getTimeline(); - expect(timeline.events.length, 1); + expect(timeline.events.length, 0); }); test('getUserByMXID', () async { @@ -338,13 +330,13 @@ void main() { final dynamic resp = await room.sendEvent( {'msgtype': 'm.text', 'body': 'hello world'}, txid: 'testtxid'); - expect(resp, '42'); + expect(resp, '\$event0'); }); test('sendEvent', () async { final dynamic resp = await room.sendTextEvent('Hello world', txid: 'testtxid'); - expect(resp, '42'); + expect(resp, '\$event1'); }); // Not working because there is no real file to test it... diff --git a/test/timeline_test.dart b/test/timeline_test.dart index 689c4a1..caf1309 100644 --- a/test/timeline_test.dart +++ b/test/timeline_test.dart @@ -143,7 +143,7 @@ void main() { expect(updateCount, 5); expect(insertList, [0, 0, 0]); expect(insertList.length, timeline.events.length); - expect(timeline.events[0].eventId, '42'); + expect(timeline.events[0].eventId, '\$event0'); expect(timeline.events[0].status, 1); client.onEvent.add(EventUpdate( @@ -155,7 +155,7 @@ void main() { 'content': {'msgtype': 'm.text', 'body': 'test'}, 'sender': '@alice:example.com', 'status': 2, - 'event_id': '42', + 'event_id': '\$event0', 'unsigned': {'transaction_id': '1234'}, 'origin_server_ts': DateTime.now().millisecondsSinceEpoch }, @@ -166,7 +166,7 @@ void main() { expect(updateCount, 6); expect(insertList, [0, 0, 0]); expect(insertList.length, timeline.events.length); - expect(timeline.events[0].eventId, '42'); + expect(timeline.events[0].eventId, '\$event0'); expect(timeline.events[0].status, 2); }); From fbc8f03f67476f54ec934822327adc4c2feab970 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 5 Jun 2020 10:15:36 +0200 Subject: [PATCH 09/18] encrypt m.room_key event properly --- lib/encryption/key_manager.dart | 9 ++++----- test/client_test.dart | 2 +- test/fake_client.dart | 2 +- test/fake_matrix_api.dart | 30 ++++++++++++++++++++++-------- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index 4234b67..243d5a5 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -419,9 +419,9 @@ class KeyManager { }, encrypted: false); } else if (event.type == 'm.room_key') { - //if (event.encryptedContent == null) { - // return; // the event wasn't encrypted, this is a security risk; - //} + if (event.encryptedContent == null) { + return; // the event wasn't encrypted, this is a security risk; + } final String roomId = event.content['room_id']; final String sessionId = event.content['session_id']; if (client.userDeviceKeys.containsKey(event.sender) && @@ -432,8 +432,7 @@ class KeyManager { .deviceKeys[event.content['requesting_device_id']] .ed25519Key; } - // event.encryptedContent['sender_key'] - setInboundGroupSession(roomId, sessionId, '', event.content, + setInboundGroupSession(roomId, sessionId, event.encryptedContent['sender_key'], event.content, forwarded: false); } } diff --git a/test/client_test.dart b/test/client_test.dart index 47ae597..a73aad2 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -40,7 +40,7 @@ void main() { // key @test:fakeServer.notExisting const pickledOlmAccount = - 'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtu/BjDjz0C3ioDgrrFdoSrn+GSeF5FGKsNu8OLkQ9Lq5+BrUutK5QSJI19uoZj2sj/OixvIpnun8XxYpXo7cfh9MEtKI8ob7lLM2OpZ8BogU70ORgkwthsPSOtxQGPhx8+y5Sg7B6KGlU'; + 'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw'; const identityKey = '7rvl3jORJkBiK4XX1e5TnGnqz068XfYJ0W++Ml63rgk'; const fingerprintKey = 'gjL//fyaFHADt9KBADGag8g7F8Up78B/K1zXeiEPLJo'; diff --git a/test/fake_client.dart b/test/fake_client.dart index 119a851..9fb3837 100644 --- a/test/fake_client.dart +++ b/test/fake_client.dart @@ -23,7 +23,7 @@ import 'fake_database.dart'; // key @test:fakeServer.notExisting const pickledOlmAccount = - 'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtu/BjDjz0C3ioDgrrFdoSrn+GSeF5FGKsNu8OLkQ9Lq5+BrUutK5QSJI19uoZj2sj/OixvIpnun8XxYpXo7cfh9MEtKI8ob7lLM2OpZ8BogU70ORgkwthsPSOtxQGPhx8+y5Sg7B6KGlU'; + 'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw'; Future getClient() async { final client = Client('testclient', debug: true, httpClient: FakeMatrixApi()); diff --git a/test/fake_matrix_api.dart b/test/fake_matrix_api.dart index ae7fea7..0c5d256 100644 --- a/test/fake_matrix_api.dart +++ b/test/fake_matrix_api.dart @@ -528,16 +528,30 @@ class FakeMatrixApi extends MockClient { 'rooms': ['!726s6s6q:example.com'] } }, - { - 'sender': '@alice:example.com', +// { +// 'sender': '@othertest:fakeServer.notExisting', +// 'content': { +// 'algorithm': 'm.megolm.v1.aes-sha2', +// 'room_id': '!726s6s6q:example.com', +// 'session_id': 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU', +// 'session_key': +// 'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw' +// }, +// 'type': 'm.room_key' +// }, + { // this is the commented out m.room_key event - only encrypted + 'sender': '@othertest:fakeServer.notExisting', 'content': { - 'algorithm': 'm.megolm.v1.aes-sha2', - 'room_id': '!726s6s6q:example.com', - 'session_id': 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU', - 'session_key': - 'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw' + 'algorithm': 'm.olm.v1.curve25519-aes-sha2', + 'sender_key': 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg', + 'ciphertext': { + '7rvl3jORJkBiK4XX1e5TnGnqz068XfYJ0W++Ml63rgk': { + 'type': 0, + 'body': 'Awogyh7K4iLUQjcOxIfi7q7LhBBqv9w0mQ6JI9+U9tv7iF4SIHC6xb5YFWf9voRnmDBbd+0vxD/xDlVNRDlPIKliLGkYGiAkEbtlo+fng4ELtO4gSLKVbcFn7tZwZCEUE8H2miBsCCKABgMKIFrKDJwB7gM3lXPt9yVoh6gQksafKt7VFCNRN5KLKqsDEAAi0AX5EfTV7jJ1ZWAbxftjoSN6kCVIxzGclbyg1HjchmNCX7nxNCHWl+q5ZgqHYZVu2n2mCVmIaKD0kvoEZeY3tV1Itb6zf67BLaU0qgW/QzHCHg5a44tNLjucvL2mumHjIG8k0BY2uh+52HeiMCvSOvtDwHg7nzCASGdqPVCj9Kzw6z7F6nL4e3mYim8zvJd7f+mD9z3ARrypUOLGkTGYbB2PQOovf0Do8WzcaRzfaUCnuu/YVZWKK7DPgG8uhw/TjR6XtraAKZysF+4DJYMG9SQWx558r6s7Z5EUOF5CU2M35w1t1Xxllb3vrS83dtf9LPCrBhLsEBeYEUBE2+bTBfl0BDKqLiB0Cc0N0ixOcHIt6e40wAvW622/gMgHlpNSx8xG12u0s6h6EMWdCXXLWd9fy2q6glFUHvA67A35q7O+M8DVml7Y9xG55Y3DHkMDc9cwgwFkBDCAYQe6pQF1nlKytcVCGREpBs/gq69gHAStMQ8WEg38Lf8u8eBr2DFexrN4U+QAk+S//P3fJgf0bQx/Eosx4fvWSz9En41iC+ADCsWQpMbwHn4JWvtAbn3oW0XmL/OgThTkJMLiCymduYAa1Hnt7a3tP0KTL2/x11F02ggQHL28cCjq5W4zUGjWjl5wo2PsKB6t8aAvMg2ujGD2rCjb4yrv5VIzAKMOZLyj7K0vSK9gwDLQ/4vq+QnKUBG5zrcOze0hX+kz2909/tmAdeCH61Ypw7gbPUJAKnmKYUiB/UgwkJvzMJSsk/SEs5SXosHDI+HsJHJp4Mp4iKD0xRMst+8f9aTjaWwh8ZvELE1ZOhhCbF3RXhxi3x2Nu8ORIz+vhEQ1NOlMc7UIo98Fk/96T36vL/fviowT4C/0AlaapZDJBmKwhmwqisMjY2n1vY29oM2p5BzY1iwP7q9BYdRFst6xwo57TNSuRwQw7IhFsf0k+ABuPEZy5xB5nPHyIRTf/pr3Hw', + }, + }, }, - 'type': 'm.room_key' + 'type': 'm.room.encrypted', }, ] }, From aa9764b51189ae76edc79596d2e5d41b90cfec35 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 5 Jun 2020 10:21:45 +0200 Subject: [PATCH 10/18] finish up olm manager tests --- test/encryption/olm_manager_test.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/encryption/olm_manager_test.dart b/test/encryption/olm_manager_test.dart index 5c2d2f1..1274432 100644 --- a/test/encryption/olm_manager_test.dart +++ b/test/encryption/olm_manager_test.dart @@ -98,5 +98,11 @@ void main() { FakeMatrixApi.calledEndpoints.containsKey('/client/r0/keys/upload'), false); }); + + test('startOutgoingOlmSessions', () async { + // start an olm session.....with ourself! + await client.encryption.olmManager.startOutgoingOlmSessions([client.userDeviceKeys[client.userID].deviceKeys[client.deviceID]]); + expect(client.encryption.olmManager.olmSessions.containsKey(client.identityKey), true); + }); }); } From 086dcae907344108f04f71011b41c23350249797 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 5 Jun 2020 10:51:11 +0200 Subject: [PATCH 11/18] add key manager tests --- lib/encryption/key_manager.dart | 5 + test/encryption/key_manager_test.dart | 162 ++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 test/encryption/key_manager_test.dart diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index 243d5a5..8a8ff42 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -147,6 +147,11 @@ class KeyManager { return sess; } + /// clear all cached inbound group sessions. useful for testing + void clearOutboundGroupSessions() { + _outboundGroupSessions.clear(); + } + /// Clears the existing outboundGroupSession but first checks if the participating /// devices have been changed. Returns false if the session has not been cleared because /// it wasn't necessary. diff --git a/test/encryption/key_manager_test.dart b/test/encryption/key_manager_test.dart new file mode 100644 index 0000000..3094332 --- /dev/null +++ b/test/encryption/key_manager_test.dart @@ -0,0 +1,162 @@ +/* + * 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 'dart:convert'; +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:test/test.dart'; +import 'package:olm/olm.dart' as olm; + +import '../fake_client.dart'; +import '../fake_matrix_api.dart'; + +void main() { + group('Key Manager', () { + 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; + + Client client; + + test('setupClient', () async { + client = await getClient(); + }); + + test('handle new m.room_key', () async { + final validSessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU'; + final validSenderKey = 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg'; + final sessionKey = 'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw'; + + + client.encryption.keyManager.clearInboundGroupSessions(); + var event = ToDeviceEvent( + sender: '@alice:example.com', + type: 'm.room_key', + content: { + 'algorithm': 'm.megolm.v1.aes-sha2', + 'room_id': '!726s6s6q:example.com', + 'session_id': validSessionId, + 'session_key': sessionKey, + }, + encryptedContent: { + 'sender_key': validSessionId, + }); + await client.encryption.keyManager.handleToDeviceEvent(event); + expect( + client.encryption.keyManager.getInboundGroupSession( + '!726s6s6q:example.com', validSessionId, validSenderKey) != + null, + true); + + // now test a few invalid scenarios + + // not encrypted + client.encryption.keyManager.clearInboundGroupSessions(); + event = ToDeviceEvent( + sender: '@alice:example.com', + type: 'm.room_key', + content: { + 'algorithm': 'm.megolm.v1.aes-sha2', + 'room_id': '!726s6s6q:example.com', + 'session_id': validSessionId, + 'session_key': sessionKey, + }); + await client.encryption.keyManager.handleToDeviceEvent(event); + expect( + client.encryption.keyManager.getInboundGroupSession( + '!726s6s6q:example.com', validSessionId, validSenderKey) != + null, + false); + }); + + test('outbound group session', () async { + final roomId = '!726s6s6q:example.com'; + expect(client.encryption.keyManager.getOutboundGroupSession(roomId) != null, false); + var sess = await client.encryption.keyManager.createOutboundGroupSession(roomId); + expect(client.encryption.keyManager.getOutboundGroupSession(roomId) != null, true); + await client.encryption.keyManager.clearOutboundGroupSession(roomId); + expect(client.encryption.keyManager.getOutboundGroupSession(roomId) != null, true); + expect(client.encryption.keyManager.getInboundGroupSession(roomId, sess.outboundGroupSession.session_id(), client.identityKey) != null, true); + + // rotate after too many messages + sess.sentMessages = 300; + await client.encryption.keyManager.clearOutboundGroupSession(roomId); + expect(client.encryption.keyManager.getOutboundGroupSession(roomId) != null, false); + + // rotate if devices in room change + sess = await client.encryption.keyManager.createOutboundGroupSession(roomId); + client.userDeviceKeys['@alice:example.com'].deviceKeys['JLAFKJWSCS'].blocked = true; + await client.encryption.keyManager.clearOutboundGroupSession(roomId); + expect(client.encryption.keyManager.getOutboundGroupSession(roomId) != null, false); + client.userDeviceKeys['@alice:example.com'].deviceKeys['JLAFKJWSCS'].blocked = false; + + // rotate if too far in the past + sess = await client.encryption.keyManager.createOutboundGroupSession(roomId); + sess.creationTime = DateTime.now().subtract(Duration(days: 30)); + await client.encryption.keyManager.clearOutboundGroupSession(roomId); + expect(client.encryption.keyManager.getOutboundGroupSession(roomId) != null, false); + + // force wipe + sess = await client.encryption.keyManager.createOutboundGroupSession(roomId); + await client.encryption.keyManager.clearOutboundGroupSession(roomId, wipe: true); + expect(client.encryption.keyManager.getOutboundGroupSession(roomId) != null, false); + + // load from database + sess = await client.encryption.keyManager.createOutboundGroupSession(roomId); + client.encryption.keyManager.clearOutboundGroupSessions(); + expect(client.encryption.keyManager.getOutboundGroupSession(roomId) != null, false); + await client.encryption.keyManager.loadOutboundGroupSession(roomId); + expect(client.encryption.keyManager.getOutboundGroupSession(roomId) != null, true); + }); + + test('inbound group session', () async { + final roomId = '!726s6s6q:example.com'; + final sessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU'; + final senderKey = 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg'; + final sessionContent = { + 'algorithm': 'm.megolm.v1.aes-sha2', + 'room_id': '!726s6s6q:example.com', + 'session_id': 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU', + 'session_key': + 'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw' + }; + client.encryption.keyManager.clearInboundGroupSessions(); + expect(client.encryption.keyManager.getInboundGroupSession(roomId, sessionId, senderKey) != null, false); + client.encryption.keyManager.setInboundGroupSession(roomId, sessionId, senderKey, sessionContent); + await Future.delayed(Duration(milliseconds: 10)); + expect(client.encryption.keyManager.getInboundGroupSession(roomId, sessionId, senderKey) != null, true); + + expect(client.encryption.keyManager.getInboundGroupSession(roomId, sessionId, senderKey) != null, true); + expect(client.encryption.keyManager.getInboundGroupSession('otherroom', sessionId, senderKey) != null, true); + expect(client.encryption.keyManager.getInboundGroupSession('otherroom', 'invalid', senderKey) != null, false); + + client.encryption.keyManager.clearInboundGroupSessions(); + expect(client.encryption.keyManager.getInboundGroupSession(roomId, sessionId, senderKey) != null, false); + await client.encryption.keyManager.loadInboundGroupSession(roomId, sessionId, senderKey); + expect(client.encryption.keyManager.getInboundGroupSession(roomId, sessionId, senderKey) != null, true); + + }); + }); +} From 8358dec3a5f2740999c1da62863a24601a7352a1 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 5 Jun 2020 10:56:51 +0200 Subject: [PATCH 12/18] analyze and format --- lib/encryption/key_manager.dart | 3 +- test/encryption/key_manager_test.dart | 159 ++++++++++++++------- test/encryption/key_verification_test.dart | 1 - test/encryption/olm_manager_test.dart | 8 +- test/fake_matrix_api.dart | 6 +- 5 files changed, 120 insertions(+), 57 deletions(-) diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index 8a8ff42..4121c11 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -437,7 +437,8 @@ class KeyManager { .deviceKeys[event.content['requesting_device_id']] .ed25519Key; } - setInboundGroupSession(roomId, sessionId, event.encryptedContent['sender_key'], event.content, + setInboundGroupSession(roomId, sessionId, + event.encryptedContent['sender_key'], event.content, forwarded: false); } } diff --git a/test/encryption/key_manager_test.dart b/test/encryption/key_manager_test.dart index 3094332..7f29edd 100644 --- a/test/encryption/key_manager_test.dart +++ b/test/encryption/key_manager_test.dart @@ -16,13 +16,11 @@ * along with this program. If not, see . */ -import 'dart:convert'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:test/test.dart'; import 'package:olm/olm.dart' as olm; import '../fake_client.dart'; -import '../fake_matrix_api.dart'; void main() { group('Key Manager', () { @@ -47,22 +45,22 @@ void main() { test('handle new m.room_key', () async { final validSessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU'; final validSenderKey = 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg'; - final sessionKey = 'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw'; - + final sessionKey = + 'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw'; client.encryption.keyManager.clearInboundGroupSessions(); var event = ToDeviceEvent( - sender: '@alice:example.com', - type: 'm.room_key', - content: { - 'algorithm': 'm.megolm.v1.aes-sha2', - 'room_id': '!726s6s6q:example.com', - 'session_id': validSessionId, - 'session_key': sessionKey, - }, - encryptedContent: { - 'sender_key': validSessionId, - }); + sender: '@alice:example.com', + type: 'm.room_key', + content: { + 'algorithm': 'm.megolm.v1.aes-sha2', + 'room_id': '!726s6s6q:example.com', + 'session_id': validSessionId, + 'session_key': sessionKey, + }, + encryptedContent: { + 'sender_key': validSessionId, + }); await client.encryption.keyManager.handleToDeviceEvent(event); expect( client.encryption.keyManager.getInboundGroupSession( @@ -75,14 +73,14 @@ void main() { // not encrypted client.encryption.keyManager.clearInboundGroupSessions(); event = ToDeviceEvent( - sender: '@alice:example.com', - type: 'm.room_key', - content: { - 'algorithm': 'm.megolm.v1.aes-sha2', - 'room_id': '!726s6s6q:example.com', - 'session_id': validSessionId, - 'session_key': sessionKey, - }); + sender: '@alice:example.com', + type: 'm.room_key', + content: { + 'algorithm': 'm.megolm.v1.aes-sha2', + 'room_id': '!726s6s6q:example.com', + 'session_id': validSessionId, + 'session_key': sessionKey, + }); await client.encryption.keyManager.handleToDeviceEvent(event); expect( client.encryption.keyManager.getInboundGroupSession( @@ -93,42 +91,72 @@ void main() { test('outbound group session', () async { final roomId = '!726s6s6q:example.com'; - expect(client.encryption.keyManager.getOutboundGroupSession(roomId) != null, false); - var sess = await client.encryption.keyManager.createOutboundGroupSession(roomId); - expect(client.encryption.keyManager.getOutboundGroupSession(roomId) != null, true); + expect( + client.encryption.keyManager.getOutboundGroupSession(roomId) != null, + false); + var sess = + await client.encryption.keyManager.createOutboundGroupSession(roomId); + expect( + client.encryption.keyManager.getOutboundGroupSession(roomId) != null, + true); await client.encryption.keyManager.clearOutboundGroupSession(roomId); - expect(client.encryption.keyManager.getOutboundGroupSession(roomId) != null, true); - expect(client.encryption.keyManager.getInboundGroupSession(roomId, sess.outboundGroupSession.session_id(), client.identityKey) != null, true); + expect( + client.encryption.keyManager.getOutboundGroupSession(roomId) != null, + true); + expect( + client.encryption.keyManager.getInboundGroupSession(roomId, + sess.outboundGroupSession.session_id(), client.identityKey) != + null, + true); // rotate after too many messages sess.sentMessages = 300; await client.encryption.keyManager.clearOutboundGroupSession(roomId); - expect(client.encryption.keyManager.getOutboundGroupSession(roomId) != null, false); + expect( + client.encryption.keyManager.getOutboundGroupSession(roomId) != null, + false); // rotate if devices in room change - sess = await client.encryption.keyManager.createOutboundGroupSession(roomId); - client.userDeviceKeys['@alice:example.com'].deviceKeys['JLAFKJWSCS'].blocked = true; + sess = + await client.encryption.keyManager.createOutboundGroupSession(roomId); + client.userDeviceKeys['@alice:example.com'].deviceKeys['JLAFKJWSCS'] + .blocked = true; await client.encryption.keyManager.clearOutboundGroupSession(roomId); - expect(client.encryption.keyManager.getOutboundGroupSession(roomId) != null, false); - client.userDeviceKeys['@alice:example.com'].deviceKeys['JLAFKJWSCS'].blocked = false; + expect( + client.encryption.keyManager.getOutboundGroupSession(roomId) != null, + false); + client.userDeviceKeys['@alice:example.com'].deviceKeys['JLAFKJWSCS'] + .blocked = false; // rotate if too far in the past - sess = await client.encryption.keyManager.createOutboundGroupSession(roomId); + sess = + await client.encryption.keyManager.createOutboundGroupSession(roomId); sess.creationTime = DateTime.now().subtract(Duration(days: 30)); await client.encryption.keyManager.clearOutboundGroupSession(roomId); - expect(client.encryption.keyManager.getOutboundGroupSession(roomId) != null, false); + expect( + client.encryption.keyManager.getOutboundGroupSession(roomId) != null, + false); // force wipe - sess = await client.encryption.keyManager.createOutboundGroupSession(roomId); - await client.encryption.keyManager.clearOutboundGroupSession(roomId, wipe: true); - expect(client.encryption.keyManager.getOutboundGroupSession(roomId) != null, false); + sess = + await client.encryption.keyManager.createOutboundGroupSession(roomId); + await client.encryption.keyManager + .clearOutboundGroupSession(roomId, wipe: true); + expect( + client.encryption.keyManager.getOutboundGroupSession(roomId) != null, + false); // load from database - sess = await client.encryption.keyManager.createOutboundGroupSession(roomId); + sess = + await client.encryption.keyManager.createOutboundGroupSession(roomId); client.encryption.keyManager.clearOutboundGroupSessions(); - expect(client.encryption.keyManager.getOutboundGroupSession(roomId) != null, false); + expect( + client.encryption.keyManager.getOutboundGroupSession(roomId) != null, + false); await client.encryption.keyManager.loadOutboundGroupSession(roomId); - expect(client.encryption.keyManager.getOutboundGroupSession(roomId) != null, true); + expect( + client.encryption.keyManager.getOutboundGroupSession(roomId) != null, + true); }); test('inbound group session', () async { @@ -143,20 +171,49 @@ void main() { 'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw' }; client.encryption.keyManager.clearInboundGroupSessions(); - expect(client.encryption.keyManager.getInboundGroupSession(roomId, sessionId, senderKey) != null, false); - client.encryption.keyManager.setInboundGroupSession(roomId, sessionId, senderKey, sessionContent); + expect( + client.encryption.keyManager + .getInboundGroupSession(roomId, sessionId, senderKey) != + null, + false); + client.encryption.keyManager + .setInboundGroupSession(roomId, sessionId, senderKey, sessionContent); await Future.delayed(Duration(milliseconds: 10)); - expect(client.encryption.keyManager.getInboundGroupSession(roomId, sessionId, senderKey) != null, true); + expect( + client.encryption.keyManager + .getInboundGroupSession(roomId, sessionId, senderKey) != + null, + true); - expect(client.encryption.keyManager.getInboundGroupSession(roomId, sessionId, senderKey) != null, true); - expect(client.encryption.keyManager.getInboundGroupSession('otherroom', sessionId, senderKey) != null, true); - expect(client.encryption.keyManager.getInboundGroupSession('otherroom', 'invalid', senderKey) != null, false); + expect( + client.encryption.keyManager + .getInboundGroupSession(roomId, sessionId, senderKey) != + null, + true); + expect( + client.encryption.keyManager + .getInboundGroupSession('otherroom', sessionId, senderKey) != + null, + true); + expect( + client.encryption.keyManager + .getInboundGroupSession('otherroom', 'invalid', senderKey) != + null, + false); client.encryption.keyManager.clearInboundGroupSessions(); - expect(client.encryption.keyManager.getInboundGroupSession(roomId, sessionId, senderKey) != null, false); - await client.encryption.keyManager.loadInboundGroupSession(roomId, sessionId, senderKey); - expect(client.encryption.keyManager.getInboundGroupSession(roomId, sessionId, senderKey) != null, true); - + expect( + client.encryption.keyManager + .getInboundGroupSession(roomId, sessionId, senderKey) != + null, + false); + await client.encryption.keyManager + .loadInboundGroupSession(roomId, sessionId, senderKey); + expect( + client.encryption.keyManager + .getInboundGroupSession(roomId, sessionId, senderKey) != + null, + true); }); }); } diff --git a/test/encryption/key_verification_test.dart b/test/encryption/key_verification_test.dart index 78a24d2..48825f0 100644 --- a/test/encryption/key_verification_test.dart +++ b/test/encryption/key_verification_test.dart @@ -43,7 +43,6 @@ void main() { var updateCounter = 0; KeyVerification keyVerification; - test('setupClient', () async { client = await getClient(); room = Room(id: '!localpart:server.abc', client: client); diff --git a/test/encryption/olm_manager_test.dart b/test/encryption/olm_manager_test.dart index 1274432..65c10d1 100644 --- a/test/encryption/olm_manager_test.dart +++ b/test/encryption/olm_manager_test.dart @@ -101,8 +101,12 @@ void main() { test('startOutgoingOlmSessions', () async { // start an olm session.....with ourself! - await client.encryption.olmManager.startOutgoingOlmSessions([client.userDeviceKeys[client.userID].deviceKeys[client.deviceID]]); - expect(client.encryption.olmManager.olmSessions.containsKey(client.identityKey), true); + await client.encryption.olmManager.startOutgoingOlmSessions( + [client.userDeviceKeys[client.userID].deviceKeys[client.deviceID]]); + expect( + client.encryption.olmManager.olmSessions + .containsKey(client.identityKey), + true); }); }); } diff --git a/test/fake_matrix_api.dart b/test/fake_matrix_api.dart index 0c5d256..f21b667 100644 --- a/test/fake_matrix_api.dart +++ b/test/fake_matrix_api.dart @@ -539,7 +539,8 @@ class FakeMatrixApi extends MockClient { // }, // 'type': 'm.room_key' // }, - { // this is the commented out m.room_key event - only encrypted + { + // this is the commented out m.room_key event - only encrypted 'sender': '@othertest:fakeServer.notExisting', 'content': { 'algorithm': 'm.olm.v1.curve25519-aes-sha2', @@ -547,7 +548,8 @@ class FakeMatrixApi extends MockClient { 'ciphertext': { '7rvl3jORJkBiK4XX1e5TnGnqz068XfYJ0W++Ml63rgk': { 'type': 0, - 'body': 'Awogyh7K4iLUQjcOxIfi7q7LhBBqv9w0mQ6JI9+U9tv7iF4SIHC6xb5YFWf9voRnmDBbd+0vxD/xDlVNRDlPIKliLGkYGiAkEbtlo+fng4ELtO4gSLKVbcFn7tZwZCEUE8H2miBsCCKABgMKIFrKDJwB7gM3lXPt9yVoh6gQksafKt7VFCNRN5KLKqsDEAAi0AX5EfTV7jJ1ZWAbxftjoSN6kCVIxzGclbyg1HjchmNCX7nxNCHWl+q5ZgqHYZVu2n2mCVmIaKD0kvoEZeY3tV1Itb6zf67BLaU0qgW/QzHCHg5a44tNLjucvL2mumHjIG8k0BY2uh+52HeiMCvSOvtDwHg7nzCASGdqPVCj9Kzw6z7F6nL4e3mYim8zvJd7f+mD9z3ARrypUOLGkTGYbB2PQOovf0Do8WzcaRzfaUCnuu/YVZWKK7DPgG8uhw/TjR6XtraAKZysF+4DJYMG9SQWx558r6s7Z5EUOF5CU2M35w1t1Xxllb3vrS83dtf9LPCrBhLsEBeYEUBE2+bTBfl0BDKqLiB0Cc0N0ixOcHIt6e40wAvW622/gMgHlpNSx8xG12u0s6h6EMWdCXXLWd9fy2q6glFUHvA67A35q7O+M8DVml7Y9xG55Y3DHkMDc9cwgwFkBDCAYQe6pQF1nlKytcVCGREpBs/gq69gHAStMQ8WEg38Lf8u8eBr2DFexrN4U+QAk+S//P3fJgf0bQx/Eosx4fvWSz9En41iC+ADCsWQpMbwHn4JWvtAbn3oW0XmL/OgThTkJMLiCymduYAa1Hnt7a3tP0KTL2/x11F02ggQHL28cCjq5W4zUGjWjl5wo2PsKB6t8aAvMg2ujGD2rCjb4yrv5VIzAKMOZLyj7K0vSK9gwDLQ/4vq+QnKUBG5zrcOze0hX+kz2909/tmAdeCH61Ypw7gbPUJAKnmKYUiB/UgwkJvzMJSsk/SEs5SXosHDI+HsJHJp4Mp4iKD0xRMst+8f9aTjaWwh8ZvELE1ZOhhCbF3RXhxi3x2Nu8ORIz+vhEQ1NOlMc7UIo98Fk/96T36vL/fviowT4C/0AlaapZDJBmKwhmwqisMjY2n1vY29oM2p5BzY1iwP7q9BYdRFst6xwo57TNSuRwQw7IhFsf0k+ABuPEZy5xB5nPHyIRTf/pr3Hw', + 'body': + 'Awogyh7K4iLUQjcOxIfi7q7LhBBqv9w0mQ6JI9+U9tv7iF4SIHC6xb5YFWf9voRnmDBbd+0vxD/xDlVNRDlPIKliLGkYGiAkEbtlo+fng4ELtO4gSLKVbcFn7tZwZCEUE8H2miBsCCKABgMKIFrKDJwB7gM3lXPt9yVoh6gQksafKt7VFCNRN5KLKqsDEAAi0AX5EfTV7jJ1ZWAbxftjoSN6kCVIxzGclbyg1HjchmNCX7nxNCHWl+q5ZgqHYZVu2n2mCVmIaKD0kvoEZeY3tV1Itb6zf67BLaU0qgW/QzHCHg5a44tNLjucvL2mumHjIG8k0BY2uh+52HeiMCvSOvtDwHg7nzCASGdqPVCj9Kzw6z7F6nL4e3mYim8zvJd7f+mD9z3ARrypUOLGkTGYbB2PQOovf0Do8WzcaRzfaUCnuu/YVZWKK7DPgG8uhw/TjR6XtraAKZysF+4DJYMG9SQWx558r6s7Z5EUOF5CU2M35w1t1Xxllb3vrS83dtf9LPCrBhLsEBeYEUBE2+bTBfl0BDKqLiB0Cc0N0ixOcHIt6e40wAvW622/gMgHlpNSx8xG12u0s6h6EMWdCXXLWd9fy2q6glFUHvA67A35q7O+M8DVml7Y9xG55Y3DHkMDc9cwgwFkBDCAYQe6pQF1nlKytcVCGREpBs/gq69gHAStMQ8WEg38Lf8u8eBr2DFexrN4U+QAk+S//P3fJgf0bQx/Eosx4fvWSz9En41iC+ADCsWQpMbwHn4JWvtAbn3oW0XmL/OgThTkJMLiCymduYAa1Hnt7a3tP0KTL2/x11F02ggQHL28cCjq5W4zUGjWjl5wo2PsKB6t8aAvMg2ujGD2rCjb4yrv5VIzAKMOZLyj7K0vSK9gwDLQ/4vq+QnKUBG5zrcOze0hX+kz2909/tmAdeCH61Ypw7gbPUJAKnmKYUiB/UgwkJvzMJSsk/SEs5SXosHDI+HsJHJp4Mp4iKD0xRMst+8f9aTjaWwh8ZvELE1ZOhhCbF3RXhxi3x2Nu8ORIz+vhEQ1NOlMc7UIo98Fk/96T36vL/fviowT4C/0AlaapZDJBmKwhmwqisMjY2n1vY29oM2p5BzY1iwP7q9BYdRFst6xwo57TNSuRwQw7IhFsf0k+ABuPEZy5xB5nPHyIRTf/pr3Hw', }, }, }, From f065a924455b8b0420060a38e97d5d492b92fc48 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 5 Jun 2020 11:32:02 +0200 Subject: [PATCH 13/18] fix coverage --- test/encryption/encrypt_decrypt_room_message_test.dart | 4 ++++ test/encryption/encrypt_decrypt_to_device_test.dart | 5 +++++ test/encryption/key_manager_test.dart | 4 ++++ test/encryption/key_verification_test.dart | 4 ++++ test/encryption/olm_manager_test.dart | 4 ++++ test/room_test.dart | 4 ++-- test/timeline_test.dart | 7 ++++--- 7 files changed, 27 insertions(+), 5 deletions(-) diff --git a/test/encryption/encrypt_decrypt_room_message_test.dart b/test/encryption/encrypt_decrypt_room_message_test.dart index f2a46da..7227134 100644 --- a/test/encryption/encrypt_decrypt_room_message_test.dart +++ b/test/encryption/encrypt_decrypt_room_message_test.dart @@ -91,5 +91,9 @@ void main() { expect(decryptedEvent.content['msgtype'], 'm.text'); expect(decryptedEvent.content['text'], 'Hello foxies!'); }); + + test('dispose client', () async { + await client.dispose(closeDatabase: true); + }); }); } diff --git a/test/encryption/encrypt_decrypt_to_device_test.dart b/test/encryption/encrypt_decrypt_to_device_test.dart index 7a35d6f..0be0a19 100644 --- a/test/encryption/encrypt_decrypt_to_device_test.dart +++ b/test/encryption/encrypt_decrypt_to_device_test.dart @@ -111,5 +111,10 @@ void main() { expect(decryptedEvent.type, 'm.to_device'); expect(decryptedEvent.content['hello'], 'superfoxies'); }); + + test('dispose client', () async { + await client.dispose(closeDatabase: true); + await otherClient.dispose(closeDatabase: true); + }); }); } diff --git a/test/encryption/key_manager_test.dart b/test/encryption/key_manager_test.dart index 7f29edd..aba2c34 100644 --- a/test/encryption/key_manager_test.dart +++ b/test/encryption/key_manager_test.dart @@ -215,5 +215,9 @@ void main() { null, true); }); + + test('dispose client', () async { + await client.dispose(closeDatabase: true); + }); }); } diff --git a/test/encryption/key_verification_test.dart b/test/encryption/key_verification_test.dart index 48825f0..1a9ddc9 100644 --- a/test/encryption/key_verification_test.dart +++ b/test/encryption/key_verification_test.dart @@ -99,5 +99,9 @@ void main() { expect(verified, true); keyVerification?.dispose(); }); + + test('dispose client', () async { + await client.dispose(closeDatabase: true); + }); }); } diff --git a/test/encryption/olm_manager_test.dart b/test/encryption/olm_manager_test.dart index 65c10d1..047d0ec 100644 --- a/test/encryption/olm_manager_test.dart +++ b/test/encryption/olm_manager_test.dart @@ -108,5 +108,9 @@ void main() { .containsKey(client.identityKey), true); }); + + test('dispose client', () async { + await client.dispose(closeDatabase: true); + }); }); } diff --git a/test/room_test.dart b/test/room_test.dart index 69000d6..0703e15 100644 --- a/test/room_test.dart +++ b/test/room_test.dart @@ -330,13 +330,13 @@ void main() { final dynamic resp = await room.sendEvent( {'msgtype': 'm.text', 'body': 'hello world'}, txid: 'testtxid'); - expect(resp, '\$event0'); + expect(resp.startsWith('\$event'), true); }); test('sendEvent', () async { final dynamic resp = await room.sendTextEvent('Hello world', txid: 'testtxid'); - expect(resp, '\$event1'); + expect(resp.startsWith('\$event'), true); }); // Not working because there is no real file to test it... diff --git a/test/timeline_test.dart b/test/timeline_test.dart index caf1309..9044a67 100644 --- a/test/timeline_test.dart +++ b/test/timeline_test.dart @@ -143,7 +143,8 @@ void main() { expect(updateCount, 5); expect(insertList, [0, 0, 0]); expect(insertList.length, timeline.events.length); - expect(timeline.events[0].eventId, '\$event0'); + final eventId = timeline.events[0].eventId; + expect(eventId.startsWith('\$event'), true); expect(timeline.events[0].status, 1); client.onEvent.add(EventUpdate( @@ -155,7 +156,7 @@ void main() { 'content': {'msgtype': 'm.text', 'body': 'test'}, 'sender': '@alice:example.com', 'status': 2, - 'event_id': '\$event0', + 'event_id': eventId, 'unsigned': {'transaction_id': '1234'}, 'origin_server_ts': DateTime.now().millisecondsSinceEpoch }, @@ -166,7 +167,7 @@ void main() { expect(updateCount, 6); expect(insertList, [0, 0, 0]); expect(insertList.length, timeline.events.length); - expect(timeline.events[0].eventId, '\$event0'); + expect(timeline.events[0].eventId, eventId); expect(timeline.events[0].status, 2); }); From fe3a697a1536d5ecfa05a1a06d9eb1b828dd6901 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 5 Jun 2020 12:07:06 +0200 Subject: [PATCH 14/18] fix test for non-olm --- test/client_test.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/client_test.dart b/test/client_test.dart index a73aad2..a316eba 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -317,7 +317,11 @@ void main() { expect(eventUpdateList.length, 2); expect(eventUpdateList[0].type, 'm.new_device'); - expect(eventUpdateList[1].type, 'm.room_key'); + if (matrix.encryptionEnabled) { + expect(eventUpdateList[1].type, 'm.room_key'); + } else { + expect(eventUpdateList[1].type, 'm.room.encrypted'); + } }); test('Login', () async { From 1c115ecf512eba1363413ccfd9791c0d90db20a6 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 5 Jun 2020 13:10:53 +0200 Subject: [PATCH 15/18] fix tests for real --- test/client_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/client_test.dart b/test/client_test.dart index a316eba..3963400 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -317,7 +317,7 @@ void main() { expect(eventUpdateList.length, 2); expect(eventUpdateList[0].type, 'm.new_device'); - if (matrix.encryptionEnabled) { + if (olmEnabled) { expect(eventUpdateList[1].type, 'm.room_key'); } else { expect(eventUpdateList[1].type, 'm.room.encrypted'); From 680e11ed61dec2d9865b6b5250cc3215aa8b6f2b Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 5 Jun 2020 15:24:40 +0200 Subject: [PATCH 16/18] test coverage thing --- pubspec.lock | 10 ++++++---- pubspec.yaml | 8 ++++++-- test.sh | 2 +- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 7f54f84..e243c74 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -596,10 +596,12 @@ packages: test_coverage: dependency: "direct dev" description: - name: test_coverage - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.1" + path: "." + ref: "6831abb314cc05e32b5d9140324e84069484b2eb" + resolved-ref: "6831abb314cc05e32b5d9140324e84069484b2eb" + url: "https://github.com/pulyaevskiy/test-coverage.git" + source: git + version: "0.4.2" timing: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1fb7673..91cd574 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,8 +29,12 @@ dependencies: dev_dependencies: test: ^1.0.0 - test_coverage: ^0.4.1 + test_coverage: + git: + url: https://github.com/pulyaevskiy/test-coverage.git + ref: 6831abb314cc05e32b5d9140324e84069484b2eb +# test_coverage: ^0.4.1 moor_generator: ^3.0.0 build_runner: ^1.5.2 pedantic: ^1.9.0 - moor_ffi: ^0.5.0 \ No newline at end of file + moor_ffi: ^0.5.0 diff --git a/test.sh b/test.sh index 5efc157..6cc1c7b 100644 --- a/test.sh +++ b/test.sh @@ -1,6 +1,6 @@ #!/bin/sh -e pub run test -p vm -pub run test_coverage +pub run test_coverage --print-test-output pub global activate remove_from_coverage pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '.g.dart$' genhtml -o coverage coverage/lcov.info || true From 22a5793e0754308d226d55c9449e3cabffa9db24 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 5 Jun 2020 15:34:13 +0200 Subject: [PATCH 17/18] hopefully fix coverage --- test/event_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/event_test.dart b/test/event_test.dart index 849141d..90b3223 100644 --- a/test/event_test.dart +++ b/test/event_test.dart @@ -220,7 +220,7 @@ void main() { event.status = -1; final resp2 = await event.sendAgain(txid: '1234'); expect(resp1, null); - expect(resp2, '\$event0'); + expect(resp2.startsWith('\$event'), true); await matrix.dispose(closeDatabase: true); }); From ac90481d1a6ab012274b1fb5397874e30dc2e2b4 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 5 Jun 2020 15:42:13 +0200 Subject: [PATCH 18/18] Revert "test coverage thing" This reverts commit 680e11ed61dec2d9865b6b5250cc3215aa8b6f2b. --- pubspec.lock | 10 ++++------ pubspec.yaml | 8 ++------ test.sh | 2 +- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index e243c74..7f54f84 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -596,12 +596,10 @@ packages: test_coverage: dependency: "direct dev" description: - path: "." - ref: "6831abb314cc05e32b5d9140324e84069484b2eb" - resolved-ref: "6831abb314cc05e32b5d9140324e84069484b2eb" - url: "https://github.com/pulyaevskiy/test-coverage.git" - source: git - version: "0.4.2" + name: test_coverage + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.1" timing: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 91cd574..1fb7673 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,12 +29,8 @@ dependencies: dev_dependencies: test: ^1.0.0 - test_coverage: - git: - url: https://github.com/pulyaevskiy/test-coverage.git - ref: 6831abb314cc05e32b5d9140324e84069484b2eb -# test_coverage: ^0.4.1 + test_coverage: ^0.4.1 moor_generator: ^3.0.0 build_runner: ^1.5.2 pedantic: ^1.9.0 - moor_ffi: ^0.5.0 + moor_ffi: ^0.5.0 \ No newline at end of file diff --git a/test.sh b/test.sh index 6cc1c7b..5efc157 100644 --- a/test.sh +++ b/test.sh @@ -1,6 +1,6 @@ #!/bin/sh -e pub run test -p vm -pub run test_coverage --print-test-output +pub run test_coverage pub global activate remove_from_coverage pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '.g.dart$' genhtml -o coverage coverage/lcov.info || true