handleToDeviceEvent method
Handle an incoming to_device event that is related to key sharing
Implementation
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
if (event.type == EventTypes.RoomKeyRequest) {
if (event.content['request_id'] is! String) {
return; // invalid event
}
if (event.content['action'] == 'request') {
// we are *receiving* a request
Logs().i(
'[KeyManager] Received key sharing request from ${event.sender}:${event.content['requesting_device_id']}...',
);
if (!event.content.containsKey('body')) {
Logs().w('[KeyManager] No body, doing nothing');
return; // no body
}
final body = event.content.tryGetMap<String, Object?>('body');
if (body == null) {
Logs().w('[KeyManager] Wrong type for body, doing nothing');
return; // wrong type for body
}
final roomId = body.tryGet<String>('room_id');
if (roomId == null) {
Logs().w(
'[KeyManager] Wrong type for room_id or no room_id, doing nothing',
);
return; // wrong type for roomId or no roomId found
}
final device = client.userDeviceKeys[event.sender]
?.deviceKeys[event.content['requesting_device_id']];
if (device == null) {
Logs().w('[KeyManager] Device not found, doing nothing');
return; // device not found
}
if (device.userId == client.userID &&
device.deviceId == client.deviceID) {
Logs().i('[KeyManager] Request is by ourself, ignoring');
return; // ignore requests by ourself
}
final room = client.getRoomById(roomId);
if (room == null) {
Logs().i('[KeyManager] Unknown room, ignoring');
return; // unknown room
}
final sessionId = body.tryGet<String>('session_id');
if (sessionId == null) {
Logs().w(
'[KeyManager] Wrong type for session_id or no session_id, doing nothing',
);
return; // wrong type for session_id
}
// okay, let's see if we have this session at all
final session = await loadInboundGroupSession(room.id, sessionId);
if (session == null) {
Logs().i('[KeyManager] Unknown session, ignoring');
return; // we don't have this session anyways
}
if (event.content['request_id'] is! String) {
Logs().w(
'[KeyManager] Wrong type for request_id or no request_id, doing nothing',
);
return; // wrong type for request_id
}
final request = KeyManagerKeyShareRequest(
requestId: event.content.tryGet<String>('request_id')!,
devices: [device],
room: room,
sessionId: sessionId,
);
if (incomingShareRequests.containsKey(request.requestId)) {
Logs().i('[KeyManager] Already processed this request, ignoring');
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) {
Logs().i('[KeyManager] All checks out, forwarding key...');
// alright, we can forward the key
await roomKeyRequest.forwardKey();
} else if (device.encryptToDevice &&
session.allowedAtIndex
.tryGet<Map<String, Object?>>(device.userId)
?.tryGet(device.curve25519Key!) !=
null) {
// if we know the user may see the message, then we can just forward the key.
// we do not need to check if the device is verified, just if it is not blocked,
// as that is the logic we already initially try to send out the room keys.
final index =
session.allowedAtIndex[device.userId]![device.curve25519Key]!;
Logs().i(
'[KeyManager] Valid foreign request, forwarding key at index $index...',
);
await roomKeyRequest.forwardKey(index);
} else {
Logs()
.i('[KeyManager] Asking client, if the key should be forwarded');
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 == EventTypes.ForwardedRoomKey) {
// we *received* an incoming key request
final encryptedContent = event.encryptedContent;
if (encryptedContent == null) {
Logs().w(
'Ignoring an unencrypted forwarded key from a to device message',
event.toJson(),
);
return;
}
final request = outgoingShareRequests.values.firstWhereOrNull(
(r) =>
r.room.id == event.content['room_id'] &&
r.sessionId == event.content['session_id'],
);
if (request == null || request.canceled) {
return; // no associated request found or it got canceled
}
final device = request.devices.firstWhereOrNull(
(d) =>
d.userId == event.sender &&
d.curve25519Key == encryptedContent['sender_key'],
);
if (device == null) {
return; // someone we didn't send our request to replied....better ignore this
}
// we add the sender key to the forwarded key chain
if (event.content['forwarding_curve25519_key_chain'] is! List) {
event.content['forwarding_curve25519_key_chain'] = <String>[];
}
(event.content['forwarding_curve25519_key_chain'] as List)
.add(encryptedContent['sender_key']);
if (event.content['sender_claimed_ed25519_key'] is! String) {
Logs().w('sender_claimed_ed255519_key has wrong type');
return; // wrong type
}
// TODO: verify that the keys work to decrypt a message
// alright, all checks out, let's go ahead and store this session
await setInboundGroupSession(
request.room.id,
request.sessionId,
device.curve25519Key!,
event.content,
forwarded: true,
senderClaimedKeys: {
'ed25519': event.content['sender_claimed_ed25519_key'] as String,
},
);
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
}
// Send with send-to-device messaging
final sendToDeviceMessage = {
'action': 'request_cancellation',
'request_id': request.requestId,
'requesting_device_id': client.deviceID,
};
final data = <String, Map<String, Map<String, dynamic>>>{};
for (final device in request.devices) {
final userData = data[device.userId] ??= {};
userData[device.deviceId!] = sendToDeviceMessage;
}
await client.sendToDevice(
EventTypes.RoomKeyRequest,
client.generateUniqueTransactionId(),
data,
);
} else if (event.type == EventTypes.RoomKey) {
Logs().v(
'[KeyManager] Received room key with session ${event.content['session_id']}',
);
final encryptedContent = event.encryptedContent;
if (encryptedContent == null) {
Logs().v('[KeyManager] not encrypted, ignoring...');
return; // the event wasn't encrypted, this is a security risk;
}
final roomId = event.content.tryGet<String>('room_id');
final sessionId = event.content.tryGet<String>('session_id');
if (roomId == null || sessionId == null) {
Logs().w(
'Either room_id or session_id are not the expected type or missing',
);
return;
}
final sender_ed25519 = client.userDeviceKeys[event.sender]
?.deviceKeys[event.content['requesting_device_id']]?.ed25519Key;
if (sender_ed25519 != null) {
event.content['sender_claimed_ed25519_key'] = sender_ed25519;
}
Logs().v('[KeyManager] Keeping room key');
await setInboundGroupSession(
roomId,
sessionId,
encryptedContent['sender_key'],
event.content,
forwarded: false,
);
}
}