Merge branch 'soru/key-requests' into 'master'

Make room key sharing requests (hopefully) more robust and spec-compliant

See merge request famedly/famedlysdk!328
This commit is contained in:
Christian Pauly 2020-05-29 06:49:37 +00:00
commit b396fc7d71
9 changed files with 557 additions and 115 deletions

View file

@ -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/public_rooms_response.dart';
export 'package:famedlysdk/src/utils/push_rules.dart'; export 'package:famedlysdk/src/utils/push_rules.dart';
export 'package:famedlysdk/src/utils/receipt.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/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/utils/turn_server_credentials.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/account_data.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/presence.dart'; export 'package:famedlysdk/src/presence.dart';
export 'package:famedlysdk/src/room.dart'; export 'package:famedlysdk/src/room.dart';
export 'package:famedlysdk/src/room_account_data.dart'; export 'package:famedlysdk/src/room_account_data.dart';

View file

@ -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/matrix_file.dart';
import 'package:famedlysdk/src/utils/open_id_credentials.dart'; import 'package:famedlysdk/src/utils/open_id_credentials.dart';
import 'package:famedlysdk/src/utils/public_rooms_response.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/session_key.dart';
import 'package:famedlysdk/src/utils/to_device_event.dart'; import 'package:famedlysdk/src/utils/to_device_event.dart';
import 'package:famedlysdk/src/utils/turn_server_credentials.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/pusher.dart';
import 'utils/well_known_informations.dart'; import 'utils/well_known_informations.dart';
import 'utils/key_verification.dart'; 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);
@ -76,6 +76,7 @@ class Client {
int get id => _id; int get id => _id;
Database database; Database database;
KeyManager keyManager;
bool enableE2eeRecovery; bool enableE2eeRecovery;
@ -86,6 +87,7 @@ class Client {
/// enableE2eeRecovery: Enable additional logic to try to recover from bad e2ee sessions /// enableE2eeRecovery: Enable additional logic to try to recover from bad e2ee sessions
Client(this.clientName, Client(this.clientName,
{this.debug = false, this.database, this.enableE2eeRecovery = false}) { {this.debug = false, this.database, this.enableE2eeRecovery = false}) {
keyManager = KeyManager(this);
onLoginStateChanged.stream.listen((loginState) { onLoginStateChanged.stream.listen((loginState) {
print('LoginState: ${loginState.toString()}'); print('LoginState: ${loginState.toString()}');
}); });
@ -155,6 +157,12 @@ class Client {
int _timeoutFactor = 1; int _timeoutFactor = 1;
int _transactionCounter = 0;
String generateUniqueTransactionId() {
_transactionCounter++;
return '${clientName}-${_transactionCounter}-${DateTime.now().millisecondsSinceEpoch}';
}
Room getRoomByAlias(String alias) { Room getRoomByAlias(String alias) {
for (var i = 0; i < rooms.length; i++) { for (var i = 0; i < rooms.length; i++) {
if (rooms[i].canonicalAlias == alias) return rooms[i]; if (rooms[i].canonicalAlias == alias) return rooms[i];
@ -816,6 +824,11 @@ class Client {
return _sync(); return _sync();
} }
/// Used for testing only
void setUserId(String s) {
_userID = s;
}
StreamSubscription _userEventSub; StreamSubscription _userEventSub;
/// Resets all settings and stops the synchronisation. /// Resets all settings and stops the synchronisation.
@ -1157,6 +1170,10 @@ class Client {
if (toDeviceEvent.type.startsWith('m.key.verification.')) { if (toDeviceEvent.type.startsWith('m.key.verification.')) {
_handleToDeviceKeyVerificationRequest(toDeviceEvent); _handleToDeviceKeyVerificationRequest(toDeviceEvent);
} }
if (['m.room_key_request', 'm.forwarded_room_key']
.contains(toDeviceEvent.type)) {
keyManager.handleToDeviceEvent(toDeviceEvent);
}
onToDeviceEvent.add(toDeviceEvent); onToDeviceEvent.add(toDeviceEvent);
} }
} }
@ -1517,7 +1534,6 @@ class Client {
try { try {
switch (toDeviceEvent.type) { switch (toDeviceEvent.type) {
case 'm.room_key': case 'm.room_key':
case 'm.forwarded_room_key':
final roomId = toDeviceEvent.content['room_id']; final roomId = toDeviceEvent.content['room_id'];
var room = getRoomById(roomId); var room = getRoomById(roomId);
if (room == null && addToPendingIfNotFound) { if (room == null && addToPendingIfNotFound) {
@ -1526,8 +1542,7 @@ class Client {
} }
room ??= Room(client: this, id: roomId); room ??= Room(client: this, id: roomId);
final String sessionId = toDeviceEvent.content['session_id']; final String sessionId = toDeviceEvent.content['session_id'];
if (toDeviceEvent.type == 'm.room_key' && if (userDeviceKeys.containsKey(toDeviceEvent.sender) &&
userDeviceKeys.containsKey(toDeviceEvent.sender) &&
userDeviceKeys[toDeviceEvent.sender] userDeviceKeys[toDeviceEvent.sender]
.deviceKeys .deviceKeys
.containsKey(toDeviceEvent.content['requesting_device_id'])) { .containsKey(toDeviceEvent.content['requesting_device_id'])) {
@ -1539,46 +1554,8 @@ class Client {
room.setInboundGroupSession( room.setInboundGroupSession(
sessionId, sessionId,
toDeviceEvent.content, 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; break;
} }
} catch (e) { } catch (e) {
@ -1904,6 +1881,7 @@ class Client {
} }
return ToDeviceEvent( return ToDeviceEvent(
content: plainContent['content'], content: plainContent['content'],
encryptedContent: toDeviceEvent.content,
type: plainContent['type'], type: plainContent['type'],
sender: toDeviceEvent.sender, sender: toDeviceEvent.sender,
); );
@ -2012,7 +1990,7 @@ class Client {
} }
} }
if (encrypted) type = 'm.room.encrypted'; if (encrypted) type = 'm.room.encrypted';
final messageID = 'msg${DateTime.now().millisecondsSinceEpoch}'; final messageID = generateUniqueTransactionId();
await jsonRequest( await jsonRequest(
type: HTTPType.PUT, type: HTTPType.PUT,
action: '/client/r0/sendToDevice/$type/$messageID', action: '/client/r0/sendToDevice/$type/$messageID',

214
lib/src/key_manager.dart Normal file
View file

@ -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 = <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);
}
}

View file

@ -835,9 +835,8 @@ class Room {
// Create new transaction id // Create new transaction id
String messageID; String messageID;
final now = DateTime.now().millisecondsSinceEpoch;
if (txid == null) { if (txid == null) {
messageID = 'msg$now'; messageID = client.generateUniqueTransactionId();
} else { } else {
messageID = txid; messageID = txid;
} }
@ -872,7 +871,7 @@ class Room {
'event_id': messageID, 'event_id': messageID,
'sender': client.userID, 'sender': client.userID,
'status': 0, 'status': 0,
'origin_server_ts': now, 'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
'content': content 'content': content
}, },
); );
@ -1849,33 +1848,7 @@ class Room {
final Set<String> _requestedSessionIds = <String>{}; final Set<String> _requestedSessionIds = <String>{};
Future<void> requestSessionKey(String sessionId, String senderKey) async { Future<void> requestSessionKey(String sessionId, String senderKey) async {
final users = await requestParticipants(); await client.keyManager.request(this, sessionId, senderKey);
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);
} }
Future<void> loadInboundGroupSessionKey(String sessionId, Future<void> loadInboundGroupSessionKey(String sessionId,

View file

@ -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<void> forwardKey() async {
var room = this.room;
await room.loadInboundGroupSessionKey(content['body']['session_id']);
final session = room.inboundGroupSessions[content['body']['session_id']];
var forwardedKeys = <dynamic>[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,
);
}
}

View file

@ -2,8 +2,9 @@ class ToDeviceEvent {
String sender; String sender;
String type; String type;
Map<String, dynamic> content; Map<String, dynamic> content;
Map<String, dynamic> encryptedContent;
ToDeviceEvent({this.sender, this.type, this.content}); ToDeviceEvent({this.sender, this.type, this.content, this.encryptedContent});
ToDeviceEvent.fromJson(Map<String, dynamic> json) { ToDeviceEvent.fromJson(Map<String, dynamic> json) {
sender = json['sender']; sender = json['sender'];

View file

@ -201,7 +201,7 @@ void main() {
await Future.delayed(Duration(milliseconds: 50)); await Future.delayed(Duration(milliseconds: 50));
expect(matrix.userDeviceKeys.length, 2); expect(matrix.userDeviceKeys.length, 2);
expect(matrix.userDeviceKeys['@alice:example.com'].outdated, false); 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( expect(
matrix.userDeviceKeys['@alice:example.com'].deviceKeys['JLAFKJWSCS'] matrix.userDeviceKeys['@alice:example.com'].deviceKeys['JLAFKJWSCS']
.verified, .verified,

View file

@ -29,6 +29,8 @@ import 'package:http/http.dart';
import 'package:http/testing.dart'; import 'package:http/testing.dart';
class FakeMatrixApi extends MockClient { class FakeMatrixApi extends MockClient {
static final calledEndpoints = <String, List<dynamic>>{};
FakeMatrixApi() FakeMatrixApi()
: super((request) async { : super((request) async {
// Collect data from Request // Collect data from Request
@ -53,6 +55,10 @@ class FakeMatrixApi extends MockClient {
} }
// Call API // Call API
if (!calledEndpoints.containsKey(action)) {
calledEndpoints[action] = <dynamic>[];
}
calledEndpoints[action].add(data);
if (api.containsKey(method) && api[method].containsKey(action)) { if (api.containsKey(method) && api[method].containsKey(action)) {
res = api[method][action](data); res = api[method][action](data);
if (res.containsKey('errcode')) { if (res.containsKey('errcode')) {
@ -859,6 +865,19 @@ class FakeMatrixApi extends MockClient {
} }
}, },
'unsigned': {'device_display_name': "Alice's mobile phone"} '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': {},
} }
} }
} }

View file

@ -21,12 +21,25 @@
* along with famedlysdk. If not, see <http://www.gnu.org/licenses/>. * along with famedlysdk. If not, see <http://www.gnu.org/licenses/>.
*/ */
import 'dart:convert';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'fake_matrix_api.dart'; import 'fake_matrix_api.dart';
import 'fake_database.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() { void main() {
/// All Tests related to device keys /// All Tests related to device keys
test('fromJson', () async { test('fromJson', () async {
@ -62,9 +75,290 @@ void main() {
room.inboundGroupSessions.keys.first; room.inboundGroupSessions.keys.first;
var roomKeyRequest = RoomKeyRequest.fromToDeviceEvent( 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 roomKeyRequest.forwardKey();
} }
await matrix.dispose(closeDatabase: true); 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);
});
} }