Make room key sharing requests (hopefully) more robust and spec-compliant
This commit is contained in:
parent
ce903a8152
commit
d672edf394
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
|
|
214
lib/src/key_manager.dart
Normal file
214
lib/src/key_manager.dart
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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<String> _requestedSessionIds = <String>{};
|
||||
|
||||
Future<void> 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<void> loadInboundGroupSessionKey(String sessionId,
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,8 +2,9 @@ class ToDeviceEvent {
|
|||
String sender;
|
||||
String type;
|
||||
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) {
|
||||
sender = json['sender'];
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -29,6 +29,8 @@ import 'package:http/http.dart';
|
|||
import 'package:http/testing.dart';
|
||||
|
||||
class FakeMatrixApi extends MockClient {
|
||||
static final calledEndpoints = <String, List<dynamic>>{};
|
||||
|
||||
FakeMatrixApi()
|
||||
: super((request) async {
|
||||
// Collect data from Request
|
||||
|
@ -53,6 +55,10 @@ class FakeMatrixApi extends MockClient {
|
|||
}
|
||||
|
||||
// Call API
|
||||
if (!calledEndpoints.containsKey(action)) {
|
||||
calledEndpoints[action] = <dynamic>[];
|
||||
}
|
||||
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': {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,12 +21,25 @@
|
|||
* along with famedlysdk. If not, see <http://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 {
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue