diff --git a/lib/encryption.dart b/lib/encryption.dart new file mode 100644 index 00000000..ef4f3470 --- /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 00000000..add3ff2d --- /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 00000000..4234b676 --- /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 00000000..f8720f4c --- /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 00000000..2d783424 --- /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 b676a584..36984779 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 00000000..2a1c6170 --- /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 00000000..5c6f0b25 --- /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 da8813d9..39860aba 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 114bfcab..40036d6a 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 080ab210..eda8c5b5 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 61bc1c7f..9b86794e 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 ebe66ea4..073a71f3 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 ce48f995..fd2ef27e 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 0b5401dd..00000000 --- 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 331d773e..d418c905 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 b4286657..e7497c16 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 29754b84..5fc0c85c 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 758fa928..0be1c4e3 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 2155b212..00000000 --- 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 2232c158..09bef104 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 00000000..243f7765 --- /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 0594bd2c..039a38f4 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 ea1539bc..26028a23 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 6801c621..00000000 --- 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 2adcb987..11300c61 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 9540caef..00000000 --- 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 6ac0d097..d2307472 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);