Merge branch 'soru/modularize-e2ee' into 'master'
split encryption stuff to other library See merge request famedly/famedlysdk!333
This commit is contained in:
		
						commit
						b8c58faaab
					
				|  | @ -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 <https://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | library encryption; | ||||||
|  | 
 | ||||||
|  | export './encryption/encryption.dart'; | ||||||
|  | export './encryption/key_manager.dart'; | ||||||
|  | export './encryption/utils/key_verification.dart'; | ||||||
|  | @ -0,0 +1,283 @@ | ||||||
|  | /* | ||||||
|  |  *   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 <https://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | 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<void> init(String olmAccount) async { | ||||||
|  |     await olmManager.init(olmAccount); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   void handleDeviceOneTimeKeysCount(Map<String, int> countJson) { | ||||||
|  |     olmManager.handleDeviceOneTimeKeysCount(countJson); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   void onSync() { | ||||||
|  |     keyVerificationManager.cleanup(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Future<void> handleToDeviceEvent(ToDeviceEvent event) async { | ||||||
|  |     if (['m.room_key', 'm.room_key_request', 'm.forwarded_room_key'] | ||||||
|  |         .contains(event.type)) { | ||||||
|  |       // a new room key or thelike. We need to handle this asap, before other | ||||||
|  |       // events in /sync are handled | ||||||
|  |       await keyManager.handleToDeviceEvent(event); | ||||||
|  |     } | ||||||
|  |     if (event.type.startsWith('m.key.verification.')) { | ||||||
|  |       // some key verification event. No need to handle it now, we can easily | ||||||
|  |       // do this in the background | ||||||
|  |       unawaited(keyVerificationManager.handleToDeviceEvent(event)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Future<ToDeviceEvent> 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<String, dynamic> 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': <String, dynamic>{ | ||||||
|  |             '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<Event> 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<Map<String, dynamic>> encryptGroupMessagePayload( | ||||||
|  |       String roomId, Map<String, dynamic> 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<String, dynamic> mRelatesTo = payload.remove('m.relates_to'); | ||||||
|  |     final payloadContent = { | ||||||
|  |       'content': payload, | ||||||
|  |       'type': type, | ||||||
|  |       'room_id': roomId, | ||||||
|  |     }; | ||||||
|  |     var encryptedPayload = <String, dynamic>{ | ||||||
|  |       '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<Map<String, dynamic>> encryptToDeviceMessagePayload( | ||||||
|  |       DeviceKeys device, String type, Map<String, dynamic> payload) async { | ||||||
|  |     return await olmManager.encryptToDeviceMessagePayload( | ||||||
|  |         device, type, payload); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Future<Map<String, dynamic>> encryptToDeviceMessage( | ||||||
|  |       List<DeviceKeys> deviceKeys, | ||||||
|  |       String type, | ||||||
|  |       Map<String, dynamic> 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.'; | ||||||
|  | } | ||||||
|  | @ -0,0 +1,517 @@ | ||||||
|  | /* | ||||||
|  |  *   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 <https://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | 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 = <String, KeyManagerKeyShareRequest>{}; | ||||||
|  |   final incomingShareRequests = <String, KeyManagerKeyShareRequest>{}; | ||||||
|  |   final _inboundGroupSessions = <String, Map<String, SessionKey>>{}; | ||||||
|  |   final _outboundGroupSessions = <String, OutboundGroupSession>{}; | ||||||
|  |   final Set<String> _loadedOutboundGroupSessions = <String>{}; | ||||||
|  |   final Set<String> _requestedSessionIds = <String>{}; | ||||||
|  | 
 | ||||||
|  |   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<String, dynamic> 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] = <String, SessionKey>{}; | ||||||
|  |     } | ||||||
|  |     _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<SessionKey> 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] = <String, SessionKey>{}; | ||||||
|  |     } | ||||||
|  |     final sess = SessionKey.fromDb(session, client.userID); | ||||||
|  |     if (!sess.isValid) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |     _inboundGroupSessions[roomId][sessionId] = sess; | ||||||
|  |     return sess; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /// clear all cached inbound group sessions. useful for testing | ||||||
|  |   void clearOutboundGroupSessions() { | ||||||
|  |     _outboundGroupSessions.clear(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /// Clears the existing outboundGroupSession but first checks if the participating | ||||||
|  |   /// devices have been changed. Returns false if the session has not been cleared because | ||||||
|  |   /// it wasn't necessary. | ||||||
|  |   Future<bool> 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<void> 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<OutboundGroupSession> 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 = <String, dynamic>{ | ||||||
|  |       '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<void> 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<void> 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<void> 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; | ||||||
|  |       } | ||||||
|  |       setInboundGroupSession(roomId, sessionId, | ||||||
|  |           event.encryptedContent['sender_key'], 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<DeviceKeys> 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<void> 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 = <dynamic>[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); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -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 <https://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | 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<String, KeyVerification> _requests = {}; | ||||||
|  | 
 | ||||||
|  |   Future<void> 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<void> 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(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,433 @@ | ||||||
|  | /* | ||||||
|  |  *   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 <https://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | 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<String, List<olm.Session>> get olmSessions => _olmSessions; | ||||||
|  |   final Map<String, List<olm.Session>> _olmSessions = {}; | ||||||
|  | 
 | ||||||
|  |   Future<void> 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<String, dynamic> signJson(Map<String, dynamic> payload) { | ||||||
|  |     if (!enabled) throw ('Encryption is disabled'); | ||||||
|  |     final Map<String, dynamic> unsigned = payload['unsigned']; | ||||||
|  |     final Map<String, dynamic> 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'] = <String, dynamic>{}; | ||||||
|  |     } | ||||||
|  |     if (!payload['signatures'].containsKey(client.userID)) { | ||||||
|  |       payload['signatures'][client.userID] = <String, dynamic>{}; | ||||||
|  |     } | ||||||
|  |     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<String, dynamic> signedJson, | ||||||
|  |       String userId, String deviceId) { | ||||||
|  |     if (!enabled) throw ('Encryption is disabled'); | ||||||
|  |     final Map<String, dynamic> 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<bool> uploadKeys( | ||||||
|  |       {bool uploadDeviceKeys = false, int oldKeyCount = 0}) async { | ||||||
|  |     if (!enabled) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // generate one-time keys | ||||||
|  |     // we generate 2/3rds of max, so that other keys people may still have can | ||||||
|  |     // still be used | ||||||
|  |     final oneTimeKeysCount = | ||||||
|  |         (_olmAccount.max_number_of_one_time_keys() * 2 / 3).floor() - | ||||||
|  |             oldKeyCount; | ||||||
|  |     _olmAccount.generate_one_time_keys(oneTimeKeysCount); | ||||||
|  |     final Map<String, dynamic> oneTimeKeys = | ||||||
|  |         json.decode(_olmAccount.one_time_keys()); | ||||||
|  | 
 | ||||||
|  |     // now sign all the one-time keys | ||||||
|  |     final signedOneTimeKeys = <String, dynamic>{}; | ||||||
|  |     for (final entry in oneTimeKeys['curve25519'].entries) { | ||||||
|  |       final key = entry.key; | ||||||
|  |       final value = entry.value; | ||||||
|  |       signedOneTimeKeys['signed_curve25519:$key'] = <String, dynamic>{}; | ||||||
|  |       signedOneTimeKeys['signed_curve25519:$key'] = signJson({ | ||||||
|  |         'key': value, | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // and now generate the payload to upload | ||||||
|  |     final keysContent = <String, dynamic>{ | ||||||
|  |       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': <String, dynamic>{}, | ||||||
|  |         }, | ||||||
|  |     }; | ||||||
|  |     if (uploadDeviceKeys) { | ||||||
|  |       final Map<String, dynamic> 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<String, dynamic>); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     final response = await client.api.uploadDeviceKeys( | ||||||
|  |       deviceKeys: uploadDeviceKeys | ||||||
|  |           ? MatrixDeviceKeys.fromJson(keysContent['device_keys']) | ||||||
|  |           : null, | ||||||
|  |       oneTimeKeys: signedOneTimeKeys, | ||||||
|  |     ); | ||||||
|  |     _olmAccount.mark_keys_as_published(); | ||||||
|  |     await client.database?.updateClientKeys(pickledOlmAccount, client.id); | ||||||
|  |     return response['signed_curve25519'] == oneTimeKeysCount; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   void handleDeviceOneTimeKeysCount(Map<String, int> 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(oldKeyCount: countJson['signed_curve25519']); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   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(); | ||||||
|  |       try { | ||||||
|  |         newSession.create_inbound_from(_olmAccount, senderKey, body); | ||||||
|  |         _olmAccount.remove_one_time_keys(newSession); | ||||||
|  |         client.database?.updateClientKeys(pickledOlmAccount, client.id); | ||||||
|  |         plaintext = newSession.decrypt(type, body); | ||||||
|  |         storeOlmSession(senderKey, newSession); | ||||||
|  |       } catch (_) { | ||||||
|  |         newSession?.free(); | ||||||
|  |         rethrow; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     final Map<String, dynamic> 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<ToDeviceEvent> decryptToDeviceEvent(ToDeviceEvent event) async { | ||||||
|  |     if (event.type != EventTypes.Encrypted) { | ||||||
|  |       return event; | ||||||
|  |     } | ||||||
|  |     final senderKey = event.content['sender_key']; | ||||||
|  |     final loadFromDb = () async { | ||||||
|  |       if (client.database == null) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |       final sessions = await client.database | ||||||
|  |           .getSingleOlmSessions(client.id, senderKey, client.userID); | ||||||
|  |       if (sessions.isEmpty) { | ||||||
|  |         return false; // okay, can't do anything | ||||||
|  |       } | ||||||
|  |       _olmSessions[senderKey] = sessions; | ||||||
|  |       return true; | ||||||
|  |     }; | ||||||
|  |     if (!_olmSessions.containsKey(senderKey)) { | ||||||
|  |       await loadFromDb(); | ||||||
|  |     } | ||||||
|  |     event = _decryptToDeviceEvent(event); | ||||||
|  |     if (event.type != EventTypes.Encrypted || !(await loadFromDb())) { | ||||||
|  |       return event; | ||||||
|  |     } | ||||||
|  |     // retry to decrypt! | ||||||
|  |     return _decryptToDeviceEvent(event); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Future<void> startOutgoingOlmSessions(List<DeviceKeys> deviceKeys) async { | ||||||
|  |     var requestingKeysFrom = <String, Map<String, String>>{}; | ||||||
|  |     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<String, dynamic> deviceKey in deviceKeysEntry.value.values) { | ||||||
|  |           if (!checkJsonSignature( | ||||||
|  |               fingerprintKey, deviceKey, userId, deviceId)) { | ||||||
|  |             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<Map<String, dynamic>> encryptToDeviceMessagePayload( | ||||||
|  |       DeviceKeys device, String type, Map<String, dynamic> 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 = <String, dynamic>{ | ||||||
|  |       'algorithm': 'm.olm.v1.curve25519-aes-sha2', | ||||||
|  |       'sender_key': identityKey, | ||||||
|  |       'ciphertext': <String, dynamic>{}, | ||||||
|  |     }; | ||||||
|  |     encryptedBody['ciphertext'][device.curve25519Key] = { | ||||||
|  |       'type': encryptResult.type, | ||||||
|  |       'body': encryptResult.body, | ||||||
|  |     }; | ||||||
|  |     return encryptedBody; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Future<Map<String, dynamic>> encryptToDeviceMessage( | ||||||
|  |       List<DeviceKeys> deviceKeys, | ||||||
|  |       String type, | ||||||
|  |       Map<String, dynamic> payload) async { | ||||||
|  |     var data = <String, Map<String, Map<String, dynamic>>>{}; | ||||||
|  |     final deviceKeysWithoutSession = List<DeviceKeys>.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; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -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 <https://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
| import 'dart:typed_data'; | import 'dart:typed_data'; | ||||||
| import 'package:random_string/random_string.dart'; | import 'package:random_string/random_string.dart'; | ||||||
| import 'package:canonical_json/canonical_json.dart'; | import 'package:canonical_json/canonical_json.dart'; | ||||||
| import 'package:olm/olm.dart' as olm; | import 'package:olm/olm.dart' as olm; | ||||||
| import '../../matrix_api.dart'; | import 'package:famedlysdk/famedlysdk.dart'; | ||||||
| import 'device_keys_list.dart'; | import 'package:famedlysdk/matrix_api.dart'; | ||||||
| import '../client.dart'; | 
 | ||||||
| import '../room.dart'; | import '../encryption.dart'; | ||||||
| 
 | 
 | ||||||
| /* | /* | ||||||
|     +-------------+                    +-----------+ |     +-------------+                    +-----------+ | ||||||
|  | @ -53,6 +71,9 @@ enum KeyVerificationState { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| List<String> _intersect(List<String> a, List<dynamic> b) { | List<String> _intersect(List<String> a, List<dynamic> b) { | ||||||
|  |   if (b == null || a == null) { | ||||||
|  |     return []; | ||||||
|  |   } | ||||||
|   final res = <String>[]; |   final res = <String>[]; | ||||||
|   for (final v in a) { |   for (final v in a) { | ||||||
|     if (b.contains(v)) { |     if (b.contains(v)) { | ||||||
|  | @ -94,7 +115,8 @@ _KeyVerificationMethod _makeVerificationMethod( | ||||||
| 
 | 
 | ||||||
| class KeyVerification { | class KeyVerification { | ||||||
|   String transactionId; |   String transactionId; | ||||||
|   final Client client; |   final Encryption encryption; | ||||||
|  |   Client get client => encryption.client; | ||||||
|   final Room room; |   final Room room; | ||||||
|   final String userId; |   final String userId; | ||||||
|   void Function() onUpdate; |   void Function() onUpdate; | ||||||
|  | @ -114,7 +136,11 @@ class KeyVerification { | ||||||
|   String canceledReason; |   String canceledReason; | ||||||
| 
 | 
 | ||||||
|   KeyVerification( |   KeyVerification( | ||||||
|       {this.client, this.room, this.userId, String deviceId, this.onUpdate}) { |       {this.encryption, | ||||||
|  |       this.room, | ||||||
|  |       this.userId, | ||||||
|  |       String deviceId, | ||||||
|  |       this.onUpdate}) { | ||||||
|     lastActivity = DateTime.now(); |     lastActivity = DateTime.now(); | ||||||
|     _deviceId ??= deviceId; |     _deviceId ??= deviceId; | ||||||
|   } |   } | ||||||
|  | @ -384,7 +410,7 @@ class KeyVerification { | ||||||
|       final newTransactionId = await room.sendEvent(payload, type: type); |       final newTransactionId = await room.sendEvent(payload, type: type); | ||||||
|       if (transactionId == null) { |       if (transactionId == null) { | ||||||
|         transactionId = newTransactionId; |         transactionId = newTransactionId; | ||||||
|         client.addKeyVerificationRequest(this); |         encryption.keyVerificationManager.addRequest(this); | ||||||
|       } |       } | ||||||
|     } else { |     } else { | ||||||
|       await client.sendToDevice( |       await client.sendToDevice( | ||||||
|  | @ -404,10 +430,9 @@ class KeyVerification { | ||||||
| 
 | 
 | ||||||
| abstract class _KeyVerificationMethod { | abstract class _KeyVerificationMethod { | ||||||
|   KeyVerification request; |   KeyVerification request; | ||||||
|   Client client; |   Encryption get encryption => request.encryption; | ||||||
|   _KeyVerificationMethod({this.request}) { |   Client get client => request.client; | ||||||
|     client = request.client; |   _KeyVerificationMethod({this.request}); | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   Future<void> handlePayload(String type, Map<String, dynamic> payload); |   Future<void> handlePayload(String type, Map<String, dynamic> payload); | ||||||
|   bool validateStart(Map<String, dynamic> payload) { |   bool validateStart(Map<String, dynamic> payload) { | ||||||
|  | @ -662,7 +687,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod { | ||||||
|     // we would also add the cross signing key here |     // we would also add the cross signing key here | ||||||
|     final deviceKeyId = 'ed25519:${client.deviceID}'; |     final deviceKeyId = 'ed25519:${client.deviceID}'; | ||||||
|     mac[deviceKeyId] = |     mac[deviceKeyId] = | ||||||
|         _calculateMac(client.fingerprintKey, baseInfo + deviceKeyId); |         _calculateMac(encryption.fingerprintKey, baseInfo + deviceKeyId); | ||||||
|     keyList.add(deviceKeyId); |     keyList.add(deviceKeyId); | ||||||
| 
 | 
 | ||||||
|     keyList.sort(); |     keyList.sort(); | ||||||
|  | @ -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 <https://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import 'dart:convert'; | ||||||
|  | 
 | ||||||
|  | import 'package:olm/olm.dart' as olm; | ||||||
|  | import '../../src/database/database.dart' show DbOutboundGroupSession; | ||||||
|  | 
 | ||||||
|  | class OutboundGroupSession { | ||||||
|  |   List<String> 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<String>.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; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -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 <https://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | 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<String, dynamic> content; | ||||||
|  |   Map<String, int> indexes; | ||||||
|  |   olm.InboundGroupSession inboundGroupSession; | ||||||
|  |   final String key; | ||||||
|  |   List<dynamic> 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<String, dynamic>.from(parsedContent) : null; | ||||||
|  |     indexes = parsedIndexes != null | ||||||
|  |         ? Map<String, int>.from(parsedIndexes) | ||||||
|  |         : <String, int>{}; | ||||||
|  |     inboundGroupSession = olm.InboundGroupSession(); | ||||||
|  |     try { | ||||||
|  |       inboundGroupSession.unpickle(key, dbEntry.pickle); | ||||||
|  |     } catch (e) { | ||||||
|  |       dispose(); | ||||||
|  |       print('[LibOlm] Unable to unpickle inboundGroupSession: ' + e.toString()); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Map<String, dynamic> toJson() { | ||||||
|  |     final data = <String, dynamic>{}; | ||||||
|  |     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()); | ||||||
|  | } | ||||||
|  | @ -22,7 +22,6 @@ export 'matrix_api.dart'; | ||||||
| export 'package:famedlysdk/src/utils/room_update.dart'; | export 'package:famedlysdk/src/utils/room_update.dart'; | ||||||
| export 'package:famedlysdk/src/utils/event_update.dart'; | export 'package:famedlysdk/src/utils/event_update.dart'; | ||||||
| export 'package:famedlysdk/src/utils/device_keys_list.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_file.dart'; | ||||||
| export 'package:famedlysdk/src/utils/matrix_id_string_extension.dart'; | export 'package:famedlysdk/src/utils/matrix_id_string_extension.dart'; | ||||||
| export 'package:famedlysdk/src/utils/uri_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/utils/to_device_event.dart'; | ||||||
| export 'package:famedlysdk/src/client.dart'; | export 'package:famedlysdk/src/client.dart'; | ||||||
| export 'package:famedlysdk/src/event.dart'; | export 'package:famedlysdk/src/event.dart'; | ||||||
| export 'package:famedlysdk/src/key_manager.dart'; |  | ||||||
| export 'package:famedlysdk/src/room.dart'; | export 'package:famedlysdk/src/room.dart'; | ||||||
| export 'package:famedlysdk/src/timeline.dart'; | export 'package:famedlysdk/src/timeline.dart'; | ||||||
| export 'package:famedlysdk/src/user.dart'; | export 'package:famedlysdk/src/user.dart'; | ||||||
|  |  | ||||||
|  | @ -20,16 +20,14 @@ import 'dart:async'; | ||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
| import 'dart:core'; | import 'dart:core'; | ||||||
| 
 | 
 | ||||||
| import 'package:canonical_json/canonical_json.dart'; |  | ||||||
| import 'package:famedlysdk/famedlysdk.dart'; | import 'package:famedlysdk/famedlysdk.dart'; | ||||||
| import 'package:famedlysdk/matrix_api.dart'; | import 'package:famedlysdk/matrix_api.dart'; | ||||||
|  | import 'package:famedlysdk/encryption.dart'; | ||||||
| import 'package:famedlysdk/src/room.dart'; | import 'package:famedlysdk/src/room.dart'; | ||||||
| import 'package:famedlysdk/src/utils/device_keys_list.dart'; | import 'package:famedlysdk/src/utils/device_keys_list.dart'; | ||||||
| import 'package:famedlysdk/src/utils/matrix_file.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:famedlysdk/src/utils/to_device_event.dart'; | ||||||
| import 'package:http/http.dart' as http; | import 'package:http/http.dart' as http; | ||||||
| import 'package:olm/olm.dart' as olm; |  | ||||||
| import 'package:pedantic/pedantic.dart'; | import 'package:pedantic/pedantic.dart'; | ||||||
| 
 | 
 | ||||||
| import 'event.dart'; | import 'event.dart'; | ||||||
|  | @ -38,8 +36,6 @@ import 'utils/event_update.dart'; | ||||||
| import 'utils/room_update.dart'; | import 'utils/room_update.dart'; | ||||||
| import 'user.dart'; | import 'user.dart'; | ||||||
| import 'database/database.dart' show Database; | import 'database/database.dart' show Database; | ||||||
| import 'utils/key_verification.dart'; |  | ||||||
| import 'key_manager.dart'; |  | ||||||
| 
 | 
 | ||||||
| typedef RoomSorter = int Function(Room a, Room b); | typedef RoomSorter = int Function(Room a, Room b); | ||||||
| 
 | 
 | ||||||
|  | @ -53,12 +49,13 @@ class Client { | ||||||
|   int get id => _id; |   int get id => _id; | ||||||
| 
 | 
 | ||||||
|   Database database; |   Database database; | ||||||
|   KeyManager keyManager; |  | ||||||
| 
 | 
 | ||||||
|   bool enableE2eeRecovery; |   bool enableE2eeRecovery; | ||||||
| 
 | 
 | ||||||
|   MatrixApi api; |   MatrixApi api; | ||||||
| 
 | 
 | ||||||
|  |   Encryption encryption; | ||||||
|  | 
 | ||||||
|   /// Create a client |   /// Create a client | ||||||
|   /// clientName = unique identifier of this client |   /// clientName = unique identifier of this client | ||||||
|   /// debug: Print debug output? |   /// debug: Print debug output? | ||||||
|  | @ -70,7 +67,6 @@ class Client { | ||||||
|       this.enableE2eeRecovery = false, |       this.enableE2eeRecovery = false, | ||||||
|       http.Client httpClient}) { |       http.Client httpClient}) { | ||||||
|     api = MatrixApi(debug: debug, httpClient: httpClient); |     api = MatrixApi(debug: debug, httpClient: httpClient); | ||||||
|     keyManager = KeyManager(this); |  | ||||||
|     onLoginStateChanged.stream.listen((loginState) { |     onLoginStateChanged.stream.listen((loginState) { | ||||||
|       if (debug) { |       if (debug) { | ||||||
|         print('[LoginState]: ${loginState.toString()}'); |         print('[LoginState]: ${loginState.toString()}'); | ||||||
|  | @ -106,18 +102,14 @@ class Client { | ||||||
|   List<Room> get rooms => _rooms; |   List<Room> get rooms => _rooms; | ||||||
|   List<Room> _rooms = []; |   List<Room> _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. |   /// 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. |   /// 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! |   /// Warning! This endpoint is for testing only! | ||||||
|   set rooms(List<Room> newList) { |   set rooms(List<Room> newList) { | ||||||
|  | @ -529,8 +521,6 @@ class Client { | ||||||
|   final StreamController<KeyVerification> onKeyVerificationRequest = |   final StreamController<KeyVerification> onKeyVerificationRequest = | ||||||
|       StreamController.broadcast(); |       StreamController.broadcast(); | ||||||
| 
 | 
 | ||||||
|   final Map<String, KeyVerification> _keyVerificationRequests = {}; |  | ||||||
| 
 |  | ||||||
|   /// Matrix synchronisation is done with https long polling. This needs a |   /// Matrix synchronisation is done with https long polling. This needs a | ||||||
|   /// timeout which is usually 30 seconds. |   /// timeout which is usually 30 seconds. | ||||||
|   int syncTimeoutSec = 30; |   int syncTimeoutSec = 30; | ||||||
|  | @ -604,31 +594,15 @@ class Client { | ||||||
| 
 | 
 | ||||||
|     if (api.accessToken == null || api.homeserver == null || _userID == null) { |     if (api.accessToken == null || api.homeserver == null || _userID == null) { | ||||||
|       // we aren't logged in |       // we aren't logged in | ||||||
|  |       encryption?.dispose(); | ||||||
|  |       encryption = null; | ||||||
|       onLoginStateChanged.add(LoginState.loggedOut); |       onLoginStateChanged.add(LoginState.loggedOut); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Try to create a new olm account or restore a previous one. |     encryption = Encryption( | ||||||
|     if (olmAccount == null) { |         debug: debug, client: this, enableE2eeRecovery: enableE2eeRecovery); | ||||||
|       try { |     await encryption.init(olmAccount); | ||||||
|         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; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     if (database != null) { |     if (database != null) { | ||||||
|       if (id != null) { |       if (id != null) { | ||||||
|  | @ -639,7 +613,7 @@ class Client { | ||||||
|           _deviceID, |           _deviceID, | ||||||
|           _deviceName, |           _deviceName, | ||||||
|           prevBatch, |           prevBatch, | ||||||
|           pickledOlmAccount, |           encryption?.pickledOlmAccount, | ||||||
|           id, |           id, | ||||||
|         ); |         ); | ||||||
|       } else { |       } else { | ||||||
|  | @ -651,11 +625,10 @@ class Client { | ||||||
|           _deviceID, |           _deviceID, | ||||||
|           _deviceName, |           _deviceName, | ||||||
|           prevBatch, |           prevBatch, | ||||||
|           pickledOlmAccount, |           encryption?.pickledOlmAccount, | ||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
|       _userDeviceKeys = await database.getUserDeviceKeys(id); |       _userDeviceKeys = await database.getUserDeviceKeys(id); | ||||||
|       _olmSessions = await database.getOlmSessions(id, _userID); |  | ||||||
|       _rooms = await database.getRoomList(this, onlyLeft: false); |       _rooms = await database.getRoomList(this, onlyLeft: false); | ||||||
|       _sortRooms(); |       _sortRooms(); | ||||||
|       accountData = await database.getAccountData(id); |       accountData = await database.getAccountData(id); | ||||||
|  | @ -674,20 +647,12 @@ class Client { | ||||||
| 
 | 
 | ||||||
|   /// Resets all settings and stops the synchronisation. |   /// Resets all settings and stops the synchronisation. | ||||||
|   void clear() { |   void clear() { | ||||||
|     olmSessions.values.forEach((List<olm.Session> 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); |     database?.clear(id); | ||||||
|     _id = api.accessToken = |     _id = api.accessToken = | ||||||
|         api.homeserver = _userID = _deviceID = _deviceName = prevBatch = null; |         api.homeserver = _userID = _deviceID = _deviceName = prevBatch = null; | ||||||
|     _rooms = []; |     _rooms = []; | ||||||
|  |     encryption?.dispose(); | ||||||
|  |     encryption = null; | ||||||
|     onLoginStateChanged.add(LoginState.loggedOut); |     onLoginStateChanged.add(LoginState.loggedOut); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -723,7 +688,9 @@ class Client { | ||||||
|       } |       } | ||||||
|       prevBatch = syncResp.nextBatch; |       prevBatch = syncResp.nextBatch; | ||||||
|       await _updateUserDeviceKeys(); |       await _updateUserDeviceKeys(); | ||||||
|       _cleanupKeyVerificationRequests(); |       if (encryptionEnabled) { | ||||||
|  |         encryption.onSync(); | ||||||
|  |       } | ||||||
|       if (hash == _syncRequest.hashCode) unawaited(_sync()); |       if (hash == _syncRequest.hashCode) unawaited(_sync()); | ||||||
|     } on MatrixException catch (exception) { |     } on MatrixException catch (exception) { | ||||||
|       onError.add(exception); |       onError.add(exception); | ||||||
|  | @ -740,7 +707,7 @@ class Client { | ||||||
|   /// Use this method only for testing utilities! |   /// Use this method only for testing utilities! | ||||||
|   Future<void> handleSync(SyncUpdate sync) async { |   Future<void> handleSync(SyncUpdate sync) async { | ||||||
|     if (sync.toDevice != null) { |     if (sync.toDevice != null) { | ||||||
|       _handleToDeviceEvents(sync.toDevice); |       await _handleToDeviceEvents(sync.toDevice); | ||||||
|     } |     } | ||||||
|     if (sync.rooms != null) { |     if (sync.rooms != null) { | ||||||
|       if (sync.rooms.join != null) { |       if (sync.rooms.join != null) { | ||||||
|  | @ -784,31 +751,12 @@ class Client { | ||||||
|     if (sync.deviceLists != null) { |     if (sync.deviceLists != null) { | ||||||
|       await _handleDeviceListsEvents(sync.deviceLists); |       await _handleDeviceListsEvents(sync.deviceLists); | ||||||
|     } |     } | ||||||
|     if (sync.deviceOneTimeKeysCount != null) { |     if (sync.deviceOneTimeKeysCount != null && encryptionEnabled) { | ||||||
|       _handleDeviceOneTimeKeysCount(sync.deviceOneTimeKeysCount); |       encryption.handleDeviceOneTimeKeysCount(sync.deviceOneTimeKeysCount); | ||||||
|     } |  | ||||||
|     while (_pendingToDeviceEvents.isNotEmpty) { |  | ||||||
|       _updateRoomsByToDeviceEvent( |  | ||||||
|         _pendingToDeviceEvents.removeLast(), |  | ||||||
|         addToPendingIfNotFound: false, |  | ||||||
|       ); |  | ||||||
|     } |     } | ||||||
|     onSync.add(sync); |     onSync.add(sync); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   void _handleDeviceOneTimeKeysCount(Map<String, int> 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<void> _handleDeviceListsEvents(DeviceListsUpdate deviceLists) async { |   Future<void> _handleDeviceListsEvents(DeviceListsUpdate deviceLists) async { | ||||||
|     if (deviceLists.changed is List) { |     if (deviceLists.changed is List) { | ||||||
|       for (final userId in deviceLists.changed) { |       for (final userId in deviceLists.changed) { | ||||||
|  | @ -827,36 +775,12 @@ class Client { | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   void _cleanupKeyVerificationRequests() { |   Future<void> _handleToDeviceEvents(List<BasicEventWithSender> events) async { | ||||||
|     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<BasicEventWithSender> events) { |  | ||||||
|     for (var i = 0; i < events.length; i++) { |     for (var i = 0; i < events.length; i++) { | ||||||
|       var toDeviceEvent = ToDeviceEvent.fromJson(events[i].toJson()); |       var toDeviceEvent = ToDeviceEvent.fromJson(events[i].toJson()); | ||||||
|       if (toDeviceEvent.type == EventTypes.Encrypted) { |       if (toDeviceEvent.type == EventTypes.Encrypted && encryptionEnabled) { | ||||||
|         try { |         try { | ||||||
|           toDeviceEvent = decryptToDeviceEvent(toDeviceEvent); |           toDeviceEvent = await encryption.decryptToDeviceEvent(toDeviceEvent); | ||||||
|         } catch (e, s) { |         } catch (e, s) { | ||||||
|           print( |           print( | ||||||
|               '[LibOlm] Could not decrypt to device event from ${toDeviceEvent.sender} with content: ${toDeviceEvent.content}'); |               '[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()); |           toDeviceEvent = ToDeviceEvent.fromJson(events[i].toJson()); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       _updateRoomsByToDeviceEvent(toDeviceEvent); |       if (encryptionEnabled) { | ||||||
|       if (toDeviceEvent.type.startsWith('m.key.verification.')) { |         await encryption.handleToDeviceEvent(toDeviceEvent); | ||||||
|         _handleToDeviceKeyVerificationRequest(toDeviceEvent); |  | ||||||
|       } |  | ||||||
|       if (['m.room_key_request', 'm.forwarded_room_key'] |  | ||||||
|           .contains(toDeviceEvent.type)) { |  | ||||||
|         keyManager.handleToDeviceEvent(toDeviceEvent); |  | ||||||
|       } |       } | ||||||
|       onToDeviceEvent.add(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<void> _handleRooms( |   Future<void> _handleRooms( | ||||||
|       Map<String, SyncRoomUpdate> rooms, Membership membership) async { |       Map<String, SyncRoomUpdate> rooms, Membership membership) async { | ||||||
|     for (final entry in rooms.entries) { |     for (final entry in rooms.entries) { | ||||||
|  | @ -1056,14 +945,8 @@ class Client { | ||||||
|         content: event, |         content: event, | ||||||
|         sortOrder: sortOrder, |         sortOrder: sortOrder, | ||||||
|       ); |       ); | ||||||
|       if (event['type'] == EventTypes.Encrypted) { |       if (event['type'] == EventTypes.Encrypted && encryptionEnabled) { | ||||||
|         update = update.decrypt(room); |         update = await 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 (type != 'ephemeral' && database != null) { |       if (type != 'ephemeral' && database != null) { | ||||||
|         await database.storeEventUpdate(id, update); |         await database.storeEventUpdate(id, update); | ||||||
|  | @ -1187,42 +1070,6 @@ class Client { | ||||||
|     if (eventUpdate.type == 'timeline') _sortRooms(); |     if (eventUpdate.type == 'timeline') _sortRooms(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   final List<ToDeviceEvent> _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; |   bool _sortLock = false; | ||||||
| 
 | 
 | ||||||
|   /// The compare function how the rooms should be sorted internally. By default |   /// The compare function how the rooms should be sorted internally. By default | ||||||
|  | @ -1287,6 +1134,9 @@ class Client { | ||||||
| 
 | 
 | ||||||
|         for (final rawDeviceKeyListEntry in response.deviceKeys.entries) { |         for (final rawDeviceKeyListEntry in response.deviceKeys.entries) { | ||||||
|           final userId = rawDeviceKeyListEntry.key; |           final userId = rawDeviceKeyListEntry.key; | ||||||
|  |           if (!userDeviceKeys.containsKey(userId)) { | ||||||
|  |             _userDeviceKeys[userId] = DeviceKeysList(userId); | ||||||
|  |           } | ||||||
|           final oldKeys = |           final oldKeys = | ||||||
|               Map<String, DeviceKeys>.from(_userDeviceKeys[userId].deviceKeys); |               Map<String, DeviceKeys>.from(_userDeviceKeys[userId].deviceKeys); | ||||||
|           _userDeviceKeys[userId].deviceKeys = {}; |           _userDeviceKeys[userId].deviceKeys = {}; | ||||||
|  | @ -1301,7 +1151,7 @@ class Client { | ||||||
|               if (entry.isValid) { |               if (entry.isValid) { | ||||||
|                 _userDeviceKeys[userId].deviceKeys[deviceId] = entry; |                 _userDeviceKeys[userId].deviceKeys[deviceId] = entry; | ||||||
|                 if (deviceId == deviceID && |                 if (deviceId == deviceID && | ||||||
|                     entry.ed25519Key == fingerprintKey) { |                     entry.ed25519Key == encryption?.fingerprintKey) { | ||||||
|                   // Always trust the own device |                   // Always trust the own device | ||||||
|                   entry.verified = true; |                   entry.verified = true; | ||||||
|                 } |                 } | ||||||
|  | @ -1349,213 +1199,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<String, dynamic> signJson(Map<String, dynamic> payload) { |  | ||||||
|     if (!encryptionEnabled) throw ('Encryption is disabled'); |  | ||||||
|     final Map<String, dynamic> unsigned = payload['unsigned']; |  | ||||||
|     final Map<String, dynamic> 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'] = <String, dynamic>{}; |  | ||||||
|     } |  | ||||||
|     payload['signatures'][userID] = <String, dynamic>{}; |  | ||||||
|     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<String, dynamic> signedJson, |  | ||||||
|       String userId, String deviceId) { |  | ||||||
|     if (!encryptionEnabled) throw ('Encryption is disabled'); |  | ||||||
|     final Map<String, dynamic> 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<bool> _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<String, dynamic> oneTimeKeys = |  | ||||||
|         json.decode(_olmAccount.one_time_keys()); |  | ||||||
| 
 |  | ||||||
|     var signedOneTimeKeys = <String, dynamic>{}; |  | ||||||
| 
 |  | ||||||
|     for (String key in oneTimeKeys['curve25519'].keys) { |  | ||||||
|       signedOneTimeKeys['signed_curve25519:$key'] = <String, dynamic>{}; |  | ||||||
|       signedOneTimeKeys['signed_curve25519:$key']['key'] = |  | ||||||
|           oneTimeKeys['curve25519'][key]; |  | ||||||
|       signedOneTimeKeys['signed_curve25519:$key'] = |  | ||||||
|           signJson(signedOneTimeKeys['signed_curve25519:$key']); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     var keysContent = <String, dynamic>{ |  | ||||||
|       if (uploadDeviceKeys) |  | ||||||
|         'device_keys': { |  | ||||||
|           'user_id': userID, |  | ||||||
|           'device_id': deviceID, |  | ||||||
|           'algorithms': [ |  | ||||||
|             'm.olm.v1.curve25519-aes-sha2', |  | ||||||
|             'm.megolm.v1.aes-sha2' |  | ||||||
|           ], |  | ||||||
|           'keys': <String, dynamic>{}, |  | ||||||
|         }, |  | ||||||
|     }; |  | ||||||
|     if (uploadDeviceKeys) { |  | ||||||
|       final Map<String, dynamic> 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<String, dynamic>); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     _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<String, dynamic> 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<String, List<olm.Session>> get olmSessions => _olmSessions; |  | ||||||
|   Map<String, List<olm.Session>> _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 |   /// 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]. |   /// the request to all devices of the current user, pass an empty list to [deviceKeys]. | ||||||
|   Future<void> sendToDevice( |   Future<void> sendToDevice( | ||||||
|  | @ -1589,96 +1232,22 @@ class Client { | ||||||
|       } |       } | ||||||
|     } else { |     } else { | ||||||
|       if (encrypted) { |       if (encrypted) { | ||||||
|         // Create new sessions with devices if there is no existing session yet. |         data = | ||||||
|         var deviceKeysWithoutSession = List<DeviceKeys>.from(deviceKeys); |             await encryption.encryptToDeviceMessage(deviceKeys, type, message); | ||||||
|         deviceKeysWithoutSession.removeWhere((DeviceKeys deviceKeys) => |       } else { | ||||||
|             olmSessions.containsKey(deviceKeys.curve25519Key)); |         for (final device in deviceKeys) { | ||||||
|         if (deviceKeysWithoutSession.isNotEmpty) { |  | ||||||
|           await startOutgoingOlmSessions(deviceKeysWithoutSession); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       for (var i = 0; i < deviceKeys.length; i++) { |  | ||||||
|         var device = deviceKeys[i]; |  | ||||||
|           if (!data.containsKey(device.userId)) { |           if (!data.containsKey(device.userId)) { | ||||||
|             data[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': <String, dynamic>{}, |  | ||||||
|           }; |  | ||||||
|           sendToDeviceMessage['ciphertext'][device.curve25519Key] = { |  | ||||||
|             'type': encryptResult.type, |  | ||||||
|             'body': encryptResult.body, |  | ||||||
|           }; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|           data[device.userId][device.deviceId] = sendToDeviceMessage; |           data[device.userId][device.deviceId] = sendToDeviceMessage; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  |     } | ||||||
|     if (encrypted) type = EventTypes.Encrypted; |     if (encrypted) type = EventTypes.Encrypted; | ||||||
|     final messageID = generateUniqueTransactionId(); |     final messageID = generateUniqueTransactionId(); | ||||||
|     await api.sendToDevice(type, messageID, data); |     await api.sendToDevice(type, messageID, data); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   Future<void> startOutgoingOlmSessions(List<DeviceKeys> deviceKeys, |  | ||||||
|       {bool checkSignature = true}) async { |  | ||||||
|     var requestingKeysFrom = <String, Map<String, String>>{}; |  | ||||||
|     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<String, dynamic> 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] |   /// 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 |   /// rule of the push rules: https://matrix.org/docs/spec/client_server/r0.6.0#m-rule-master | ||||||
|   bool get allPushNotificationsMuted { |   bool get allPushNotificationsMuted { | ||||||
|  |  | ||||||
|  | @ -91,6 +91,22 @@ class Database extends _$Database { | ||||||
|     return res; |     return res; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   Future<List<olm.Session>> getSingleOlmSessions( | ||||||
|  |       int clientId, String identityKey, String userId) async { | ||||||
|  |     final rows = await dbGetOlmSessions(clientId, identityKey).get(); | ||||||
|  |     final res = <olm.Session>[]; | ||||||
|  |     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<DbOutboundGroupSession> getDbOutboundGroupSession( |   Future<DbOutboundGroupSession> getDbOutboundGroupSession( | ||||||
|       int clientId, String roomId) async { |       int clientId, String roomId) async { | ||||||
|     final res = await dbGetOutboundGroupSession(clientId, roomId).get(); |     final res = await dbGetOutboundGroupSession(clientId, roomId).get(); | ||||||
|  |  | ||||||
|  | @ -4851,6 +4851,19 @@ abstract class _$Database extends GeneratedDatabase { | ||||||
|         readsFrom: {olmSessions}).map(_rowToDbOlmSessions); |         readsFrom: {olmSessions}).map(_rowToDbOlmSessions); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   Selectable<DbOlmSessions> 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<int> storeOlmSession( |   Future<int> storeOlmSession( | ||||||
|       int client_id, String identitiy_key, String session_id, String pickle) { |       int client_id, String identitiy_key, String session_id, String pickle) { | ||||||
|     return customInsert( |     return customInsert( | ||||||
|  |  | ||||||
|  | @ -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; | getAllUserDeviceKeys: SELECT * FROM user_device_keys WHERE client_id = :client_id; | ||||||
| getAllUserDeviceKeysKeys: SELECT * FROM user_device_keys_key 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; | 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); | 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; | 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; | dbGetOutboundGroupSession: SELECT * FROM outbound_group_sessions WHERE client_id = :client_id AND room_id = :room_id; | ||||||
|  |  | ||||||
|  | @ -19,6 +19,7 @@ | ||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
| import 'dart:typed_data'; | import 'dart:typed_data'; | ||||||
| import 'package:famedlysdk/famedlysdk.dart'; | import 'package:famedlysdk/famedlysdk.dart'; | ||||||
|  | import 'package:famedlysdk/encryption.dart'; | ||||||
| import 'package:famedlysdk/src/utils/receipt.dart'; | import 'package:famedlysdk/src/utils/receipt.dart'; | ||||||
| import 'package:http/http.dart' as http; | import 'package:http/http.dart' as http; | ||||||
| import 'package:matrix_file_e2ee/matrix_file_e2ee.dart'; | import 'package:matrix_file_e2ee/matrix_file_e2ee.dart'; | ||||||
|  | @ -333,36 +334,6 @@ class Event extends MatrixEvent { | ||||||
|     return await timeline.getEventById(replyEventId); |     return await timeline.getEventById(replyEventId); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   Future<void> 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<Event> 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 |   /// If this event is encrypted and the decryption was not successful because | ||||||
|   /// the session is unknown, this requests the session key from other devices |   /// 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 |   /// in the room. If the event is not encrypted or the decryption failed because | ||||||
|  |  | ||||||
|  | @ -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 = <String, KeyManagerKeyShareRequest>{}; |  | ||||||
|   final incomingShareRequests = <String, KeyManagerKeyShareRequest>{}; |  | ||||||
| 
 |  | ||||||
|   KeyManager(this.client); |  | ||||||
| 
 |  | ||||||
|   /// Request a certain key from another device |  | ||||||
|   Future<void> 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<void> 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<DeviceKeys> 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<void> 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 = <dynamic>[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); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  | @ -17,21 +17,18 @@ | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| import 'dart:async'; | import 'dart:async'; | ||||||
| import 'dart:convert'; |  | ||||||
| 
 | 
 | ||||||
| import 'package:famedlysdk/matrix_api.dart'; | import 'package:famedlysdk/matrix_api.dart'; | ||||||
| import 'package:pedantic/pedantic.dart'; | import 'package:famedlysdk/encryption.dart'; | ||||||
| import 'package:famedlysdk/famedlysdk.dart'; | import 'package:famedlysdk/famedlysdk.dart'; | ||||||
| import 'package:famedlysdk/src/client.dart'; | import 'package:famedlysdk/src/client.dart'; | ||||||
| import 'package:famedlysdk/src/event.dart'; | import 'package:famedlysdk/src/event.dart'; | ||||||
| import 'package:famedlysdk/src/utils/event_update.dart'; | import 'package:famedlysdk/src/utils/event_update.dart'; | ||||||
| import 'package:famedlysdk/src/utils/room_update.dart'; | import 'package:famedlysdk/src/utils/room_update.dart'; | ||||||
| import 'package:famedlysdk/src/utils/matrix_file.dart'; | import 'package:famedlysdk/src/utils/matrix_file.dart'; | ||||||
| import 'package:famedlysdk/src/utils/session_key.dart'; |  | ||||||
| import 'package:image/image.dart'; | import 'package:image/image.dart'; | ||||||
| import 'package:matrix_file_e2ee/matrix_file_e2ee.dart'; | import 'package:matrix_file_e2ee/matrix_file_e2ee.dart'; | ||||||
| import 'package:mime_type/mime_type.dart'; | import 'package:mime_type/mime_type.dart'; | ||||||
| import 'package:olm/olm.dart' as olm; |  | ||||||
| import 'package:html_unescape/html_unescape.dart'; | import 'package:html_unescape/html_unescape.dart'; | ||||||
| 
 | 
 | ||||||
| import './user.dart'; | import './user.dart'; | ||||||
|  | @ -81,13 +78,6 @@ class Room { | ||||||
|   /// Key-Value store for private account data only visible for this user. |   /// Key-Value store for private account data only visible for this user. | ||||||
|   Map<String, BasicRoomEvent> roomAccountData = {}; |   Map<String, BasicRoomEvent> roomAccountData = {}; | ||||||
| 
 | 
 | ||||||
|   olm.OutboundGroupSession get outboundGroupSession => _outboundGroupSession; |  | ||||||
|   olm.OutboundGroupSession _outboundGroupSession; |  | ||||||
| 
 |  | ||||||
|   List<String> _outboundGroupSessionDevices; |  | ||||||
|   DateTime _outboundGroupSessionCreationTime; |  | ||||||
|   int _outboundGroupSessionSentMessages; |  | ||||||
| 
 |  | ||||||
|   double _newestSortOrder; |   double _newestSortOrder; | ||||||
|   double _oldestSortOrder; |   double _oldestSortOrder; | ||||||
| 
 | 
 | ||||||
|  | @ -110,168 +100,6 @@ class Room { | ||||||
|         _oldestSortOrder, _newestSortOrder, client.id, id); |         _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<void> createOutboundGroupSession() async { |  | ||||||
|     await clearOutboundGroupSession(wipe: true); |  | ||||||
|     var deviceKeys = await getUserDeviceKeys(); |  | ||||||
|     olm.OutboundGroupSession outboundGroupSession; |  | ||||||
|     var outboundGroupSessionDevices = <String>[]; |  | ||||||
|     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 = <String, dynamic>{ |  | ||||||
|       '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<void> _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<bool> clearOutboundGroupSession({bool wipe = false}) async { |  | ||||||
|     if (!wipe && _outboundGroupSessionDevices != null) { |  | ||||||
|       // first check if the devices in the room changed |  | ||||||
|       var deviceKeys = await getUserDeviceKeys(); |  | ||||||
|       var outboundGroupSessionDevices = <String>[]; |  | ||||||
|       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<String, SessionKey> get inboundGroupSessions => _inboundGroupSessions; |  | ||||||
|   final _inboundGroupSessions = <String, SessionKey>{}; |  | ||||||
| 
 |  | ||||||
|   /// Add a new session key to the [sessionKeys]. |  | ||||||
|   void setInboundGroupSession(String sessionId, Map<String, dynamic> 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<void> _tryAgainDecryptLastMessage() async { |  | ||||||
|     if (getState(EventTypes.Encrypted) != null) { |  | ||||||
|       await getState(EventTypes.Encrypted).decryptAndStore(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Returns the [Event] for the given [typeKey] and optional [stateKey]. |   /// Returns the [Event] for the given [typeKey] and optional [stateKey]. | ||||||
|   /// If no [stateKey] is provided, it defaults to an empty string. |   /// If no [stateKey] is provided, it defaults to an empty string. | ||||||
|   Event getState(String typeKey, [String stateKey = '']) => |   Event getState(String typeKey, [String stateKey = '']) => | ||||||
|  | @ -281,23 +109,13 @@ class Room { | ||||||
|   /// typeKey/stateKey key pair if there is one. |   /// typeKey/stateKey key pair if there is one. | ||||||
|   void setState(Event state) { |   void setState(Event state) { | ||||||
|     // Decrypt if necessary |     // Decrypt if necessary | ||||||
|     if (state.type == EventTypes.Encrypted) { |     if (state.type == EventTypes.Encrypted && client.encryptionEnabled) { | ||||||
|       try { |       try { | ||||||
|         state = decryptGroupMessage(state); |         state = client.encryption.decryptRoomEventSync(id, state); | ||||||
|       } catch (e) { |       } catch (e) { | ||||||
|         print('[LibOlm] Could not decrypt room state: ' + e.toString()); |         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) > |     if ((getState(state.type)?.originServerTs?.millisecondsSinceEpoch ?? 0) > | ||||||
|         (state.originServerTs?.millisecondsSinceEpoch ?? 1)) { |         (state.originServerTs?.millisecondsSinceEpoch ?? 1)) { | ||||||
|       return; |       return; | ||||||
|  | @ -883,7 +701,8 @@ class Room { | ||||||
|     // Send the text and on success, store and display a *sent* event. |     // Send the text and on success, store and display a *sent* event. | ||||||
|     try { |     try { | ||||||
|       final sendMessageContent = encrypted && client.encryptionEnabled |       final sendMessageContent = encrypted && client.encryptionEnabled | ||||||
|           ? await encryptGroupMessagePayload(content, type: type) |           ? await client.encryption | ||||||
|  |               .encryptGroupMessagePayload(id, content, type: type) | ||||||
|           : content; |           : content; | ||||||
|       final res = await client.api.sendMessage( |       final res = await client.api.sendMessage( | ||||||
|         id, |         id, | ||||||
|  | @ -999,55 +818,42 @@ class Room { | ||||||
|     if (onHistoryReceived != null) onHistoryReceived(); |     if (onHistoryReceived != null) onHistoryReceived(); | ||||||
|     prev_batch = resp.end; |     prev_batch = resp.end; | ||||||
| 
 | 
 | ||||||
|     final dbActions = <Future<dynamic> Function()>[]; |     final loadFn = () async { | ||||||
|     if (client.database != null) { |  | ||||||
|       dbActions.add( |  | ||||||
|           () => client.database.setRoomPrevBatch(prev_batch, client.id, id)); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|       if (!((resp.chunk?.isNotEmpty ?? false) && resp.end != null)) return; |       if (!((resp.chunk?.isNotEmpty ?? false) && resp.end != null)) return; | ||||||
| 
 | 
 | ||||||
|       if (resp.state != null) { |       if (resp.state != null) { | ||||||
|         for (final state in resp.state) { |         for (final state in resp.state) { | ||||||
|         var eventUpdate = EventUpdate( |           await EventUpdate( | ||||||
|             type: 'state', |             type: 'state', | ||||||
|             roomID: id, |             roomID: id, | ||||||
|             eventType: state.type, |             eventType: state.type, | ||||||
|             content: state.toJson(), |             content: state.toJson(), | ||||||
|             sortOrder: oldSortOrder, |             sortOrder: oldSortOrder, | ||||||
|         ).decrypt(this); |           ).decrypt(this, store: true); | ||||||
|         client.onEvent.add(eventUpdate); |  | ||||||
|         if (client.database != null) { |  | ||||||
|           dbActions.add( |  | ||||||
|               () => client.database.storeEventUpdate(client.id, eventUpdate)); |  | ||||||
|         } |  | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       for (final hist in resp.chunk) { |       for (final hist in resp.chunk) { | ||||||
|       var eventUpdate = EventUpdate( |         final eventUpdate = await EventUpdate( | ||||||
|           type: 'history', |           type: 'history', | ||||||
|           roomID: id, |           roomID: id, | ||||||
|           eventType: hist.type, |           eventType: hist.type, | ||||||
|           content: hist.toJson(), |           content: hist.toJson(), | ||||||
|           sortOrder: oldSortOrder, |           sortOrder: oldSortOrder, | ||||||
|       ).decrypt(this); |         ).decrypt(this, store: true); | ||||||
|         client.onEvent.add(eventUpdate); |         client.onEvent.add(eventUpdate); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|     if (client.database != null) { |     if (client.database != null) { | ||||||
|         dbActions.add( |       await client.database.transaction(() async { | ||||||
|             () => client.database.storeEventUpdate(client.id, eventUpdate)); |         await client.database.setRoomPrevBatch(resp.end, client.id, id); | ||||||
|       } |         await loadFn(); | ||||||
|     } |  | ||||||
|     if (client.database != null) { |  | ||||||
|       dbActions |  | ||||||
|           .add(() => client.database.setRoomPrevBatch(resp.end, client.id, id)); |  | ||||||
|     } |  | ||||||
|     await client.database?.transaction(() async { |  | ||||||
|       for (final f in dbActions) { |  | ||||||
|         await f(); |  | ||||||
|       } |  | ||||||
|         await updateSortOrder(); |         await updateSortOrder(); | ||||||
|       }); |       }); | ||||||
|  |     } else { | ||||||
|  |       await loadFn(); | ||||||
|  |     } | ||||||
|     client.onRoomUpdate.add( |     client.onRoomUpdate.add( | ||||||
|       RoomUpdate( |       RoomUpdate( | ||||||
|         id: id, |         id: id, | ||||||
|  | @ -1147,7 +953,6 @@ class Room { | ||||||
|       } |       } | ||||||
|       for (final rawState in rawStates) { |       for (final rawState in rawStates) { | ||||||
|         final newState = Event.fromDb(rawState, newRoom); |         final newState = Event.fromDb(rawState, newRoom); | ||||||
|         ; |  | ||||||
|         newRoom.setState(newState); |         newRoom.setState(newState); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  | @ -1187,13 +992,13 @@ class Room { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Try again to decrypt encrypted events and update the database. |     // 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 { |       await client.database.transaction(() async { | ||||||
|         for (var i = 0; i < events.length; i++) { |         for (var i = 0; i < events.length; i++) { | ||||||
|           if (events[i].type == EventTypes.Encrypted && |           if (events[i].type == EventTypes.Encrypted && | ||||||
|               events[i].content['body'] == DecryptError.UNKNOWN_SESSION) { |               events[i].content['body'] == DecryptError.UNKNOWN_SESSION) { | ||||||
|             await events[i].loadSession(); |             events[i] = await client.encryption | ||||||
|             events[i] = await events[i].decryptAndStore(); |                 .decryptRoomEvent(id, events[i], store: true); | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|  | @ -1746,209 +1551,10 @@ class Room { | ||||||
|     return deviceKeys; |     return deviceKeys; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   bool _restoredOutboundGroupSession = false; |  | ||||||
| 
 |  | ||||||
|   Future<void> 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<String>.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<Map<String, dynamic>> encryptGroupMessagePayload( |  | ||||||
|       Map<String, dynamic> 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<String, dynamic> mRelatesTo = payload.remove('m.relates_to'); |  | ||||||
|     final payloadContent = { |  | ||||||
|       'content': payload, |  | ||||||
|       'type': type, |  | ||||||
|       'room_id': id, |  | ||||||
|     }; |  | ||||||
|     var encryptedPayload = <String, dynamic>{ |  | ||||||
|       '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<String> _requestedSessionIds = <String>{}; |  | ||||||
| 
 |  | ||||||
|   Future<void> requestSessionKey(String sessionId, String senderKey) async { |   Future<void> requestSessionKey(String sessionId, String senderKey) async { | ||||||
|     await client.keyManager.request(this, sessionId, senderKey); |     if (!client.encryptionEnabled) { | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   Future<void> 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; |       return; | ||||||
|     } |     } | ||||||
|     try { |     await client.encryption.keyManager.request(this, sessionId, senderKey); | ||||||
|       _inboundGroupSessions[sessionId] = |  | ||||||
|           SessionKey.fromDb(session, client.userID); |  | ||||||
|     } catch (e) { |  | ||||||
|       print('[LibOlm] Could not unpickle inboundGroupSession: ' + e.toString()); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 |  | ||||||
|   Future<void> 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); |  | ||||||
|     } |  | ||||||
|     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<String, dynamic> 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': <String, dynamic>{ |  | ||||||
|             '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, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 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.'; |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -19,6 +19,7 @@ | ||||||
| import 'dart:async'; | import 'dart:async'; | ||||||
| 
 | 
 | ||||||
| import 'package:famedlysdk/matrix_api.dart'; | import 'package:famedlysdk/matrix_api.dart'; | ||||||
|  | import 'package:famedlysdk/encryption.dart'; | ||||||
| 
 | 
 | ||||||
| import 'event.dart'; | import 'event.dart'; | ||||||
| import 'room.dart'; | import 'room.dart'; | ||||||
|  | @ -97,12 +98,16 @@ class Timeline { | ||||||
|   void _sessionKeyReceived(String sessionId) async { |   void _sessionKeyReceived(String sessionId) async { | ||||||
|     var decryptAtLeastOneEvent = false; |     var decryptAtLeastOneEvent = false; | ||||||
|     final decryptFn = () async { |     final decryptFn = () async { | ||||||
|  |       if (!room.client.encryptionEnabled) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|       for (var i = 0; i < events.length; i++) { |       for (var i = 0; i < events.length; i++) { | ||||||
|         if (events[i].type == EventTypes.Encrypted && |         if (events[i].type == EventTypes.Encrypted && | ||||||
|             events[i].messageType == MessageTypes.BadEncrypted && |             events[i].messageType == MessageTypes.BadEncrypted && | ||||||
|             events[i].content['body'] == DecryptError.UNKNOWN_SESSION && |             events[i].content['body'] == DecryptError.UNKNOWN_SESSION && | ||||||
|             events[i].content['session_id'] == sessionId) { |             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) { |           if (events[i].type != EventTypes.Encrypted) { | ||||||
|             decryptAtLeastOneEvent = true; |             decryptAtLeastOneEvent = true; | ||||||
|           } |           } | ||||||
|  |  | ||||||
|  | @ -1,11 +1,11 @@ | ||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
| 
 | 
 | ||||||
| import 'package:famedlysdk/matrix_api.dart'; | import 'package:famedlysdk/matrix_api.dart'; | ||||||
|  | import 'package:famedlysdk/encryption.dart'; | ||||||
| 
 | 
 | ||||||
| import '../client.dart'; | import '../client.dart'; | ||||||
| import '../database/database.dart' show DbUserDeviceKey, DbUserDeviceKeysKey; | import '../database/database.dart' show DbUserDeviceKey, DbUserDeviceKeysKey; | ||||||
| import '../event.dart'; | import '../event.dart'; | ||||||
| import 'key_verification.dart'; |  | ||||||
| 
 | 
 | ||||||
| class DeviceKeysList { | class DeviceKeysList { | ||||||
|   String userId; |   String userId; | ||||||
|  | @ -78,12 +78,6 @@ class DeviceKeys extends MatrixDeviceKeys { | ||||||
| 
 | 
 | ||||||
|   Future<void> setBlocked(bool newBlocked, Client client) { |   Future<void> setBlocked(bool newBlocked, Client client) { | ||||||
|     blocked = newBlocked; |     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 |     return client.database | ||||||
|         ?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId); |         ?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId); | ||||||
|   } |   } | ||||||
|  | @ -157,10 +151,10 @@ class DeviceKeys extends MatrixDeviceKeys { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   KeyVerification startVerification(Client client) { |   KeyVerification startVerification(Client client) { | ||||||
|     final request = |     final request = KeyVerification( | ||||||
|         KeyVerification(client: client, userId: userId, deviceId: deviceId); |         encryption: client.encryption, userId: userId, deviceId: deviceId); | ||||||
|     request.start(); |     request.start(); | ||||||
|     client.addKeyVerificationRequest(request); |     client.encryption.keyVerificationManager.addRequest(request); | ||||||
|     return request; |     return request; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -42,13 +42,14 @@ class EventUpdate { | ||||||
|   EventUpdate( |   EventUpdate( | ||||||
|       {this.eventType, this.roomID, this.type, this.content, this.sortOrder}); |       {this.eventType, this.roomID, this.type, this.content, this.sortOrder}); | ||||||
| 
 | 
 | ||||||
|   EventUpdate decrypt(Room room) { |   Future<EventUpdate> decrypt(Room room, {bool store = false}) async { | ||||||
|     if (eventType != EventTypes.Encrypted) { |     if (eventType != EventTypes.Encrypted || !room.client.encryptionEnabled) { | ||||||
|       return this; |       return this; | ||||||
|     } |     } | ||||||
|     try { |     try { | ||||||
|       var decrpytedEvent = |       var decrpytedEvent = await room.client.encryption.decryptRoomEvent( | ||||||
|           room.decryptGroupMessage(Event.fromJson(content, room, sortOrder)); |           room.id, Event.fromJson(content, room, sortOrder), | ||||||
|  |           store: store, updateType: type); | ||||||
|       return EventUpdate( |       return EventUpdate( | ||||||
|         eventType: decrpytedEvent.type, |         eventType: decrpytedEvent.type, | ||||||
|         roomID: roomID, |         roomID: roomID, | ||||||
|  |  | ||||||
|  | @ -1,59 +0,0 @@ | ||||||
| import 'dart:convert'; |  | ||||||
| 
 |  | ||||||
| import 'package:olm/olm.dart'; |  | ||||||
| 
 |  | ||||||
| import '../database/database.dart' show DbInboundGroupSession; |  | ||||||
| import '../event.dart'; |  | ||||||
| 
 |  | ||||||
| class SessionKey { |  | ||||||
|   Map<String, dynamic> content; |  | ||||||
|   Map<String, int> indexes; |  | ||||||
|   InboundGroupSession inboundGroupSession; |  | ||||||
|   final String key; |  | ||||||
|   List<dynamic> 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<String, dynamic>.from(parsedContent) : null; |  | ||||||
|     indexes = parsedIndexes != null |  | ||||||
|         ? Map<String, int>.from(parsedIndexes) |  | ||||||
|         : <String, int>{}; |  | ||||||
|     var newInboundGroupSession = InboundGroupSession(); |  | ||||||
|     newInboundGroupSession.unpickle(key, dbEntry.pickle); |  | ||||||
|     inboundGroupSession = newInboundGroupSession; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   SessionKey.fromJson(Map<String, dynamic> json, String key) : key = key { |  | ||||||
|     content = json['content'] != null |  | ||||||
|         ? Map<String, dynamic>.from(json['content']) |  | ||||||
|         : null; |  | ||||||
|     indexes = json['indexes'] != null |  | ||||||
|         ? Map<String, int>.from(json['indexes']) |  | ||||||
|         : <String, int>{}; |  | ||||||
|     var newInboundGroupSession = InboundGroupSession(); |  | ||||||
|     newInboundGroupSession.unpickle(key, json['inboundGroupSession']); |  | ||||||
|     inboundGroupSession = newInboundGroupSession; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   Map<String, dynamic> toJson() { |  | ||||||
|     final data = <String, dynamic>{}; |  | ||||||
|     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()); |  | ||||||
| } |  | ||||||
|  | @ -17,7 +17,6 @@ | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| import 'dart:async'; | import 'dart:async'; | ||||||
| import 'dart:convert'; |  | ||||||
| import 'dart:typed_data'; | import 'dart:typed_data'; | ||||||
| 
 | 
 | ||||||
| import 'package:famedlysdk/famedlysdk.dart'; | import 'package:famedlysdk/famedlysdk.dart'; | ||||||
|  | @ -39,8 +38,9 @@ void main() { | ||||||
|   Future<List<EventUpdate>> eventUpdateListFuture; |   Future<List<EventUpdate>> eventUpdateListFuture; | ||||||
|   Future<List<ToDeviceEvent>> toDeviceUpdateListFuture; |   Future<List<ToDeviceEvent>> toDeviceUpdateListFuture; | ||||||
| 
 | 
 | ||||||
|  |   // key @test:fakeServer.notExisting | ||||||
|   const pickledOlmAccount = |   const pickledOlmAccount = | ||||||
|       'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuweStA+EKZvvHZO0SnwRp0Hw7sv8UMYvXw'; |       'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw'; | ||||||
|   const identityKey = '7rvl3jORJkBiK4XX1e5TnGnqz068XfYJ0W++Ml63rgk'; |   const identityKey = '7rvl3jORJkBiK4XX1e5TnGnqz068XfYJ0W++Ml63rgk'; | ||||||
|   const fingerprintKey = 'gjL//fyaFHADt9KBADGag8g7F8Up78B/K1zXeiEPLJo'; |   const fingerprintKey = 'gjL//fyaFHADt9KBADGag8g7F8Up78B/K1zXeiEPLJo'; | ||||||
| 
 | 
 | ||||||
|  | @ -134,24 +134,6 @@ void main() { | ||||||
|       expect(matrix.directChats, matrix.accountData['m.direct'].content); |       expect(matrix.directChats, matrix.accountData['m.direct'].content); | ||||||
|       expect(matrix.presences.length, 1); |       expect(matrix.presences.length, 1); | ||||||
|       expect(matrix.rooms[1].ephemerals.length, 2); |       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.length, 1); | ||||||
|       expect(matrix.rooms[1].typingUsers[0].id, '@alice:example.com'); |       expect(matrix.rooms[1].typingUsers[0].id, '@alice:example.com'); | ||||||
|       expect(matrix.rooms[1].roomAccountData.length, 3); |       expect(matrix.rooms[1].roomAccountData.length, 3); | ||||||
|  | @ -177,7 +159,7 @@ void main() { | ||||||
|       expect(presenceCounter, 1); |       expect(presenceCounter, 1); | ||||||
|       expect(accountDataCounter, 3); |       expect(accountDataCounter, 3); | ||||||
|       await Future.delayed(Duration(milliseconds: 50)); |       await Future.delayed(Duration(milliseconds: 50)); | ||||||
|       expect(matrix.userDeviceKeys.length, 3); |       expect(matrix.userDeviceKeys.length, 4); | ||||||
|       expect(matrix.userDeviceKeys['@alice:example.com'].outdated, false); |       expect(matrix.userDeviceKeys['@alice:example.com'].outdated, false); | ||||||
|       expect(matrix.userDeviceKeys['@alice:example.com'].deviceKeys.length, 2); |       expect(matrix.userDeviceKeys['@alice:example.com'].deviceKeys.length, 2); | ||||||
|       expect( |       expect( | ||||||
|  | @ -196,7 +178,7 @@ void main() { | ||||||
|         } |         } | ||||||
|       })); |       })); | ||||||
|       await Future.delayed(Duration(milliseconds: 50)); |       await Future.delayed(Duration(milliseconds: 50)); | ||||||
|       expect(matrix.userDeviceKeys.length, 2); |       expect(matrix.userDeviceKeys.length, 3); | ||||||
|       expect(matrix.userDeviceKeys['@alice:example.com'].outdated, true); |       expect(matrix.userDeviceKeys['@alice:example.com'].outdated, true); | ||||||
| 
 | 
 | ||||||
|       await matrix.handleSync(SyncUpdate.fromJson({ |       await matrix.handleSync(SyncUpdate.fromJson({ | ||||||
|  | @ -335,7 +317,11 @@ void main() { | ||||||
|       expect(eventUpdateList.length, 2); |       expect(eventUpdateList.length, 2); | ||||||
| 
 | 
 | ||||||
|       expect(eventUpdateList[0].type, 'm.new_device'); |       expect(eventUpdateList[0].type, 'm.new_device'); | ||||||
|  |       if (olmEnabled) { | ||||||
|         expect(eventUpdateList[1].type, 'm.room_key'); |         expect(eventUpdateList[1].type, 'm.room_key'); | ||||||
|  |       } else { | ||||||
|  |         expect(eventUpdateList[1].type, 'm.room.encrypted'); | ||||||
|  |       } | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     test('Login', () async { |     test('Login', () async { | ||||||
|  | @ -388,115 +374,6 @@ void main() { | ||||||
|           'mxc://example.org/SEsfnsuifSDFSSEF'); |           'mxc://example.org/SEsfnsuifSDFSSEF'); | ||||||
|       expect(aliceProfile.displayname, 'Alice Margatroid'); |       expect(aliceProfile.displayname, 'Alice Margatroid'); | ||||||
|     }); |     }); | ||||||
| 
 |  | ||||||
|     test('signJson', () { |  | ||||||
|       if (matrix.encryptionEnabled) { |  | ||||||
|         expect(matrix.fingerprintKey.isNotEmpty, true); |  | ||||||
|         expect(matrix.identityKey.isNotEmpty, true); |  | ||||||
|         var payload = <String, dynamic>{ |  | ||||||
|           '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<String, dynamic>.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({ |     var deviceKeys = DeviceKeys.fromJson({ | ||||||
|       'user_id': '@alice:example.com', |       'user_id': '@alice:example.com', | ||||||
|       'device_id': 'JLAFKJWSCS', |       'device_id': 'JLAFKJWSCS', | ||||||
|  | @ -512,16 +389,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 { |     test('sendToDevice', () async { | ||||||
|       await matrix.sendToDevice( |       await matrix.sendToDevice( | ||||||
|           [deviceKeys], |           [deviceKeys], | ||||||
|  | @ -547,13 +414,6 @@ void main() { | ||||||
| 
 | 
 | ||||||
|       await Future.delayed(Duration(milliseconds: 50)); |       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.isLogged(), true); | ||||||
|       expect(client1.rooms.length, 2); |       expect(client1.rooms.length, 2); | ||||||
| 
 | 
 | ||||||
|  | @ -571,12 +431,9 @@ void main() { | ||||||
|       expect(client2.deviceID, client1.deviceID); |       expect(client2.deviceID, client1.deviceID); | ||||||
|       expect(client2.deviceName, client1.deviceName); |       expect(client2.deviceName, client1.deviceName); | ||||||
|       if (client2.encryptionEnabled) { |       if (client2.encryptionEnabled) { | ||||||
|         await client2.rooms[1].restoreOutboundGroupSession(); |         expect(client2.encryption.pickledOlmAccount, | ||||||
|         expect(client2.pickledOlmAccount, client1.pickledOlmAccount); |             client1.encryption.pickledOlmAccount); | ||||||
|         expect(json.encode(client2.rooms[1].inboundGroupSessions[sessionKey]), |  | ||||||
|             json.encode(client1.rooms[1].inboundGroupSessions[sessionKey])); |  | ||||||
|         expect(client2.rooms[1].id, client1.rooms[1].id); |         expect(client2.rooms[1].id, client1.rooms[1].id); | ||||||
|         expect(client2.rooms[1].outboundGroupSession.session_key(), sessionKey); |  | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       await client1.logout(); |       await client1.logout(); | ||||||
|  |  | ||||||
|  | @ -0,0 +1,99 @@ | ||||||
|  | /* | ||||||
|  |  *   Ansible inventory script used at Famedly GmbH for managing many hosts | ||||||
|  |  *   Copyright (C) 2020 Famedly GmbH | ||||||
|  |  * | ||||||
|  |  *   This program is free software: you can redistribute it and/or modify | ||||||
|  |  *   it under the terms of the GNU Affero General Public License as | ||||||
|  |  *   published by the Free Software Foundation, either version 3 of the | ||||||
|  |  *   License, or (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  *   This program is distributed in the hope that it will be useful, | ||||||
|  |  *   but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
|  |  *   GNU Affero General Public License for more details. | ||||||
|  |  * | ||||||
|  |  *   You should have received a copy of the GNU Affero General Public License | ||||||
|  |  *   along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import 'package:famedlysdk/famedlysdk.dart'; | ||||||
|  | import 'package:test/test.dart'; | ||||||
|  | import 'package:olm/olm.dart' as olm; | ||||||
|  | 
 | ||||||
|  | import '../fake_client.dart'; | ||||||
|  | 
 | ||||||
|  | void main() { | ||||||
|  |   group('Encrypt/Decrypt room message', () { | ||||||
|  |     var olmEnabled = true; | ||||||
|  |     try { | ||||||
|  |       olm.init(); | ||||||
|  |       olm.Account(); | ||||||
|  |     } catch (_) { | ||||||
|  |       olmEnabled = false; | ||||||
|  |       print('[LibOlm] Failed to load LibOlm: ' + _.toString()); | ||||||
|  |     } | ||||||
|  |     print('[LibOlm] Enabled: $olmEnabled'); | ||||||
|  | 
 | ||||||
|  |     if (!olmEnabled) return; | ||||||
|  | 
 | ||||||
|  |     Client client; | ||||||
|  |     final roomId = '!726s6s6q:example.com'; | ||||||
|  |     Room room; | ||||||
|  |     Map<String, dynamic> payload; | ||||||
|  |     final now = DateTime.now(); | ||||||
|  | 
 | ||||||
|  |     test('setupClient', () async { | ||||||
|  |       client = await getClient(); | ||||||
|  |       room = client.getRoomById(roomId); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('encrypt payload', () async { | ||||||
|  |       payload = await client.encryption.encryptGroupMessagePayload(roomId, { | ||||||
|  |         'msgtype': 'm.text', | ||||||
|  |         'text': 'Hello foxies!', | ||||||
|  |       }); | ||||||
|  |       expect(payload['algorithm'], 'm.megolm.v1.aes-sha2'); | ||||||
|  |       expect(payload['ciphertext'] is String, true); | ||||||
|  |       expect(payload['device_id'], client.deviceID); | ||||||
|  |       expect(payload['sender_key'], client.identityKey); | ||||||
|  |       expect(payload['session_id'] is String, true); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('decrypt payload', () async { | ||||||
|  |       final encryptedEvent = Event( | ||||||
|  |         type: EventTypes.Encrypted, | ||||||
|  |         content: payload, | ||||||
|  |         roomId: roomId, | ||||||
|  |         room: room, | ||||||
|  |         originServerTs: now, | ||||||
|  |         eventId: '\$event', | ||||||
|  |       ); | ||||||
|  |       final decryptedEvent = | ||||||
|  |           await client.encryption.decryptRoomEvent(roomId, encryptedEvent); | ||||||
|  |       expect(decryptedEvent.type, 'm.room.message'); | ||||||
|  |       expect(decryptedEvent.content['msgtype'], 'm.text'); | ||||||
|  |       expect(decryptedEvent.content['text'], 'Hello foxies!'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('decrypt payload nocache', () async { | ||||||
|  |       client.encryption.keyManager.clearInboundGroupSessions(); | ||||||
|  |       final encryptedEvent = Event( | ||||||
|  |         type: EventTypes.Encrypted, | ||||||
|  |         content: payload, | ||||||
|  |         roomId: roomId, | ||||||
|  |         room: room, | ||||||
|  |         originServerTs: now, | ||||||
|  |         eventId: '\$event', | ||||||
|  |       ); | ||||||
|  |       final decryptedEvent = | ||||||
|  |           await client.encryption.decryptRoomEvent(roomId, encryptedEvent); | ||||||
|  |       expect(decryptedEvent.type, 'm.room.message'); | ||||||
|  |       expect(decryptedEvent.content['msgtype'], 'm.text'); | ||||||
|  |       expect(decryptedEvent.content['text'], 'Hello foxies!'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('dispose client', () async { | ||||||
|  |       await client.dispose(closeDatabase: true); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | @ -0,0 +1,120 @@ | ||||||
|  | /* | ||||||
|  |  *   Ansible inventory script used at Famedly GmbH for managing many hosts | ||||||
|  |  *   Copyright (C) 2020 Famedly GmbH | ||||||
|  |  * | ||||||
|  |  *   This program is free software: you can redistribute it and/or modify | ||||||
|  |  *   it under the terms of the GNU Affero General Public License as | ||||||
|  |  *   published by the Free Software Foundation, either version 3 of the | ||||||
|  |  *   License, or (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  *   This program is distributed in the hope that it will be useful, | ||||||
|  |  *   but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
|  |  *   GNU Affero General Public License for more details. | ||||||
|  |  * | ||||||
|  |  *   You should have received a copy of the GNU Affero General Public License | ||||||
|  |  *   along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import 'package:famedlysdk/famedlysdk.dart'; | ||||||
|  | import 'package:test/test.dart'; | ||||||
|  | import 'package:olm/olm.dart' as olm; | ||||||
|  | 
 | ||||||
|  | import '../fake_client.dart'; | ||||||
|  | import '../fake_matrix_api.dart'; | ||||||
|  | 
 | ||||||
|  | void main() { | ||||||
|  |   // key @othertest:fakeServer.notExisting | ||||||
|  |   const otherPickledOlmAccount = | ||||||
|  |       'VWhVApbkcilKAEGppsPDf9nNVjaK8/IxT3asSR0sYg0S5KgbfE8vXEPwoiKBX2cEvwX3OessOBOkk+ZE7TTbjlrh/KEd31p8Wo+47qj0AP+Ky+pabnhi+/rTBvZy+gfzTqUfCxZrkzfXI9Op4JnP6gYmy7dVX2lMYIIs9WCO1jcmIXiXum5jnfXu1WLfc7PZtO2hH+k9CDKosOFaXRBmsu8k/BGXPSoWqUpvu6WpEG9t5STk4FeAzA'; | ||||||
|  | 
 | ||||||
|  |   group('Encrypt/Decrypt to-device messages', () { | ||||||
|  |     var olmEnabled = true; | ||||||
|  |     try { | ||||||
|  |       olm.init(); | ||||||
|  |       olm.Account(); | ||||||
|  |     } catch (_) { | ||||||
|  |       olmEnabled = false; | ||||||
|  |       print('[LibOlm] Failed to load LibOlm: ' + _.toString()); | ||||||
|  |     } | ||||||
|  |     print('[LibOlm] Enabled: $olmEnabled'); | ||||||
|  | 
 | ||||||
|  |     if (!olmEnabled) return; | ||||||
|  | 
 | ||||||
|  |     Client client; | ||||||
|  |     var otherClient = | ||||||
|  |         Client('othertestclient', debug: true, httpClient: FakeMatrixApi()); | ||||||
|  |     DeviceKeys device; | ||||||
|  |     Map<String, dynamic> payload; | ||||||
|  | 
 | ||||||
|  |     test('setupClient', () async { | ||||||
|  |       client = await getClient(); | ||||||
|  |       otherClient.database = client.database; | ||||||
|  |       await otherClient.checkServer('https://fakeServer.notExisting'); | ||||||
|  |       otherClient.connect( | ||||||
|  |         newToken: 'abc', | ||||||
|  |         newUserID: '@othertest:fakeServer.notExisting', | ||||||
|  |         newHomeserver: otherClient.api.homeserver, | ||||||
|  |         newDeviceName: 'Text Matrix Client', | ||||||
|  |         newDeviceID: 'FOXDEVICE', | ||||||
|  |         newOlmAccount: otherPickledOlmAccount, | ||||||
|  |       ); | ||||||
|  | 
 | ||||||
|  |       await Future.delayed(Duration(milliseconds: 10)); | ||||||
|  |       device = DeviceKeys( | ||||||
|  |         userId: client.userID, | ||||||
|  |         deviceId: client.deviceID, | ||||||
|  |         algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'], | ||||||
|  |         keys: { | ||||||
|  |           'curve25519:${client.deviceID}': client.identityKey, | ||||||
|  |           'ed25519:${client.deviceID}': client.fingerprintKey, | ||||||
|  |         }, | ||||||
|  |         verified: true, | ||||||
|  |         blocked: false, | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('encryptToDeviceMessage', () async { | ||||||
|  |       payload = await otherClient.encryption | ||||||
|  |           .encryptToDeviceMessage([device], 'm.to_device', {'hello': 'foxies'}); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('encryptToDeviceMessagePayload', () async { | ||||||
|  |       // just a hard test if nothing errors | ||||||
|  |       await otherClient.encryption.encryptToDeviceMessagePayload( | ||||||
|  |           device, 'm.to_device', {'hello': 'foxies'}); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('decryptToDeviceEvent', () async { | ||||||
|  |       final encryptedEvent = ToDeviceEvent( | ||||||
|  |         sender: '@othertest:fakeServer.notExisting', | ||||||
|  |         type: EventTypes.Encrypted, | ||||||
|  |         content: payload[client.userID][client.deviceID], | ||||||
|  |       ); | ||||||
|  |       final decryptedEvent = | ||||||
|  |           await client.encryption.decryptToDeviceEvent(encryptedEvent); | ||||||
|  |       expect(decryptedEvent.type, 'm.to_device'); | ||||||
|  |       expect(decryptedEvent.content['hello'], 'foxies'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('decryptToDeviceEvent nocache', () async { | ||||||
|  |       client.encryption.olmManager.olmSessions.clear(); | ||||||
|  |       payload = await otherClient.encryption.encryptToDeviceMessage( | ||||||
|  |           [device], 'm.to_device', {'hello': 'superfoxies'}); | ||||||
|  |       final encryptedEvent = ToDeviceEvent( | ||||||
|  |         sender: '@othertest:fakeServer.notExisting', | ||||||
|  |         type: EventTypes.Encrypted, | ||||||
|  |         content: payload[client.userID][client.deviceID], | ||||||
|  |       ); | ||||||
|  |       final decryptedEvent = | ||||||
|  |           await client.encryption.decryptToDeviceEvent(encryptedEvent); | ||||||
|  |       expect(decryptedEvent.type, 'm.to_device'); | ||||||
|  |       expect(decryptedEvent.content['hello'], 'superfoxies'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('dispose client', () async { | ||||||
|  |       await client.dispose(closeDatabase: true); | ||||||
|  |       await otherClient.dispose(closeDatabase: true); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | @ -0,0 +1,223 @@ | ||||||
|  | /* | ||||||
|  |  *   Ansible inventory script used at Famedly GmbH for managing many hosts | ||||||
|  |  *   Copyright (C) 2020 Famedly GmbH | ||||||
|  |  * | ||||||
|  |  *   This program is free software: you can redistribute it and/or modify | ||||||
|  |  *   it under the terms of the GNU Affero General Public License as | ||||||
|  |  *   published by the Free Software Foundation, either version 3 of the | ||||||
|  |  *   License, or (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  *   This program is distributed in the hope that it will be useful, | ||||||
|  |  *   but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
|  |  *   GNU Affero General Public License for more details. | ||||||
|  |  * | ||||||
|  |  *   You should have received a copy of the GNU Affero General Public License | ||||||
|  |  *   along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import 'package:famedlysdk/famedlysdk.dart'; | ||||||
|  | import 'package:test/test.dart'; | ||||||
|  | import 'package:olm/olm.dart' as olm; | ||||||
|  | 
 | ||||||
|  | import '../fake_client.dart'; | ||||||
|  | 
 | ||||||
|  | void main() { | ||||||
|  |   group('Key Manager', () { | ||||||
|  |     var olmEnabled = true; | ||||||
|  |     try { | ||||||
|  |       olm.init(); | ||||||
|  |       olm.Account(); | ||||||
|  |     } catch (_) { | ||||||
|  |       olmEnabled = false; | ||||||
|  |       print('[LibOlm] Failed to load LibOlm: ' + _.toString()); | ||||||
|  |     } | ||||||
|  |     print('[LibOlm] Enabled: $olmEnabled'); | ||||||
|  | 
 | ||||||
|  |     if (!olmEnabled) return; | ||||||
|  | 
 | ||||||
|  |     Client client; | ||||||
|  | 
 | ||||||
|  |     test('setupClient', () async { | ||||||
|  |       client = await getClient(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('handle new m.room_key', () async { | ||||||
|  |       final validSessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU'; | ||||||
|  |       final validSenderKey = 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg'; | ||||||
|  |       final sessionKey = | ||||||
|  |           'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw'; | ||||||
|  | 
 | ||||||
|  |       client.encryption.keyManager.clearInboundGroupSessions(); | ||||||
|  |       var event = ToDeviceEvent( | ||||||
|  |           sender: '@alice:example.com', | ||||||
|  |           type: 'm.room_key', | ||||||
|  |           content: { | ||||||
|  |             'algorithm': 'm.megolm.v1.aes-sha2', | ||||||
|  |             'room_id': '!726s6s6q:example.com', | ||||||
|  |             'session_id': validSessionId, | ||||||
|  |             'session_key': sessionKey, | ||||||
|  |           }, | ||||||
|  |           encryptedContent: { | ||||||
|  |             'sender_key': validSessionId, | ||||||
|  |           }); | ||||||
|  |       await client.encryption.keyManager.handleToDeviceEvent(event); | ||||||
|  |       expect( | ||||||
|  |           client.encryption.keyManager.getInboundGroupSession( | ||||||
|  |                   '!726s6s6q:example.com', validSessionId, validSenderKey) != | ||||||
|  |               null, | ||||||
|  |           true); | ||||||
|  | 
 | ||||||
|  |       // now test a few invalid scenarios | ||||||
|  | 
 | ||||||
|  |       // not encrypted | ||||||
|  |       client.encryption.keyManager.clearInboundGroupSessions(); | ||||||
|  |       event = ToDeviceEvent( | ||||||
|  |           sender: '@alice:example.com', | ||||||
|  |           type: 'm.room_key', | ||||||
|  |           content: { | ||||||
|  |             'algorithm': 'm.megolm.v1.aes-sha2', | ||||||
|  |             'room_id': '!726s6s6q:example.com', | ||||||
|  |             'session_id': validSessionId, | ||||||
|  |             'session_key': sessionKey, | ||||||
|  |           }); | ||||||
|  |       await client.encryption.keyManager.handleToDeviceEvent(event); | ||||||
|  |       expect( | ||||||
|  |           client.encryption.keyManager.getInboundGroupSession( | ||||||
|  |                   '!726s6s6q:example.com', validSessionId, validSenderKey) != | ||||||
|  |               null, | ||||||
|  |           false); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('outbound group session', () async { | ||||||
|  |       final roomId = '!726s6s6q:example.com'; | ||||||
|  |       expect( | ||||||
|  |           client.encryption.keyManager.getOutboundGroupSession(roomId) != null, | ||||||
|  |           false); | ||||||
|  |       var sess = | ||||||
|  |           await client.encryption.keyManager.createOutboundGroupSession(roomId); | ||||||
|  |       expect( | ||||||
|  |           client.encryption.keyManager.getOutboundGroupSession(roomId) != null, | ||||||
|  |           true); | ||||||
|  |       await client.encryption.keyManager.clearOutboundGroupSession(roomId); | ||||||
|  |       expect( | ||||||
|  |           client.encryption.keyManager.getOutboundGroupSession(roomId) != null, | ||||||
|  |           true); | ||||||
|  |       expect( | ||||||
|  |           client.encryption.keyManager.getInboundGroupSession(roomId, | ||||||
|  |                   sess.outboundGroupSession.session_id(), client.identityKey) != | ||||||
|  |               null, | ||||||
|  |           true); | ||||||
|  | 
 | ||||||
|  |       // rotate after too many messages | ||||||
|  |       sess.sentMessages = 300; | ||||||
|  |       await client.encryption.keyManager.clearOutboundGroupSession(roomId); | ||||||
|  |       expect( | ||||||
|  |           client.encryption.keyManager.getOutboundGroupSession(roomId) != null, | ||||||
|  |           false); | ||||||
|  | 
 | ||||||
|  |       // rotate if devices in room change | ||||||
|  |       sess = | ||||||
|  |           await client.encryption.keyManager.createOutboundGroupSession(roomId); | ||||||
|  |       client.userDeviceKeys['@alice:example.com'].deviceKeys['JLAFKJWSCS'] | ||||||
|  |           .blocked = true; | ||||||
|  |       await client.encryption.keyManager.clearOutboundGroupSession(roomId); | ||||||
|  |       expect( | ||||||
|  |           client.encryption.keyManager.getOutboundGroupSession(roomId) != null, | ||||||
|  |           false); | ||||||
|  |       client.userDeviceKeys['@alice:example.com'].deviceKeys['JLAFKJWSCS'] | ||||||
|  |           .blocked = false; | ||||||
|  | 
 | ||||||
|  |       // rotate if too far in the past | ||||||
|  |       sess = | ||||||
|  |           await client.encryption.keyManager.createOutboundGroupSession(roomId); | ||||||
|  |       sess.creationTime = DateTime.now().subtract(Duration(days: 30)); | ||||||
|  |       await client.encryption.keyManager.clearOutboundGroupSession(roomId); | ||||||
|  |       expect( | ||||||
|  |           client.encryption.keyManager.getOutboundGroupSession(roomId) != null, | ||||||
|  |           false); | ||||||
|  | 
 | ||||||
|  |       // force wipe | ||||||
|  |       sess = | ||||||
|  |           await client.encryption.keyManager.createOutboundGroupSession(roomId); | ||||||
|  |       await client.encryption.keyManager | ||||||
|  |           .clearOutboundGroupSession(roomId, wipe: true); | ||||||
|  |       expect( | ||||||
|  |           client.encryption.keyManager.getOutboundGroupSession(roomId) != null, | ||||||
|  |           false); | ||||||
|  | 
 | ||||||
|  |       // load from database | ||||||
|  |       sess = | ||||||
|  |           await client.encryption.keyManager.createOutboundGroupSession(roomId); | ||||||
|  |       client.encryption.keyManager.clearOutboundGroupSessions(); | ||||||
|  |       expect( | ||||||
|  |           client.encryption.keyManager.getOutboundGroupSession(roomId) != null, | ||||||
|  |           false); | ||||||
|  |       await client.encryption.keyManager.loadOutboundGroupSession(roomId); | ||||||
|  |       expect( | ||||||
|  |           client.encryption.keyManager.getOutboundGroupSession(roomId) != null, | ||||||
|  |           true); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('inbound group session', () async { | ||||||
|  |       final roomId = '!726s6s6q:example.com'; | ||||||
|  |       final sessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU'; | ||||||
|  |       final senderKey = 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg'; | ||||||
|  |       final sessionContent = <String, dynamic>{ | ||||||
|  |         'algorithm': 'm.megolm.v1.aes-sha2', | ||||||
|  |         'room_id': '!726s6s6q:example.com', | ||||||
|  |         'session_id': 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU', | ||||||
|  |         'session_key': | ||||||
|  |             'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw' | ||||||
|  |       }; | ||||||
|  |       client.encryption.keyManager.clearInboundGroupSessions(); | ||||||
|  |       expect( | ||||||
|  |           client.encryption.keyManager | ||||||
|  |                   .getInboundGroupSession(roomId, sessionId, senderKey) != | ||||||
|  |               null, | ||||||
|  |           false); | ||||||
|  |       client.encryption.keyManager | ||||||
|  |           .setInboundGroupSession(roomId, sessionId, senderKey, sessionContent); | ||||||
|  |       await Future.delayed(Duration(milliseconds: 10)); | ||||||
|  |       expect( | ||||||
|  |           client.encryption.keyManager | ||||||
|  |                   .getInboundGroupSession(roomId, sessionId, senderKey) != | ||||||
|  |               null, | ||||||
|  |           true); | ||||||
|  | 
 | ||||||
|  |       expect( | ||||||
|  |           client.encryption.keyManager | ||||||
|  |                   .getInboundGroupSession(roomId, sessionId, senderKey) != | ||||||
|  |               null, | ||||||
|  |           true); | ||||||
|  |       expect( | ||||||
|  |           client.encryption.keyManager | ||||||
|  |                   .getInboundGroupSession('otherroom', sessionId, senderKey) != | ||||||
|  |               null, | ||||||
|  |           true); | ||||||
|  |       expect( | ||||||
|  |           client.encryption.keyManager | ||||||
|  |                   .getInboundGroupSession('otherroom', 'invalid', senderKey) != | ||||||
|  |               null, | ||||||
|  |           false); | ||||||
|  | 
 | ||||||
|  |       client.encryption.keyManager.clearInboundGroupSessions(); | ||||||
|  |       expect( | ||||||
|  |           client.encryption.keyManager | ||||||
|  |                   .getInboundGroupSession(roomId, sessionId, senderKey) != | ||||||
|  |               null, | ||||||
|  |           false); | ||||||
|  |       await client.encryption.keyManager | ||||||
|  |           .loadInboundGroupSession(roomId, sessionId, senderKey); | ||||||
|  |       expect( | ||||||
|  |           client.encryption.keyManager | ||||||
|  |                   .getInboundGroupSession(roomId, sessionId, senderKey) != | ||||||
|  |               null, | ||||||
|  |           true); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('dispose client', () async { | ||||||
|  |       await client.dispose(closeDatabase: true); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | @ -0,0 +1,331 @@ | ||||||
|  | /* | ||||||
|  |  *   Ansible inventory script used at Famedly GmbH for managing many hosts | ||||||
|  |  *   Copyright (C) 2020 Famedly GmbH | ||||||
|  |  * | ||||||
|  |  *   This program is free software: you can redistribute it and/or modify | ||||||
|  |  *   it under the terms of the GNU Affero General Public License as | ||||||
|  |  *   published by the Free Software Foundation, either version 3 of the | ||||||
|  |  *   License, or (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  *   This program is distributed in the hope that it will be useful, | ||||||
|  |  *   but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
|  |  *   GNU Affero General Public License for more details. | ||||||
|  |  * | ||||||
|  |  *   You should have received a copy of the GNU Affero General Public License | ||||||
|  |  *   along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import 'dart:convert'; | ||||||
|  | import 'package:famedlysdk/famedlysdk.dart'; | ||||||
|  | import 'package:test/test.dart'; | ||||||
|  | import 'package:olm/olm.dart' as olm; | ||||||
|  | 
 | ||||||
|  | import '../fake_client.dart'; | ||||||
|  | import '../fake_matrix_api.dart'; | ||||||
|  | 
 | ||||||
|  | Map<String, dynamic> jsonDecode(dynamic payload) { | ||||||
|  |   if (payload is String) { | ||||||
|  |     try { | ||||||
|  |       return json.decode(payload); | ||||||
|  |     } catch (e) { | ||||||
|  |       return {}; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (payload is Map<String, dynamic>) return payload; | ||||||
|  |   return {}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void main() { | ||||||
|  |   /// All Tests related to device keys | ||||||
|  |   group('Key Request', () { | ||||||
|  |     var olmEnabled = true; | ||||||
|  |     try { | ||||||
|  |       olm.init(); | ||||||
|  |       olm.Account(); | ||||||
|  |     } catch (_) { | ||||||
|  |       olmEnabled = false; | ||||||
|  |       print('[LibOlm] Failed to load LibOlm: ' + _.toString()); | ||||||
|  |     } | ||||||
|  |     print('[LibOlm] Enabled: $olmEnabled'); | ||||||
|  | 
 | ||||||
|  |     if (!olmEnabled) return; | ||||||
|  | 
 | ||||||
|  |     final validSessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU'; | ||||||
|  |     final validSenderKey = '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI'; | ||||||
|  |     test('Create Request', () async { | ||||||
|  |       var matrix = await getClient(); | ||||||
|  |       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 = await getClient(); | ||||||
|  |       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 = await getClient(); | ||||||
|  |       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); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| /* | /* | ||||||
|  *   Ansible inventory script used at Famedly GmbH for managing many hosts |  *   Ansible inventory script used at Famedly GmbH for managing many hosts | ||||||
|  *   Copyright (C) 2019, 2020 Famedly GmbH |  *   Copyright (C) 2020 Famedly GmbH | ||||||
|  * |  * | ||||||
|  *   This program is free software: you can redistribute it and/or modify |  *   This program is free software: you can redistribute it and/or modify | ||||||
|  *   it under the terms of the GNU Affero General Public License as |  *   it under the terms of the GNU Affero General Public License as | ||||||
|  | @ -17,10 +17,11 @@ | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| import 'package:famedlysdk/famedlysdk.dart'; | import 'package:famedlysdk/famedlysdk.dart'; | ||||||
|  | import 'package:famedlysdk/encryption.dart'; | ||||||
| import 'package:test/test.dart'; | import 'package:test/test.dart'; | ||||||
| import 'package:olm/olm.dart' as olm; | import 'package:olm/olm.dart' as olm; | ||||||
| 
 | 
 | ||||||
| import 'fake_matrix_api.dart'; | import '../fake_client.dart'; | ||||||
| 
 | 
 | ||||||
| void main() { | void main() { | ||||||
|   /// All Tests related to the ChatTime |   /// All Tests related to the ChatTime | ||||||
|  | @ -35,19 +36,24 @@ void main() { | ||||||
|     } |     } | ||||||
|     print('[LibOlm] Enabled: $olmEnabled'); |     print('[LibOlm] Enabled: $olmEnabled'); | ||||||
| 
 | 
 | ||||||
|     var client = Client('testclient', debug: true, httpClient: FakeMatrixApi()); |     if (!olmEnabled) return; | ||||||
|     client.api.homeserver = Uri.parse('https://fakeserver.notexisting'); | 
 | ||||||
|     var room = Room(id: '!localpart:server.abc', client: client); |     Client client; | ||||||
|  |     Room room; | ||||||
|     var updateCounter = 0; |     var updateCounter = 0; | ||||||
|     final keyVerification = KeyVerification( |     KeyVerification keyVerification; | ||||||
|       client: client, | 
 | ||||||
|  |     test('setupClient', () async { | ||||||
|  |       client = await getClient(); | ||||||
|  |       room = Room(id: '!localpart:server.abc', client: client); | ||||||
|  |       keyVerification = KeyVerification( | ||||||
|  |         encryption: client.encryption, | ||||||
|         room: room, |         room: room, | ||||||
|         userId: '@alice:example.com', |         userId: '@alice:example.com', | ||||||
|         deviceId: 'ABCD', |         deviceId: 'ABCD', | ||||||
|         onUpdate: () => updateCounter++, |         onUpdate: () => updateCounter++, | ||||||
|       ); |       ); | ||||||
| 
 |     }); | ||||||
|     if (!olmEnabled) return; |  | ||||||
| 
 | 
 | ||||||
|     test('acceptSas', () async { |     test('acceptSas', () async { | ||||||
|       await keyVerification.acceptSas(); |       await keyVerification.acceptSas(); | ||||||
|  | @ -91,7 +97,11 @@ void main() { | ||||||
|     test('verifyActivity', () async { |     test('verifyActivity', () async { | ||||||
|       final verified = await keyVerification.verifyActivity(); |       final verified = await keyVerification.verifyActivity(); | ||||||
|       expect(verified, true); |       expect(verified, true); | ||||||
|  |       keyVerification?.dispose(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('dispose client', () async { | ||||||
|  |       await client.dispose(closeDatabase: true); | ||||||
|     }); |     }); | ||||||
|     keyVerification.dispose(); |  | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  | @ -0,0 +1,116 @@ | ||||||
|  | /* | ||||||
|  |  *   Ansible inventory script used at Famedly GmbH for managing many hosts | ||||||
|  |  *   Copyright (C) 2020 Famedly GmbH | ||||||
|  |  * | ||||||
|  |  *   This program is free software: you can redistribute it and/or modify | ||||||
|  |  *   it under the terms of the GNU Affero General Public License as | ||||||
|  |  *   published by the Free Software Foundation, either version 3 of the | ||||||
|  |  *   License, or (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  *   This program is distributed in the hope that it will be useful, | ||||||
|  |  *   but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
|  |  *   GNU Affero General Public License for more details. | ||||||
|  |  * | ||||||
|  |  *   You should have received a copy of the GNU Affero General Public License | ||||||
|  |  *   along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import 'dart:convert'; | ||||||
|  | import 'package:famedlysdk/famedlysdk.dart'; | ||||||
|  | import 'package:test/test.dart'; | ||||||
|  | import 'package:olm/olm.dart' as olm; | ||||||
|  | 
 | ||||||
|  | import '../fake_client.dart'; | ||||||
|  | import '../fake_matrix_api.dart'; | ||||||
|  | 
 | ||||||
|  | void main() { | ||||||
|  |   group('Olm Manager', () { | ||||||
|  |     var olmEnabled = true; | ||||||
|  |     try { | ||||||
|  |       olm.init(); | ||||||
|  |       olm.Account(); | ||||||
|  |     } catch (_) { | ||||||
|  |       olmEnabled = false; | ||||||
|  |       print('[LibOlm] Failed to load LibOlm: ' + _.toString()); | ||||||
|  |     } | ||||||
|  |     print('[LibOlm] Enabled: $olmEnabled'); | ||||||
|  | 
 | ||||||
|  |     if (!olmEnabled) return; | ||||||
|  | 
 | ||||||
|  |     Client client; | ||||||
|  | 
 | ||||||
|  |     test('setupClient', () async { | ||||||
|  |       client = await getClient(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('signatures', () async { | ||||||
|  |       final payload = <String, dynamic>{ | ||||||
|  |         'fox': 'floof', | ||||||
|  |       }; | ||||||
|  |       final signedPayload = client.encryption.olmManager.signJson(payload); | ||||||
|  |       expect( | ||||||
|  |           client.encryption.olmManager.checkJsonSignature(client.fingerprintKey, | ||||||
|  |               signedPayload, client.userID, client.deviceID), | ||||||
|  |           true); | ||||||
|  |       expect( | ||||||
|  |           client.encryption.olmManager.checkJsonSignature( | ||||||
|  |               client.fingerprintKey, payload, client.userID, client.deviceID), | ||||||
|  |           false); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('uploadKeys', () async { | ||||||
|  |       FakeMatrixApi.calledEndpoints.clear(); | ||||||
|  |       final res = | ||||||
|  |           await client.encryption.olmManager.uploadKeys(uploadDeviceKeys: true); | ||||||
|  |       expect(res, true); | ||||||
|  |       var sent = json.decode( | ||||||
|  |           FakeMatrixApi.calledEndpoints['/client/r0/keys/upload'].first); | ||||||
|  |       expect(sent['device_keys'] != null, true); | ||||||
|  |       expect(sent['one_time_keys'] != null, true); | ||||||
|  |       expect(sent['one_time_keys'].keys.length, 66); | ||||||
|  |       FakeMatrixApi.calledEndpoints.clear(); | ||||||
|  |       await client.encryption.olmManager.uploadKeys(); | ||||||
|  |       sent = json.decode( | ||||||
|  |           FakeMatrixApi.calledEndpoints['/client/r0/keys/upload'].first); | ||||||
|  |       expect(sent['device_keys'] != null, false); | ||||||
|  |       FakeMatrixApi.calledEndpoints.clear(); | ||||||
|  |       await client.encryption.olmManager.uploadKeys(oldKeyCount: 20); | ||||||
|  |       sent = json.decode( | ||||||
|  |           FakeMatrixApi.calledEndpoints['/client/r0/keys/upload'].first); | ||||||
|  |       expect(sent['one_time_keys'].keys.length, 46); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('handleDeviceOneTimeKeysCount', () async { | ||||||
|  |       FakeMatrixApi.calledEndpoints.clear(); | ||||||
|  |       client.encryption.olmManager | ||||||
|  |           .handleDeviceOneTimeKeysCount({'signed_curve25519': 20}); | ||||||
|  |       await Future.delayed(Duration(milliseconds: 50)); | ||||||
|  |       expect( | ||||||
|  |           FakeMatrixApi.calledEndpoints.containsKey('/client/r0/keys/upload'), | ||||||
|  |           true); | ||||||
|  | 
 | ||||||
|  |       FakeMatrixApi.calledEndpoints.clear(); | ||||||
|  |       client.encryption.olmManager | ||||||
|  |           .handleDeviceOneTimeKeysCount({'signed_curve25519': 70}); | ||||||
|  |       await Future.delayed(Duration(milliseconds: 50)); | ||||||
|  |       expect( | ||||||
|  |           FakeMatrixApi.calledEndpoints.containsKey('/client/r0/keys/upload'), | ||||||
|  |           false); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('startOutgoingOlmSessions', () async { | ||||||
|  |       // start an olm session.....with ourself! | ||||||
|  |       await client.encryption.olmManager.startOutgoingOlmSessions( | ||||||
|  |           [client.userDeviceKeys[client.userID].deviceKeys[client.deviceID]]); | ||||||
|  |       expect( | ||||||
|  |           client.encryption.olmManager.olmSessions | ||||||
|  |               .containsKey(client.identityKey), | ||||||
|  |           true); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('dispose client', () async { | ||||||
|  |       await client.dispose(closeDatabase: true); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | @ -20,6 +20,7 @@ import 'dart:convert'; | ||||||
| 
 | 
 | ||||||
| import 'package:famedlysdk/famedlysdk.dart'; | import 'package:famedlysdk/famedlysdk.dart'; | ||||||
| import 'package:famedlysdk/matrix_api.dart'; | import 'package:famedlysdk/matrix_api.dart'; | ||||||
|  | import 'package:famedlysdk/encryption.dart'; | ||||||
| import 'package:famedlysdk/src/event.dart'; | import 'package:famedlysdk/src/event.dart'; | ||||||
| import 'package:test/test.dart'; | import 'package:test/test.dart'; | ||||||
| 
 | 
 | ||||||
|  | @ -219,7 +220,7 @@ void main() { | ||||||
|       event.status = -1; |       event.status = -1; | ||||||
|       final resp2 = await event.sendAgain(txid: '1234'); |       final resp2 = await event.sendAgain(txid: '1234'); | ||||||
|       expect(resp1, null); |       expect(resp1, null); | ||||||
|       expect(resp2, '42'); |       expect(resp2.startsWith('\$event'), true); | ||||||
| 
 | 
 | ||||||
|       await matrix.dispose(closeDatabase: true); |       await matrix.dispose(closeDatabase: true); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  | @ -0,0 +1,48 @@ | ||||||
|  | /* | ||||||
|  |  *   Ansible inventory script used at Famedly GmbH for managing many hosts | ||||||
|  |  *   Copyright (C) 2020 Famedly GmbH | ||||||
|  |  * | ||||||
|  |  *   This program is free software: you can redistribute it and/or modify | ||||||
|  |  *   it under the terms of the GNU Affero General Public License as | ||||||
|  |  *   published by the Free Software Foundation, either version 3 of the | ||||||
|  |  *   License, or (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  *   This program is distributed in the hope that it will be useful, | ||||||
|  |  *   but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
|  |  *   GNU Affero General Public License for more details. | ||||||
|  |  * | ||||||
|  |  *   You should have received a copy of the GNU Affero General Public License | ||||||
|  |  *   along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import 'package:famedlysdk/famedlysdk.dart'; | ||||||
|  | 
 | ||||||
|  | import 'fake_matrix_api.dart'; | ||||||
|  | import 'fake_database.dart'; | ||||||
|  | 
 | ||||||
|  | // key @test:fakeServer.notExisting | ||||||
|  | const pickledOlmAccount = | ||||||
|  |     'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw'; | ||||||
|  | 
 | ||||||
|  | Future<Client> getClient() async { | ||||||
|  |   final client = Client('testclient', debug: true, httpClient: FakeMatrixApi()); | ||||||
|  |   client.database = getDatabase(); | ||||||
|  |   await client.checkServer('https://fakeServer.notExisting'); | ||||||
|  |   final resp = await client.api.login( | ||||||
|  |     type: 'm.login.password', | ||||||
|  |     user: 'test', | ||||||
|  |     password: '1234', | ||||||
|  |     initialDeviceDisplayName: 'Fluffy Matrix Client', | ||||||
|  |   ); | ||||||
|  |   client.connect( | ||||||
|  |     newToken: resp.accessToken, | ||||||
|  |     newUserID: resp.userId, | ||||||
|  |     newHomeserver: client.api.homeserver, | ||||||
|  |     newDeviceName: 'Text Matrix Client', | ||||||
|  |     newDeviceID: resp.deviceId, | ||||||
|  |     newOlmAccount: pickledOlmAccount, | ||||||
|  |   ); | ||||||
|  |   await Future.delayed(Duration(milliseconds: 10)); | ||||||
|  |   return client; | ||||||
|  | } | ||||||
|  | @ -25,6 +25,7 @@ import 'package:http/testing.dart'; | ||||||
| 
 | 
 | ||||||
| class FakeMatrixApi extends MockClient { | class FakeMatrixApi extends MockClient { | ||||||
|   static final calledEndpoints = <String, List<dynamic>>{}; |   static final calledEndpoints = <String, List<dynamic>>{}; | ||||||
|  |   static int eventCounter = 0; | ||||||
| 
 | 
 | ||||||
|   FakeMatrixApi() |   FakeMatrixApi() | ||||||
|       : super((request) async { |       : super((request) async { | ||||||
|  | @ -527,16 +528,32 @@ class FakeMatrixApi extends MockClient { | ||||||
|             'rooms': ['!726s6s6q:example.com'] |             'rooms': ['!726s6s6q:example.com'] | ||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|  | //        { | ||||||
|  | //          'sender': '@othertest:fakeServer.notExisting', | ||||||
|  | //          'content': { | ||||||
|  | //            'algorithm': 'm.megolm.v1.aes-sha2', | ||||||
|  | //            'room_id': '!726s6s6q:example.com', | ||||||
|  | //            'session_id': 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU', | ||||||
|  | //            'session_key': | ||||||
|  | //                'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw' | ||||||
|  | //          }, | ||||||
|  | //          'type': 'm.room_key' | ||||||
|  | //        }, | ||||||
|         { |         { | ||||||
|           'sender': '@alice:example.com', |           // this is the commented out m.room_key event - only encrypted | ||||||
|  |           'sender': '@othertest:fakeServer.notExisting', | ||||||
|           'content': { |           'content': { | ||||||
|             'algorithm': 'm.megolm.v1.aes-sha2', |             'algorithm': 'm.olm.v1.curve25519-aes-sha2', | ||||||
|             'room_id': '!726s6s6q:example.com', |             'sender_key': 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg', | ||||||
|             'session_id': 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU', |             'ciphertext': { | ||||||
|             'session_key': |               '7rvl3jORJkBiK4XX1e5TnGnqz068XfYJ0W++Ml63rgk': { | ||||||
|                 'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw' |                 'type': 0, | ||||||
|  |                 'body': | ||||||
|  |                     'Awogyh7K4iLUQjcOxIfi7q7LhBBqv9w0mQ6JI9+U9tv7iF4SIHC6xb5YFWf9voRnmDBbd+0vxD/xDlVNRDlPIKliLGkYGiAkEbtlo+fng4ELtO4gSLKVbcFn7tZwZCEUE8H2miBsCCKABgMKIFrKDJwB7gM3lXPt9yVoh6gQksafKt7VFCNRN5KLKqsDEAAi0AX5EfTV7jJ1ZWAbxftjoSN6kCVIxzGclbyg1HjchmNCX7nxNCHWl+q5ZgqHYZVu2n2mCVmIaKD0kvoEZeY3tV1Itb6zf67BLaU0qgW/QzHCHg5a44tNLjucvL2mumHjIG8k0BY2uh+52HeiMCvSOvtDwHg7nzCASGdqPVCj9Kzw6z7F6nL4e3mYim8zvJd7f+mD9z3ARrypUOLGkTGYbB2PQOovf0Do8WzcaRzfaUCnuu/YVZWKK7DPgG8uhw/TjR6XtraAKZysF+4DJYMG9SQWx558r6s7Z5EUOF5CU2M35w1t1Xxllb3vrS83dtf9LPCrBhLsEBeYEUBE2+bTBfl0BDKqLiB0Cc0N0ixOcHIt6e40wAvW622/gMgHlpNSx8xG12u0s6h6EMWdCXXLWd9fy2q6glFUHvA67A35q7O+M8DVml7Y9xG55Y3DHkMDc9cwgwFkBDCAYQe6pQF1nlKytcVCGREpBs/gq69gHAStMQ8WEg38Lf8u8eBr2DFexrN4U+QAk+S//P3fJgf0bQx/Eosx4fvWSz9En41iC+ADCsWQpMbwHn4JWvtAbn3oW0XmL/OgThTkJMLiCymduYAa1Hnt7a3tP0KTL2/x11F02ggQHL28cCjq5W4zUGjWjl5wo2PsKB6t8aAvMg2ujGD2rCjb4yrv5VIzAKMOZLyj7K0vSK9gwDLQ/4vq+QnKUBG5zrcOze0hX+kz2909/tmAdeCH61Ypw7gbPUJAKnmKYUiB/UgwkJvzMJSsk/SEs5SXosHDI+HsJHJp4Mp4iKD0xRMst+8f9aTjaWwh8ZvELE1ZOhhCbF3RXhxi3x2Nu8ORIz+vhEQ1NOlMc7UIo98Fk/96T36vL/fviowT4C/0AlaapZDJBmKwhmwqisMjY2n1vY29oM2p5BzY1iwP7q9BYdRFst6xwo57TNSuRwQw7IhFsf0k+ABuPEZy5xB5nPHyIRTf/pr3Hw', | ||||||
|               }, |               }, | ||||||
|           'type': 'm.room_key' |             }, | ||||||
|  |           }, | ||||||
|  |           'type': 'm.room.encrypted', | ||||||
|         }, |         }, | ||||||
|       ] |       ] | ||||||
|     }, |     }, | ||||||
|  | @ -1567,7 +1584,20 @@ class FakeMatrixApi extends MockClient { | ||||||
|                     } |                     } | ||||||
|                   } |                   } | ||||||
|                 } |                 } | ||||||
|               } |               }, | ||||||
|  |               '@test:fakeServer.notExisting': { | ||||||
|  |                 'GHTYAJCE': { | ||||||
|  |                   'signed_curve25519:AAAAAQ': { | ||||||
|  |                     'key': 'qc72ve94cA28iuE0fXa98QO3uls39DHWdQlYyvvhGh0', | ||||||
|  |                     'signatures': { | ||||||
|  |                       '@test:fakeServer.notExisting': { | ||||||
|  |                         'ed25519:GHTYAJCE': | ||||||
|  |                             'dFwffr5kTKefO7sjnWLMhTzw7oV31nkPIDRxFy5OQT2OP5++Ao0KRbaBZ6qfuT7lW1owKK0Xk3s7QTBvc/eNDA', | ||||||
|  |                       }, | ||||||
|  |                     }, | ||||||
|  |                   }, | ||||||
|  |                 }, | ||||||
|  |               }, | ||||||
|             } |             } | ||||||
|           }, |           }, | ||||||
|       '/client/r0/rooms/!localpart%3Aexample.com/invite': (var req) => {}, |       '/client/r0/rooms/!localpart%3Aexample.com/invite': (var req) => {}, | ||||||
|  | @ -1584,7 +1614,8 @@ class FakeMatrixApi extends MockClient { | ||||||
|       '/client/r0/keys/upload': (var req) => { |       '/client/r0/keys/upload': (var req) => { | ||||||
|             'one_time_key_counts': { |             'one_time_key_counts': { | ||||||
|               'curve25519': 10, |               'curve25519': 10, | ||||||
|               'signed_curve25519': 100, |               'signed_curve25519': | ||||||
|  |                   json.decode(req)['one_time_keys']?.keys?.length ?? 0, | ||||||
|             } |             } | ||||||
|           }, |           }, | ||||||
|       '/client/r0/keys/query': (var req) => { |       '/client/r0/keys/query': (var req) => { | ||||||
|  | @ -1625,8 +1656,42 @@ class FakeMatrixApi extends MockClient { | ||||||
|                   }, |                   }, | ||||||
|                   'signatures': {}, |                   'signatures': {}, | ||||||
|                 }, |                 }, | ||||||
|               } |               }, | ||||||
|             } |               '@test:fakeServer.notExisting': { | ||||||
|  |                 'GHTYAJCE': { | ||||||
|  |                   'user_id': '@test:fakeServer.notExisting', | ||||||
|  |                   'device_id': 'GHTYAJCE', | ||||||
|  |                   'algorithms': [ | ||||||
|  |                     'm.olm.v1.curve25519-aes-sha2', | ||||||
|  |                     'm.megolm.v1.aes-sha2' | ||||||
|  |                   ], | ||||||
|  |                   'keys': { | ||||||
|  |                     'curve25519:GHTYAJCE': | ||||||
|  |                         '7rvl3jORJkBiK4XX1e5TnGnqz068XfYJ0W++Ml63rgk', | ||||||
|  |                     'ed25519:GHTYAJCE': | ||||||
|  |                         'gjL//fyaFHADt9KBADGag8g7F8Up78B/K1zXeiEPLJo' | ||||||
|  |                   }, | ||||||
|  |                   'signatures': {}, | ||||||
|  |                 }, | ||||||
|  |               }, | ||||||
|  |               '@othertest:fakeServer.notExisting': { | ||||||
|  |                 'FOXDEVICE': { | ||||||
|  |                   'user_id': '@othertest:fakeServer.notExisting', | ||||||
|  |                   'device_id': 'FOXDEVICE', | ||||||
|  |                   'algorithms': [ | ||||||
|  |                     'm.olm.v1.curve25519-aes-sha2', | ||||||
|  |                     'm.megolm.v1.aes-sha2' | ||||||
|  |                   ], | ||||||
|  |                   'keys': { | ||||||
|  |                     'curve25519:FOXDEVICE': | ||||||
|  |                         'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg', | ||||||
|  |                     'ed25519:FOXDEVICE': | ||||||
|  |                         'R5/p04tticvdlNIxiiBIP0j9OQWv8ep6eEU6/lWKDxw', | ||||||
|  |                   }, | ||||||
|  |                   'signatures': {}, | ||||||
|  |                 }, | ||||||
|  |               }, | ||||||
|  |             }, | ||||||
|           }, |           }, | ||||||
|       '/client/r0/register': (var req) => { |       '/client/r0/register': (var req) => { | ||||||
|             'user_id': '@testuser:example.com', |             'user_id': '@testuser:example.com', | ||||||
|  | @ -1706,13 +1771,13 @@ class FakeMatrixApi extends MockClient { | ||||||
|       '/client/r0/directory/room/%23testalias%3Aexample.com': (var reqI) => {}, |       '/client/r0/directory/room/%23testalias%3Aexample.com': (var reqI) => {}, | ||||||
|       '/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.message/testtxid': |       '/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.message/testtxid': | ||||||
|           (var reqI) => { |           (var reqI) => { | ||||||
|                 'event_id': '42', |                 'event_id': '\$event${FakeMatrixApi.eventCounter++}', | ||||||
|               }, |               }, | ||||||
|       '/client/r0/rooms/!localpart%3Aexample.com/typing/%40alice%3Aexample.com': |       '/client/r0/rooms/!localpart%3Aexample.com/typing/%40alice%3Aexample.com': | ||||||
|           (var req) => {}, |           (var req) => {}, | ||||||
|       '/client/r0/rooms/%211234%3Aexample.com/send/m.room.message/1234': |       '/client/r0/rooms/%211234%3Aexample.com/send/m.room.message/1234': | ||||||
|           (var reqI) => { |           (var reqI) => { | ||||||
|                 'event_id': '42', |                 'event_id': '\$event${FakeMatrixApi.eventCounter++}', | ||||||
|               }, |               }, | ||||||
|       '/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags/testtag': |       '/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags/testtag': | ||||||
|           (var req) => {}, |           (var req) => {}, | ||||||
|  |  | ||||||
|  | @ -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 <https://www.gnu.org/licenses/>. |  | ||||||
|  */ |  | ||||||
| 
 |  | ||||||
| import 'dart:convert'; |  | ||||||
| import 'package:famedlysdk/famedlysdk.dart'; |  | ||||||
| import 'package:test/test.dart'; |  | ||||||
| 
 |  | ||||||
| import 'fake_matrix_api.dart'; |  | ||||||
| import 'fake_database.dart'; |  | ||||||
| 
 |  | ||||||
| Map<String, dynamic> jsonDecode(dynamic payload) { |  | ||||||
|   if (payload is String) { |  | ||||||
|     try { |  | ||||||
|       return json.decode(payload); |  | ||||||
|     } catch (e) { |  | ||||||
|       return {}; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   if (payload is Map<String, dynamic>) return payload; |  | ||||||
|   return {}; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void main() { |  | ||||||
|   /// All Tests related to device keys |  | ||||||
|   test('fromJson', () async { |  | ||||||
|     var rawJson = <String, dynamic>{ |  | ||||||
|       '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); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
|  | @ -26,7 +26,7 @@ import 'package:famedlysdk/src/database/database.dart' | ||||||
|     show DbRoom, DbRoomState, DbRoomAccountData; |     show DbRoom, DbRoomState, DbRoomAccountData; | ||||||
| import 'package:test/test.dart'; | import 'package:test/test.dart'; | ||||||
| 
 | 
 | ||||||
| import 'fake_matrix_api.dart'; | import 'fake_client.dart'; | ||||||
| 
 | 
 | ||||||
| import 'dart:typed_data'; | import 'dart:typed_data'; | ||||||
| 
 | 
 | ||||||
|  | @ -37,15 +37,7 @@ void main() { | ||||||
|   /// All Tests related to the Event |   /// All Tests related to the Event | ||||||
|   group('Room', () { |   group('Room', () { | ||||||
|     test('Login', () async { |     test('Login', () async { | ||||||
|       matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi()); |       matrix = await getClient(); | ||||||
| 
 |  | ||||||
|       final checkResp = |  | ||||||
|           await matrix.checkServer('https://fakeServer.notExisting'); |  | ||||||
| 
 |  | ||||||
|       final loginResp = await matrix.login('test', '1234'); |  | ||||||
| 
 |  | ||||||
|       expect(checkResp, true); |  | ||||||
|       expect(loginResp, true); |  | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     test('Create from json', () async { |     test('Create from json', () async { | ||||||
|  | @ -315,7 +307,7 @@ void main() { | ||||||
| 
 | 
 | ||||||
|     test('getTimeline', () async { |     test('getTimeline', () async { | ||||||
|       final timeline = await room.getTimeline(); |       final timeline = await room.getTimeline(); | ||||||
|       expect(timeline.events, []); |       expect(timeline.events.length, 0); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     test('getUserByMXID', () async { |     test('getUserByMXID', () async { | ||||||
|  | @ -338,13 +330,13 @@ void main() { | ||||||
|       final dynamic resp = await room.sendEvent( |       final dynamic resp = await room.sendEvent( | ||||||
|           {'msgtype': 'm.text', 'body': 'hello world'}, |           {'msgtype': 'm.text', 'body': 'hello world'}, | ||||||
|           txid: 'testtxid'); |           txid: 'testtxid'); | ||||||
|       expect(resp, '42'); |       expect(resp.startsWith('\$event'), true); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     test('sendEvent', () async { |     test('sendEvent', () async { | ||||||
|       final dynamic resp = |       final dynamic resp = | ||||||
|           await room.sendTextEvent('Hello world', txid: 'testtxid'); |           await room.sendTextEvent('Hello world', txid: 'testtxid'); | ||||||
|       expect(resp, '42'); |       expect(resp.startsWith('\$event'), true); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     // Not working because there is no real file to test it... |     // Not working because there is no real file to test it... | ||||||
|  | @ -388,60 +380,6 @@ void main() { | ||||||
|       ); |       ); | ||||||
|       expect(room.encrypted, true); |       expect(room.encrypted, true); | ||||||
|       expect(room.encryptionAlgorithm, 'm.megolm.v1.aes-sha2'); |       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 { |     test('setPushRuleState', () async { | ||||||
|  |  | ||||||
|  | @ -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 <https://www.gnu.org/licenses/>. |  | ||||||
|  */ |  | ||||||
| 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())); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
|  | @ -143,7 +143,8 @@ void main() { | ||||||
|       expect(updateCount, 5); |       expect(updateCount, 5); | ||||||
|       expect(insertList, [0, 0, 0]); |       expect(insertList, [0, 0, 0]); | ||||||
|       expect(insertList.length, timeline.events.length); |       expect(insertList.length, timeline.events.length); | ||||||
|       expect(timeline.events[0].eventId, '42'); |       final eventId = timeline.events[0].eventId; | ||||||
|  |       expect(eventId.startsWith('\$event'), true); | ||||||
|       expect(timeline.events[0].status, 1); |       expect(timeline.events[0].status, 1); | ||||||
| 
 | 
 | ||||||
|       client.onEvent.add(EventUpdate( |       client.onEvent.add(EventUpdate( | ||||||
|  | @ -155,7 +156,7 @@ void main() { | ||||||
|             'content': {'msgtype': 'm.text', 'body': 'test'}, |             'content': {'msgtype': 'm.text', 'body': 'test'}, | ||||||
|             'sender': '@alice:example.com', |             'sender': '@alice:example.com', | ||||||
|             'status': 2, |             'status': 2, | ||||||
|             'event_id': '42', |             'event_id': eventId, | ||||||
|             'unsigned': {'transaction_id': '1234'}, |             'unsigned': {'transaction_id': '1234'}, | ||||||
|             'origin_server_ts': DateTime.now().millisecondsSinceEpoch |             'origin_server_ts': DateTime.now().millisecondsSinceEpoch | ||||||
|           }, |           }, | ||||||
|  | @ -166,7 +167,7 @@ void main() { | ||||||
|       expect(updateCount, 6); |       expect(updateCount, 6); | ||||||
|       expect(insertList, [0, 0, 0]); |       expect(insertList, [0, 0, 0]); | ||||||
|       expect(insertList.length, timeline.events.length); |       expect(insertList.length, timeline.events.length); | ||||||
|       expect(timeline.events[0].eventId, '42'); |       expect(timeline.events[0].eventId, eventId); | ||||||
|       expect(timeline.events[0].status, 2); |       expect(timeline.events[0].status, 2); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -88,7 +88,8 @@ void test() async { | ||||||
|   await room.enableEncryption(); |   await room.enableEncryption(); | ||||||
|   await Future.delayed(Duration(seconds: 5)); |   await Future.delayed(Duration(seconds: 5)); | ||||||
|   assert(room.encrypted == true); |   assert(room.encrypted == true); | ||||||
|   assert(room.outboundGroupSession == null); |   assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) == | ||||||
|  |       null); | ||||||
| 
 | 
 | ||||||
|   print('++++ ($testUserA) Check known olm devices ++++'); |   print('++++ ($testUserA) Check known olm devices ++++'); | ||||||
|   assert(testClientA.userDeviceKeys.containsKey(testUserB)); |   assert(testClientA.userDeviceKeys.containsKey(testUserB)); | ||||||
|  | @ -123,16 +124,30 @@ void test() async { | ||||||
|   print("++++ ($testUserA) Send encrypted message: '$testMessage' ++++"); |   print("++++ ($testUserA) Send encrypted message: '$testMessage' ++++"); | ||||||
|   await room.sendTextEvent(testMessage); |   await room.sendTextEvent(testMessage); | ||||||
|   await Future.delayed(Duration(seconds: 5)); |   await Future.delayed(Duration(seconds: 5)); | ||||||
|   assert(room.outboundGroupSession != null); |   assert(room.client.encryption.keyManager.getOutboundGroupSession(room.id) != | ||||||
|   var currentSessionIdA = room.outboundGroupSession.session_id(); |       null); | ||||||
|   assert(room.inboundGroupSessions |   var currentSessionIdA = room.client.encryption.keyManager | ||||||
|       .containsKey(room.outboundGroupSession.session_id())); |       .getOutboundGroupSession(room.id) | ||||||
|   assert(testClientA.olmSessions[testClientB.identityKey].length == 1); |       .outboundGroupSession | ||||||
|   assert(testClientB.olmSessions[testClientA.identityKey].length == 1); |       .session_id(); | ||||||
|   assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() == |   assert(room.client.encryption.keyManager | ||||||
|       testClientB.olmSessions[testClientA.identityKey].first.session_id()); |           .getInboundGroupSession(room.id, currentSessionIdA, '') != | ||||||
|   assert(inviteRoom.inboundGroupSessions |       null); | ||||||
|       .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(inviteRoom.client.encryption.keyManager | ||||||
|  |           .getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') != | ||||||
|  |       null); | ||||||
|   assert(room.lastMessage == testMessage); |   assert(room.lastMessage == testMessage); | ||||||
|   assert(inviteRoom.lastMessage == testMessage); |   assert(inviteRoom.lastMessage == testMessage); | ||||||
|   print( |   print( | ||||||
|  | @ -141,14 +156,27 @@ void test() async { | ||||||
|   print("++++ ($testUserA) Send again encrypted message: '$testMessage2' ++++"); |   print("++++ ($testUserA) Send again encrypted message: '$testMessage2' ++++"); | ||||||
|   await room.sendTextEvent(testMessage2); |   await room.sendTextEvent(testMessage2); | ||||||
|   await Future.delayed(Duration(seconds: 5)); |   await Future.delayed(Duration(seconds: 5)); | ||||||
|   assert(testClientA.olmSessions[testClientB.identityKey].length == 1); |   assert(testClientA | ||||||
|   assert(testClientB.olmSessions[testClientA.identityKey].length == 1); |           .encryption.olmManager.olmSessions[testClientB.identityKey].length == | ||||||
|   assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() == |       1); | ||||||
|       testClientB.olmSessions[testClientA.identityKey].first.session_id()); |   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(room.client.encryption.keyManager | ||||||
|   assert(inviteRoom.inboundGroupSessions |           .getOutboundGroupSession(room.id) | ||||||
|       .containsKey(room.outboundGroupSession.session_id())); |           .outboundGroupSession | ||||||
|  |           .session_id() == | ||||||
|  |       currentSessionIdA); | ||||||
|  |   assert(room.client.encryption.keyManager | ||||||
|  |           .getInboundGroupSession(room.id, currentSessionIdA, '') != | ||||||
|  |       null); | ||||||
|   assert(room.lastMessage == testMessage2); |   assert(room.lastMessage == testMessage2); | ||||||
|   assert(inviteRoom.lastMessage == testMessage2); |   assert(inviteRoom.lastMessage == testMessage2); | ||||||
|   print( |   print( | ||||||
|  | @ -157,14 +185,31 @@ void test() async { | ||||||
|   print("++++ ($testUserB) Send again encrypted message: '$testMessage3' ++++"); |   print("++++ ($testUserB) Send again encrypted message: '$testMessage3' ++++"); | ||||||
|   await inviteRoom.sendTextEvent(testMessage3); |   await inviteRoom.sendTextEvent(testMessage3); | ||||||
|   await Future.delayed(Duration(seconds: 5)); |   await Future.delayed(Duration(seconds: 5)); | ||||||
|   assert(testClientA.olmSessions[testClientB.identityKey].length == 1); |   assert(testClientA | ||||||
|   assert(testClientB.olmSessions[testClientA.identityKey].length == 1); |           .encryption.olmManager.olmSessions[testClientB.identityKey].length == | ||||||
|   assert(room.outboundGroupSession.session_id() == currentSessionIdA); |       1); | ||||||
|   assert(inviteRoom.outboundGroupSession != null); |   assert(testClientB | ||||||
|   assert(inviteRoom.inboundGroupSessions |           .encryption.olmManager.olmSessions[testClientA.identityKey].length == | ||||||
|       .containsKey(inviteRoom.outboundGroupSession.session_id())); |       1); | ||||||
|   assert(room.inboundGroupSessions |   assert(room.client.encryption.keyManager | ||||||
|       .containsKey(inviteRoom.outboundGroupSession.session_id())); |           .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(inviteRoom.lastMessage == testMessage3); | ||||||
|   assert(room.lastMessage == testMessage3); |   assert(room.lastMessage == testMessage3); | ||||||
|   print( |   print( | ||||||
|  | @ -180,18 +225,42 @@ void test() async { | ||||||
|   print("++++ ($testUserA) Send again encrypted message: '$testMessage4' ++++"); |   print("++++ ($testUserA) Send again encrypted message: '$testMessage4' ++++"); | ||||||
|   await room.sendTextEvent(testMessage4); |   await room.sendTextEvent(testMessage4); | ||||||
|   await Future.delayed(Duration(seconds: 5)); |   await Future.delayed(Duration(seconds: 5)); | ||||||
|   assert(testClientA.olmSessions[testClientB.identityKey].length == 1); |   assert(testClientA | ||||||
|   assert(testClientB.olmSessions[testClientA.identityKey].length == 1); |           .encryption.olmManager.olmSessions[testClientB.identityKey].length == | ||||||
|   assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() == |       1); | ||||||
|       testClientB.olmSessions[testClientA.identityKey].first.session_id()); |   assert(testClientB | ||||||
|   assert(testClientA.olmSessions[testClientC.identityKey].length == 1); |           .encryption.olmManager.olmSessions[testClientA.identityKey].length == | ||||||
|   assert(testClientC.olmSessions[testClientA.identityKey].length == 1); |       1); | ||||||
|   assert(testClientA.olmSessions[testClientC.identityKey].first.session_id() == |   assert(testClientA | ||||||
|       testClientC.olmSessions[testClientA.identityKey].first.session_id()); |           .encryption.olmManager.olmSessions[testClientB.identityKey].first | ||||||
|   assert(room.outboundGroupSession.session_id() != currentSessionIdA); |           .session_id() == | ||||||
|   currentSessionIdA = room.outboundGroupSession.session_id(); |       testClientB | ||||||
|   assert(inviteRoom.inboundGroupSessions |           .encryption.olmManager.olmSessions[testClientA.identityKey].first | ||||||
|       .containsKey(room.outboundGroupSession.session_id())); |           .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(room.lastMessage == testMessage4); | ||||||
|   assert(inviteRoom.lastMessage == testMessage4); |   assert(inviteRoom.lastMessage == testMessage4); | ||||||
|   print( |   print( | ||||||
|  | @ -206,14 +275,30 @@ void test() async { | ||||||
|   print("++++ ($testUserA) Send again encrypted message: '$testMessage6' ++++"); |   print("++++ ($testUserA) Send again encrypted message: '$testMessage6' ++++"); | ||||||
|   await room.sendTextEvent(testMessage6); |   await room.sendTextEvent(testMessage6); | ||||||
|   await Future.delayed(Duration(seconds: 5)); |   await Future.delayed(Duration(seconds: 5)); | ||||||
|   assert(testClientA.olmSessions[testClientB.identityKey].length == 1); |   assert(testClientA | ||||||
|   assert(testClientB.olmSessions[testClientA.identityKey].length == 1); |           .encryption.olmManager.olmSessions[testClientB.identityKey].length == | ||||||
|   assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() == |       1); | ||||||
|       testClientB.olmSessions[testClientA.identityKey].first.session_id()); |   assert(testClientB | ||||||
|   assert(room.outboundGroupSession.session_id() != currentSessionIdA); |           .encryption.olmManager.olmSessions[testClientA.identityKey].length == | ||||||
|   currentSessionIdA = room.outboundGroupSession.session_id(); |       1); | ||||||
|   assert(inviteRoom.inboundGroupSessions |   assert(testClientA | ||||||
|       .containsKey(room.outboundGroupSession.session_id())); |           .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(room.lastMessage == testMessage6); | ||||||
|   assert(inviteRoom.lastMessage == testMessage6); |   assert(inviteRoom.lastMessage == testMessage6); | ||||||
|   print( |   print( | ||||||
|  | @ -241,18 +326,18 @@ void test() async { | ||||||
|     assert(restoredRoom.inboundGroupSessions.keys.toList()[i] == |     assert(restoredRoom.inboundGroupSessions.keys.toList()[i] == | ||||||
|         room.inboundGroupSessions.keys.toList()[i]); |         room.inboundGroupSessions.keys.toList()[i]); | ||||||
|   } |   } | ||||||
|   assert(testClientA.olmSessions[testClientB.identityKey].length == 1); |   assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].length == 1); | ||||||
|   assert(testClientB.olmSessions[testClientA.identityKey].length == 1); |   assert(testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].length == 1); | ||||||
|   assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() == |   assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].first.session_id() == | ||||||
|       testClientB.olmSessions[testClientA.identityKey].first.session_id()); |       testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].first.session_id()); | ||||||
| 
 | 
 | ||||||
|   print("++++ ($testUserA) Send again encrypted message: '$testMessage5' ++++"); |   print("++++ ($testUserA) Send again encrypted message: '$testMessage5' ++++"); | ||||||
|   await restoredRoom.sendTextEvent(testMessage5); |   await restoredRoom.sendTextEvent(testMessage5); | ||||||
|   await Future.delayed(Duration(seconds: 5)); |   await Future.delayed(Duration(seconds: 5)); | ||||||
|   assert(testClientA.olmSessions[testClientB.identityKey].length == 1); |   assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].length == 1); | ||||||
|   assert(testClientB.olmSessions[testClientA.identityKey].length == 1); |   assert(testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].length == 1); | ||||||
|   assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() == |   assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey].first.session_id() == | ||||||
|       testClientB.olmSessions[testClientA.identityKey].first.session_id()); |       testClientB.encryption.olmManager.olmSessions[testClientA.identityKey].first.session_id()); | ||||||
|   assert(restoredRoom.lastMessage == testMessage5); |   assert(restoredRoom.lastMessage == testMessage5); | ||||||
|   assert(inviteRoom.lastMessage == testMessage5); |   assert(inviteRoom.lastMessage == testMessage5); | ||||||
|   assert(testClientB.getRoomById(roomId).lastMessage == testMessage5); |   assert(testClientB.getRoomById(roomId).lastMessage == testMessage5); | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue