handleToDeviceEvent method

Future<void> handleToDeviceEvent(
  1. ToDeviceEvent event
)

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