handlePayload method
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;
}
}