handlePayload method

Future<void> handlePayload(
  1. String type,
  2. Map<String, dynamic> payload, [
  3. String? eventId
])

Implementation

Future<void> handlePayload(
  String type,
  Map<String, dynamic> payload, [
  String? eventId,
]) async {
  if (isDone) {
    return; // no need to do anything with already canceled requests
  }
  while (_handlePayloadLock) {
    await Future.delayed(Duration(milliseconds: 50));
  }
  _handlePayloadLock = true;
  Logs().i('[Key Verification] Received type $type: $payload');
  try {
    var thisLastStep = lastStep;
    switch (type) {
      case EventTypes.KeyVerificationRequest:
        _deviceId ??= payload['from_device'];
        transactionId ??= eventId ?? payload['transaction_id'];
        // verify the timestamp
        final now = DateTime.now();
        final verifyTime =
            DateTime.fromMillisecondsSinceEpoch(payload['timestamp']);
        if (now.subtract(Duration(minutes: 10)).isAfter(verifyTime) ||
            now.add(Duration(minutes: 5)).isBefore(verifyTime)) {
          // if the request is more than 20min in the past we just silently fail it
          // to not generate too many cancels
          await cancel(
            'm.timeout',
            now.subtract(Duration(minutes: 20)).isAfter(verifyTime),
          );
          return;
        }

        // ensure we have the other sides keys
        if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
          await client.updateUserDeviceKeys(additionalUsers: {userId});
          if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
            await cancel('im.fluffychat.unknown_device');
            return;
          }
        }

        oppositePossibleMethods = List<String>.from(payload['methods']);
        // verify it has a method we can use
        possibleMethods = _calculatePossibleMethods(
          knownVerificationMethods,
          payload['methods'],
        );
        if (possibleMethods.isEmpty) {
          // reject it outright
          await cancel('m.unknown_method');
          return;
        }

        setState(KeyVerificationState.askAccept);
        break;
      case EventTypes.KeyVerificationReady:
        if (deviceId == '*') {
          _deviceId = payload['from_device']; // gotta set the real device id
          transactionId ??= eventId ?? payload['transaction_id'];
          // and broadcast the cancel to the other devices
          final devices = List<DeviceKeys>.from(
            client.userDeviceKeys[userId]?.deviceKeys.values ??
                Iterable.empty(),
          );
          devices.removeWhere(
            (d) => {deviceId, client.deviceID}.contains(d.deviceId),
          );
          final cancelPayload = <String, dynamic>{
            'reason': 'Another device accepted the request',
            'code': 'm.accepted',
          };
          makePayload(cancelPayload);
          await client.sendToDeviceEncrypted(
            devices,
            EventTypes.KeyVerificationCancel,
            cancelPayload,
          );
        }
        _deviceId ??= payload['from_device'];

        // ensure we have the other sides keys
        if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
          await client.updateUserDeviceKeys(additionalUsers: {userId});
          if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
            await cancel('im.fluffychat.unknown_device');
            return;
          }
        }

        oppositePossibleMethods = List<String>.from(payload['methods']);
        possibleMethods = _calculatePossibleMethods(
          knownVerificationMethods,
          payload['methods'],
        );
        if (possibleMethods.isEmpty) {
          // reject it outright
          await cancel('m.unknown_method');
          return;
        }
        // as both parties can send a start, the last step being "ready" is race-condition prone
        // as such, we better set it *before* we send our start
        lastStep = type;

        // setup QRData from outgoing request (incoming ready)
        qrCode = await generateQrCode();

        // play nice with sdks < 0.20.5
        // https://matrix.to/#/!KBwfdofYJUmnsVoqwn:famedly.de/$wlHXlLQJdfrqKAF5KkuQrXydwOhY_uyqfH4ReasZqnA?via=neko.dev&via=famedly.de&via=lihotzki.de
        if (!isQrSupported(knownVerificationMethods, payload['methods'])) {
          if (knownVerificationMethods.contains(EventTypes.Sas)) {
            final method = _method =
                _makeVerificationMethod(possibleMethods.first, this);
            await method.sendStart();
            setState(KeyVerificationState.waitingAccept);
          }
        } else {
          // allow user to choose
          setState(KeyVerificationState.askChoice);
        }

        break;
      case EventTypes.KeyVerificationStart:
        _deviceId ??= payload['from_device'];
        transactionId ??= eventId ?? payload['transaction_id'];
        if (_method != null) {
          // the other side sent us a start, even though we already sent one
          if (payload['method'] == _method!.type) {
            // same method. Determine priority
            final ourEntry = '${client.userID}|${client.deviceID}';
            final entries = [ourEntry, '$userId|$deviceId'];
            entries.sort();
            if (entries.first == ourEntry) {
              // our start won, nothing to do
              return;
            } else {
              // the other start won, let's hand off
              startedVerification = false; // it is now as if they started
              thisLastStep = lastStep =
                  EventTypes.KeyVerificationRequest; // we fake the last step
              _method!.dispose(); // in case anything got created already
            }
          } else {
            // methods don't match up, let's cancel this
            await cancel('m.unexpected_message');
            return;
          }
        }
        if (!(await verifyLastStep([
          EventTypes.KeyVerificationRequest,
          EventTypes.KeyVerificationReady,
        ]))) {
          return; // abort
        }
        if (!knownVerificationMethods.contains(payload['method'])) {
          await cancel('m.unknown_method');
          return;
        }

        if (lastStep == EventTypes.KeyVerificationRequest) {
          if (!possibleMethods.contains(payload['method'])) {
            await cancel('m.unknown_method');
            return;
          }
        }

        // ensure we have the other sides keys
        if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
          await client.updateUserDeviceKeys(additionalUsers: {userId});
          if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
            await cancel('im.fluffychat.unknown_device');
            return;
          }
        }

        _method = _makeVerificationMethod(payload['method'], this);
        if (lastStep == null) {
          // validate the start time
          if (room != null) {
            // we just silently ignore in-room-verification starts
            await cancel('m.unknown_method', true);
            return;
          }
          // validate the specific payload
          if (!_method!.validateStart(payload)) {
            await cancel('m.unknown_method');
            return;
          }
          startPayload = payload;
          setState(KeyVerificationState.askAccept);
        } else {
          Logs().i('handling start in method.....');
          await _method!.handlePayload(type, payload);
        }
        break;
      case EventTypes.KeyVerificationDone:
        if (state == KeyVerificationState.showQRSuccess) {
          await send(EventTypes.KeyVerificationDone, {});
          setState(KeyVerificationState.done);
        }
        break;
      case EventTypes.KeyVerificationCancel:
        canceled = true;
        canceledCode = payload['code'];
        canceledReason = payload['reason'];
        setState(KeyVerificationState.error);
        break;
      default:
        final method = _method;
        if (method != null) {
          await method.handlePayload(type, payload);
        } else {
          await cancel('m.invalid_message');
        }
        break;
    }
    if (lastStep == thisLastStep) {
      lastStep = type;
    }
  } catch (err, stacktrace) {
    Logs().e('[Key Verification] An error occured', err, stacktrace);
    await cancel('m.invalid_message');
  } finally {
    _handlePayloadLock = false;
  }
}