diff --git a/lib/famedlysdk.dart b/lib/famedlysdk.dart index b8d778b..94a0f15 100644 --- a/lib/famedlysdk.dart +++ b/lib/famedlysdk.dart @@ -38,7 +38,6 @@ export 'package:famedlysdk/src/utils/profile.dart'; export 'package:famedlysdk/src/utils/public_rooms_response.dart'; export 'package:famedlysdk/src/utils/push_rules.dart'; export 'package:famedlysdk/src/utils/receipt.dart'; -export 'package:famedlysdk/src/utils/room_key_request.dart'; export 'package:famedlysdk/src/utils/states_map.dart'; export 'package:famedlysdk/src/utils/to_device_event.dart'; export 'package:famedlysdk/src/utils/turn_server_credentials.dart'; @@ -47,6 +46,7 @@ export 'package:famedlysdk/src/utils/well_known_informations.dart'; export 'package:famedlysdk/src/account_data.dart'; export 'package:famedlysdk/src/client.dart'; export 'package:famedlysdk/src/event.dart'; +export 'package:famedlysdk/src/key_manager.dart'; export 'package:famedlysdk/src/presence.dart'; export 'package:famedlysdk/src/room.dart'; export 'package:famedlysdk/src/room_account_data.dart'; diff --git a/lib/src/client.dart b/lib/src/client.dart index 6282788..addd9bf 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -35,7 +35,6 @@ import 'package:famedlysdk/src/utils/device_keys_list.dart'; import 'package:famedlysdk/src/utils/matrix_file.dart'; import 'package:famedlysdk/src/utils/open_id_credentials.dart'; import 'package:famedlysdk/src/utils/public_rooms_response.dart'; -import 'package:famedlysdk/src/utils/room_key_request.dart'; import 'package:famedlysdk/src/utils/session_key.dart'; import 'package:famedlysdk/src/utils/to_device_event.dart'; import 'package:famedlysdk/src/utils/turn_server_credentials.dart'; @@ -57,6 +56,7 @@ import 'database/database.dart' show Database; import 'utils/pusher.dart'; import 'utils/well_known_informations.dart'; import 'utils/key_verification.dart'; +import 'key_manager.dart'; typedef RoomSorter = int Function(Room a, Room b); @@ -76,6 +76,7 @@ class Client { int get id => _id; Database database; + KeyManager keyManager; bool enableE2eeRecovery; @@ -86,6 +87,7 @@ class Client { /// enableE2eeRecovery: Enable additional logic to try to recover from bad e2ee sessions Client(this.clientName, {this.debug = false, this.database, this.enableE2eeRecovery = false}) { + keyManager = KeyManager(this); onLoginStateChanged.stream.listen((loginState) { print('LoginState: ${loginState.toString()}'); }); @@ -155,6 +157,12 @@ class Client { int _timeoutFactor = 1; + int _transactionCounter = 0; + String generateUniqueTransactionId() { + _transactionCounter++; + return '${clientName}-${_transactionCounter}-${DateTime.now().millisecondsSinceEpoch}'; + } + Room getRoomByAlias(String alias) { for (var i = 0; i < rooms.length; i++) { if (rooms[i].canonicalAlias == alias) return rooms[i]; @@ -816,6 +824,11 @@ class Client { return _sync(); } + /// Used for testing only + void setUserId(String s) { + _userID = s; + } + StreamSubscription _userEventSub; /// Resets all settings and stops the synchronisation. @@ -1157,6 +1170,10 @@ class Client { if (toDeviceEvent.type.startsWith('m.key.verification.')) { _handleToDeviceKeyVerificationRequest(toDeviceEvent); } + if (['m.room_key_request', 'm.forwarded_room_key'] + .contains(toDeviceEvent.type)) { + keyManager.handleToDeviceEvent(toDeviceEvent); + } onToDeviceEvent.add(toDeviceEvent); } } @@ -1517,7 +1534,6 @@ class Client { try { switch (toDeviceEvent.type) { case 'm.room_key': - case 'm.forwarded_room_key': final roomId = toDeviceEvent.content['room_id']; var room = getRoomById(roomId); if (room == null && addToPendingIfNotFound) { @@ -1526,8 +1542,7 @@ class Client { } room ??= Room(client: this, id: roomId); final String sessionId = toDeviceEvent.content['session_id']; - if (toDeviceEvent.type == 'm.room_key' && - userDeviceKeys.containsKey(toDeviceEvent.sender) && + if (userDeviceKeys.containsKey(toDeviceEvent.sender) && userDeviceKeys[toDeviceEvent.sender] .deviceKeys .containsKey(toDeviceEvent.content['requesting_device_id'])) { @@ -1539,46 +1554,8 @@ class Client { room.setInboundGroupSession( sessionId, toDeviceEvent.content, - forwarded: toDeviceEvent.type == 'm.forwarded_room_key', + forwarded: false, ); - if (toDeviceEvent.type == 'm.forwarded_room_key') { - await sendToDevice( - [], - 'm.room_key_request', - { - 'action': 'request_cancellation', - 'request_id': base64 - .encode(utf8.encode(toDeviceEvent.content['room_id'])), - 'requesting_device_id': room.client.deviceID, - }, - encrypted: false, - ); - } - break; - case 'm.room_key_request': - if (!toDeviceEvent.content.containsKey('body')) break; - var room = getRoomById(toDeviceEvent.content['body']['room_id']); - DeviceKeys deviceKeys; - final String sessionId = toDeviceEvent.content['body']['session_id']; - if (userDeviceKeys.containsKey(toDeviceEvent.sender) && - userDeviceKeys[toDeviceEvent.sender] - .deviceKeys - .containsKey(toDeviceEvent.content['requesting_device_id'])) { - deviceKeys = userDeviceKeys[toDeviceEvent.sender] - .deviceKeys[toDeviceEvent.content['requesting_device_id']]; - await room.loadInboundGroupSessionKey(sessionId); - if (room.inboundGroupSessions.containsKey(sessionId)) { - final roomKeyRequest = - RoomKeyRequest.fromToDeviceEvent(toDeviceEvent, this); - if (deviceKeys.userId == userID && - deviceKeys.verified && - !deviceKeys.blocked) { - await roomKeyRequest.forwardKey(); - } else if (roomKeyRequest.requestingDevice != null) { - onRoomKeyRequest.add(roomKeyRequest); - } - } - } break; } } catch (e) { @@ -1904,6 +1881,7 @@ class Client { } return ToDeviceEvent( content: plainContent['content'], + encryptedContent: toDeviceEvent.content, type: plainContent['type'], sender: toDeviceEvent.sender, ); @@ -2012,7 +1990,7 @@ class Client { } } if (encrypted) type = 'm.room.encrypted'; - final messageID = 'msg${DateTime.now().millisecondsSinceEpoch}'; + final messageID = generateUniqueTransactionId(); await jsonRequest( type: HTTPType.PUT, action: '/client/r0/sendToDevice/$type/$messageID', diff --git a/lib/src/key_manager.dart b/lib/src/key_manager.dart new file mode 100644 index 0000000..0b5401d --- /dev/null +++ b/lib/src/key_manager.dart @@ -0,0 +1,214 @@ +import 'client.dart'; +import 'room.dart'; +import 'utils/to_device_event.dart'; +import 'utils/device_keys_list.dart'; + +class KeyManager { + final Client client; + final outgoingShareRequests = {}; + final incomingShareRequests = {}; + + KeyManager(this.client); + + /// Request a certain key from another device + Future request(Room room, String sessionId, String senderKey) async { + // while we just send the to-device event to '*', we still need to save the + // devices themself to know where to send the cancel to after receiving a reply + final devices = await room.getUserDeviceKeys(); + final requestId = client.generateUniqueTransactionId(); + final request = KeyManagerKeyShareRequest( + requestId: requestId, + devices: devices, + room: room, + sessionId: sessionId, + senderKey: senderKey, + ); + await client.sendToDevice( + [], + 'm.room_key_request', + { + 'action': 'request', + 'body': { + 'algorithm': 'm.megolm.v1.aes-sha2', + 'room_id': room.id, + 'sender_key': senderKey, + 'session_id': sessionId, + }, + 'request_id': requestId, + 'requesting_device_id': client.deviceID, + }, + encrypted: false, + toUsers: await room.requestParticipants()); + outgoingShareRequests[request.requestId] = request; + } + + /// Handle an incoming to_device event that is related to key sharing + Future handleToDeviceEvent(ToDeviceEvent event) async { + if (event.type == 'm.room_key_request') { + if (!event.content.containsKey('request_id')) { + return; // invalid event + } + if (event.content['action'] == 'request') { + // we are *receiving* a request + if (!event.content.containsKey('body')) { + return; // no body + } + if (!client.userDeviceKeys.containsKey(event.sender) || + !client.userDeviceKeys[event.sender].deviceKeys + .containsKey(event.content['requesting_device_id'])) { + return; // device not found + } + final device = client.userDeviceKeys[event.sender] + .deviceKeys[event.content['requesting_device_id']]; + if (device.userId == client.userID && + device.deviceId == client.deviceID) { + return; // ignore requests by ourself + } + final room = client.getRoomById(event.content['body']['room_id']); + if (room == null) { + return; // unknown room + } + final sessionId = event.content['body']['session_id']; + // okay, let's see if we have this session at all + await room.loadInboundGroupSessionKey(sessionId); + if (!room.inboundGroupSessions.containsKey(sessionId)) { + return; // we don't have this session anyways + } + final request = KeyManagerKeyShareRequest( + requestId: event.content['request_id'], + devices: [device], + room: room, + sessionId: event.content['body']['session_id'], + senderKey: event.content['body']['sender_key'], + ); + if (incomingShareRequests.containsKey(request.requestId)) { + return; // we don't want to process one and the same request multiple times + } + incomingShareRequests[request.requestId] = request; + final roomKeyRequest = + RoomKeyRequest.fromToDeviceEvent(event, this, request); + if (device.userId == client.userID && + device.verified && + !device.blocked) { + // alright, we can forward the key + await roomKeyRequest.forwardKey(); + } else { + client.onRoomKeyRequest + .add(roomKeyRequest); // let the client handle this + } + } else if (event.content['action'] == 'request_cancellation') { + // we got told to cancel an incoming request + if (!incomingShareRequests.containsKey(event.content['request_id'])) { + return; // we don't know this request anyways + } + // alright, let's just cancel this request + final request = incomingShareRequests[event.content['request_id']]; + request.canceled = true; + incomingShareRequests.remove(request.requestId); + } + } else if (event.type == 'm.forwarded_room_key') { + // we *received* an incoming key request + if (event.encryptedContent == null) { + return; // event wasn't encrypted, this is a security risk + } + final request = outgoingShareRequests.values.firstWhere( + (r) => + r.room.id == event.content['room_id'] && + r.sessionId == event.content['session_id'] && + r.senderKey == event.content['sender_key'], + orElse: () => null); + if (request == null || request.canceled) { + return; // no associated request found or it got canceled + } + final device = request.devices.firstWhere( + (d) => + d.userId == event.sender && + d.curve25519Key == event.encryptedContent['sender_key'], + orElse: () => null); + if (device == null) { + return; // someone we didn't send our request to replied....better ignore this + } + // TODO: verify that the keys work to decrypt a message + // alright, all checks out, let's go ahead and store this session + request.room.setInboundGroupSession(request.sessionId, event.content, + forwarded: true); + request.devices.removeWhere( + (k) => k.userId == device.userId && k.deviceId == device.deviceId); + outgoingShareRequests.remove(request.requestId); + // send cancel to all other devices + if (request.devices.isEmpty) { + return; // no need to send any cancellation + } + await client.sendToDevice( + request.devices, + 'm.room_key_request', + { + 'action': 'request_cancellation', + 'request_id': request.requestId, + 'requesting_device_id': client.deviceID, + }, + encrypted: false); + } + } +} + +class KeyManagerKeyShareRequest { + final String requestId; + final List devices; + final Room room; + final String sessionId; + final String senderKey; + bool canceled; + + KeyManagerKeyShareRequest( + {this.requestId, + this.devices, + this.room, + this.sessionId, + this.senderKey, + this.canceled = false}); +} + +class RoomKeyRequest extends ToDeviceEvent { + KeyManager keyManager; + KeyManagerKeyShareRequest request; + RoomKeyRequest.fromToDeviceEvent(ToDeviceEvent toDeviceEvent, + KeyManager keyManager, KeyManagerKeyShareRequest request) { + this.keyManager = keyManager; + this.request = request; + sender = toDeviceEvent.sender; + content = toDeviceEvent.content; + type = toDeviceEvent.type; + } + + Room get room => request.room; + + DeviceKeys get requestingDevice => request.devices.first; + + Future forwardKey() async { + if (request.canceled) { + keyManager.incomingShareRequests.remove(request.requestId); + return; // request is canceled, don't send anything + } + var room = this.room; + await room.loadInboundGroupSessionKey(request.sessionId); + final session = room.inboundGroupSessions[request.sessionId]; + var forwardedKeys = [keyManager.client.identityKey]; + for (final key in session.forwardingCurve25519KeyChain) { + forwardedKeys.add(key); + } + await requestingDevice.setVerified(true, keyManager.client); + var message = session.content; + message['forwarding_curve25519_key_chain'] = forwardedKeys; + + message['session_key'] = session.inboundGroupSession + .export_session(session.inboundGroupSession.first_known_index()); + // send the actual reply of the key back to the requester + await keyManager.client.sendToDevice( + [requestingDevice], + 'm.forwarded_room_key', + message, + ); + keyManager.incomingShareRequests.remove(request.requestId); + } +} diff --git a/lib/src/room.dart b/lib/src/room.dart index 4a4612e..b629d51 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -835,9 +835,8 @@ class Room { // Create new transaction id String messageID; - final now = DateTime.now().millisecondsSinceEpoch; if (txid == null) { - messageID = 'msg$now'; + messageID = client.generateUniqueTransactionId(); } else { messageID = txid; } @@ -872,7 +871,7 @@ class Room { 'event_id': messageID, 'sender': client.userID, 'status': 0, - 'origin_server_ts': now, + 'origin_server_ts': DateTime.now().millisecondsSinceEpoch, 'content': content }, ); @@ -1849,33 +1848,7 @@ class Room { final Set _requestedSessionIds = {}; Future requestSessionKey(String sessionId, String senderKey) async { - final users = await requestParticipants(); - await client.sendToDevice( - [], - 'm.room_key_request', - { - 'action': 'request_cancellation', - 'request_id': base64.encode(utf8.encode(sessionId)), - 'requesting_device_id': client.deviceID, - }, - encrypted: false, - toUsers: users); - await client.sendToDevice( - [], - 'm.room_key_request', - { - 'action': 'request', - 'body': { - 'algorithm': 'm.megolm.v1.aes-sha2', - 'room_id': id, - 'sender_key': senderKey, - 'session_id': sessionId, - }, - 'request_id': base64.encode(utf8.encode(sessionId)), - 'requesting_device_id': client.deviceID, - }, - encrypted: false, - toUsers: users); + await client.keyManager.request(this, sessionId, senderKey); } Future loadInboundGroupSessionKey(String sessionId, diff --git a/lib/src/utils/room_key_request.dart b/lib/src/utils/room_key_request.dart deleted file mode 100644 index 778e93e..0000000 --- a/lib/src/utils/room_key_request.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:famedlysdk/famedlysdk.dart'; - -class RoomKeyRequest extends ToDeviceEvent { - Client client; - RoomKeyRequest.fromToDeviceEvent(ToDeviceEvent toDeviceEvent, Client client) { - this.client = client; - sender = toDeviceEvent.sender; - content = toDeviceEvent.content; - type = toDeviceEvent.type; - } - - Room get room => client.getRoomById(content['body']['room_id']); - - DeviceKeys get requestingDevice => - client.userDeviceKeys[sender].deviceKeys[content['requesting_device_id']]; - - Future forwardKey() async { - var room = this.room; - await room.loadInboundGroupSessionKey(content['body']['session_id']); - final session = room.inboundGroupSessions[content['body']['session_id']]; - var forwardedKeys = [client.identityKey]; - for (final key in session.forwardingCurve25519KeyChain) { - forwardedKeys.add(key); - } - await requestingDevice.setVerified(true, client); - var message = session.content; - message['forwarding_curve25519_key_chain'] = forwardedKeys; - - message['session_key'] = session.inboundGroupSession - .export_session(session.inboundGroupSession.first_known_index()); - await client.sendToDevice( - [requestingDevice], - 'm.forwarded_room_key', - message, - ); - } -} diff --git a/lib/src/utils/to_device_event.dart b/lib/src/utils/to_device_event.dart index 3b7f59d..7d1a2f7 100644 --- a/lib/src/utils/to_device_event.dart +++ b/lib/src/utils/to_device_event.dart @@ -2,8 +2,9 @@ class ToDeviceEvent { String sender; String type; Map content; + Map encryptedContent; - ToDeviceEvent({this.sender, this.type, this.content}); + ToDeviceEvent({this.sender, this.type, this.content, this.encryptedContent}); ToDeviceEvent.fromJson(Map json) { sender = json['sender']; diff --git a/test/client_test.dart b/test/client_test.dart index fc3b939..8d49f49 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -201,7 +201,7 @@ void main() { await Future.delayed(Duration(milliseconds: 50)); expect(matrix.userDeviceKeys.length, 2); expect(matrix.userDeviceKeys['@alice:example.com'].outdated, false); - expect(matrix.userDeviceKeys['@alice:example.com'].deviceKeys.length, 1); + expect(matrix.userDeviceKeys['@alice:example.com'].deviceKeys.length, 2); expect( matrix.userDeviceKeys['@alice:example.com'].deviceKeys['JLAFKJWSCS'] .verified, diff --git a/test/fake_matrix_api.dart b/test/fake_matrix_api.dart index 54b4d05..1b5d552 100644 --- a/test/fake_matrix_api.dart +++ b/test/fake_matrix_api.dart @@ -29,6 +29,8 @@ import 'package:http/http.dart'; import 'package:http/testing.dart'; class FakeMatrixApi extends MockClient { + static final calledEndpoints = >{}; + FakeMatrixApi() : super((request) async { // Collect data from Request @@ -53,6 +55,10 @@ class FakeMatrixApi extends MockClient { } // Call API + if (!calledEndpoints.containsKey(action)) { + calledEndpoints[action] = []; + } + calledEndpoints[action].add(data); if (api.containsKey(method) && api[method].containsKey(action)) { res = api[method][action](data); if (res.containsKey('errcode')) { @@ -859,6 +865,19 @@ class FakeMatrixApi extends MockClient { } }, 'unsigned': {'device_display_name': "Alice's mobile phone"} + }, + 'OTHERDEVICE': { + 'user_id': '@alice:example.com', + 'device_id': 'OTHERDEVICE', + 'algorithms': [ + 'm.olm.v1.curve25519-aes-sha2', + 'm.megolm.v1.aes-sha2' + ], + 'keys': { + 'curve25519:OTHERDEVICE': 'blah', + 'ed25519:OTHERDEVICE': 'blah' + }, + 'signatures': {}, } } } diff --git a/test/room_key_request_test.dart b/test/room_key_request_test.dart index 4788cb5..5ff83c3 100644 --- a/test/room_key_request_test.dart +++ b/test/room_key_request_test.dart @@ -21,12 +21,25 @@ * along with famedlysdk. If not, see . */ +import 'dart:convert'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:test/test.dart'; import 'fake_matrix_api.dart'; import 'fake_database.dart'; +Map jsonDecode(dynamic payload) { + if (payload is String) { + try { + return json.decode(payload); + } catch (e) { + return {}; + } + } + if (payload is Map) return payload; + return {}; +} + void main() { /// All Tests related to device keys test('fromJson', () async { @@ -62,9 +75,290 @@ void main() { room.inboundGroupSessions.keys.first; var roomKeyRequest = RoomKeyRequest.fromToDeviceEvent( - ToDeviceEvent.fromJson(rawJson), matrix); + 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); + matrix.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); + matrix.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'] + .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': '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); + matrix.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); + }); }