/*
* Famedly Matrix SDK
* Copyright (C) 2020, 2021 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:canonical_json/canonical_json.dart';
import 'package:crypto/crypto.dart' as crypto;
import 'package:typed_data/typed_data.dart';
import 'package:vodozemac/vodozemac.dart' as vod;
import 'package:matrix/encryption/encryption.dart';
import 'package:matrix/encryption/utils/base64_unpadded.dart';
import 'package:matrix/matrix.dart';
import 'package:matrix/src/utils/crypto/crypto.dart' as uc;
/*
+-------------+ +-----------+
| AliceDevice | | BobDevice |
+-------------+ +-----------+
| |
| (m.key.verification.request) |
|-------------------------------->| (ASK FOR VERIFICATION REQUEST)
| |
| (m.key.verification.ready) |
|<--------------------------------|
| |
| (m.key.verification.start) | we will probably not send this
|<--------------------------------| for simplicities sake
| |
| m.key.verification.start |
|-------------------------------->| (ASK FOR VERIFICATION REQUEST)
| |
| m.key.verification.accept |
|<--------------------------------|
| |
| m.key.verification.key |
|-------------------------------->|
| |
| m.key.verification.key |
|<--------------------------------|
| |
| COMPARE EMOJI / NUMBERS |
| |
| m.key.verification.mac |
|-------------------------------->| success
| |
| m.key.verification.mac |
success |<--------------------------------|
| |
*/
/// QR key verification
/// You create possible methods from `client.verificationMethods` on device A
/// and send a request using `request.start()` which calls `sendRequest()` your client
/// now is in `waitingAccept` state, where ideally your client would now show some
/// waiting indicator.
///
/// On device B you now get a `m.key.verification.request`, you check the
/// `methods` from the request payload and see if anything is possible.
/// If not you cancel the request. (should this be cancelled? couldn't another device handle this?)
/// you the set the state to `askAccept`.
///
/// Your client would now show a button to accept/decline the request.
/// The user can then use `acceptVerification()`to accept the verification which
/// then sends a `m.key.verification.ready`. This also calls `generateQrCode()`
/// in it which populates the `request.qrData` depending on the qr mode.
/// B now sets the state `askChoice`
///
/// On device A you now get the ready event, which setups the `possibleMethods`
/// and `qrData` on A's side. Similarly A now sets their state to `askChoice`
///
/// At his point both sides are on the `askChoice` state.
///
/// BACKWARDS COMPATIBILITY HACK:
/// To work well with sdks prior to QR verification (0.20.5 and older), start will
/// be sent with ready itself if only sas is supported. This avoids weird glare
/// issues faced with start from both sides if clients are not on the same sdk
/// version (0.20.5 vs next)
/// https://matrix.to/#/!KBwfdofYJUmnsVoqwn:famedly.de/$wlHXlLQJdfrqKAF5KkuQrXydwOhY_uyqfH4ReasZqnA?via=neko.dev&via=famedly.de&via=lihotzki.de
/// Here your clients would ideally show a list of the `possibleMethods` and the
/// user can choose one. For QR specifically, you can show the QR code on the
/// device which supports showing the qr code and the device which supports
/// scanning can scan this code.
///
/// Assuming device A scans device B's code, device A would now send a `m.key.verification.start`,
/// you do this using the `continueVerificatio()` method. You can pass
/// `m.reciprocate.v1` or `m.sas.v1` here, and also attach the qrData here.
/// This then calls `verifyQrData()` internally, which sets the `randomSharedSecretForQRCode`
/// to the one from the QR code. Device A is now set to `showQRSuccess` state and shows
/// a green sheild. (Maybe add a text below saying tell device B you scanned the
/// code successfully.)
///
/// (some keys magic happens here, check `verifyQrData()`, `verifyKeysQR()` to know more)
///
/// On device B you get the `m.key.verification.start` event. The secret sent in
/// the start request is then verified, device B is then set to the `confirmQRScan`
/// state. Your device should show a dialog to confirm from B that A's device shows
/// the green shield (is in the done state). Once B confirms this physically, you
/// call the `acceptQRScanConfirmation()` function, which then does some keys
/// magic and sets B's state to `done`.
///
/// A gets the `m.key.verification.done` messsage and sends a done back, both
/// users can now dismiss the verification dialog safely.
enum KeyVerificationState {
askChoice,
askAccept,
askSSSS,
waitingAccept,
askSas,
showQRSuccess, // scanner after QR scan was successfull
confirmQRScan, // shower after getting start
waitingSas,
done,
error
}
enum KeyVerificationMethod { emoji, numbers, qrShow, qrScan, reciprocate }
bool isQrSupported(List knownVerificationMethods, List possibleMethods) {
return knownVerificationMethods.contains(EventTypes.QRShow) &&
possibleMethods.contains(EventTypes.QRScan) ||
knownVerificationMethods.contains(EventTypes.QRScan) &&
possibleMethods.contains(EventTypes.QRShow);
}
List _intersect(List? a, List? b) =>
(b == null || a == null) ? [] : a.where(b.contains).toList();
List _calculatePossibleMethods(
List knownMethods,
List payloadMethods,
) {
final output = [];
final copyKnownMethods = List.from(knownMethods);
final copyPayloadMethods = List.from(payloadMethods);
copyKnownMethods
.removeWhere((element) => !copyPayloadMethods.contains(element));
// remove qr modes for now, check if they are possible and add later
copyKnownMethods.removeWhere((element) => element.startsWith('m.qr_code'));
output.addAll(copyKnownMethods);
if (isQrSupported(knownMethods, payloadMethods)) {
// scan/show combo found, add whichever is known to us to our possible methods.
if (payloadMethods.contains(EventTypes.QRScan) &&
knownMethods.contains(EventTypes.QRShow)) {
output.add(EventTypes.QRShow);
}
if (payloadMethods.contains(EventTypes.QRShow) &&
knownMethods.contains(EventTypes.QRScan)) {
output.add(EventTypes.QRScan);
}
} else {
output.remove(EventTypes.Reciprocate);
}
return output;
}
List _bytesToInt(Uint8List bytes, int totalBits) {
final ret = [];
var current = 0;
var numBits = 0;
for (final byte in bytes) {
for (final bit in [7, 6, 5, 4, 3, 2, 1, 0]) {
numBits++;
current |= ((byte >> bit) & 1) << (totalBits - numBits);
if (numBits >= totalBits) {
ret.add(current);
current = 0;
numBits = 0;
}
}
}
return ret;
}
_KeyVerificationMethod _makeVerificationMethod(
String type,
KeyVerification request,
) {
if (type == EventTypes.Sas) {
return _KeyVerificationMethodSas(request: request);
}
if (type == EventTypes.Reciprocate) {
return _KeyVerificationMethodQRReciprocate(request: request);
}
throw Exception('Unkown method type');
}
class KeyVerification {
String? transactionId;
final Encryption encryption;
Client get client => encryption.client;
final Room? room;
final String userId;
void Function()? onUpdate;
String? get deviceId => _deviceId;
String? _deviceId;
bool startedVerification = false;
_KeyVerificationMethod? _method;
List possibleMethods = [];
List oppositePossibleMethods = [];
Map? startPayload;
String? _nextAction;
List _verifiedDevices = [];
DateTime lastActivity;
String? lastStep;
KeyVerificationState state = KeyVerificationState.waitingAccept;
bool canceled = false;
String? canceledCode;
String? canceledReason;
bool get isDone =>
canceled ||
{KeyVerificationState.error, KeyVerificationState.done}.contains(state);
// qr stuff
QRCode? qrCode;
String? randomSharedSecretForQRCode;
SignableKey? keyToVerify;
KeyVerification({
required this.encryption,
this.room,
required this.userId,
String? deviceId,
this.onUpdate,
}) : _deviceId = deviceId,
lastActivity = DateTime.now();
void dispose() {
Logs().i('[Key Verification] disposing object...');
randomSharedSecretForQRCode = null;
_method?.dispose();
}
static String? getTransactionId(Map payload) {
return payload['transaction_id'] ??
(payload['m.relates_to'] is Map
? payload['m.relates_to']['event_id']
: null);
}
List get knownVerificationMethods {
final methods = {};
if (client.verificationMethods.contains(KeyVerificationMethod.numbers) ||
client.verificationMethods.contains(KeyVerificationMethod.emoji)) {
methods.add(EventTypes.Sas);
}
/// `qrCanWork` - qr cannot work if we are verifying another master key but our own is unverified
final qrCanWork = (userId == client.userID) ||
((client.userDeviceKeys[client.userID]?.masterKey?.verified ?? false));
if (client.verificationMethods.contains(KeyVerificationMethod.qrShow) &&
qrCanWork) {
methods.add(EventTypes.QRShow);
methods.add(EventTypes.Reciprocate);
}
if (client.verificationMethods.contains(KeyVerificationMethod.qrScan) &&
qrCanWork) {
methods.add(EventTypes.QRScan);
methods.add(EventTypes.Reciprocate);
}
return methods.toList();
}
/// Once you get a ready event, i.e both sides are in a `askChoice` state,
/// send either `m.reciprocate.v1` or `m.sas.v1` here. If you continue with
/// qr, send the qrData you just scanned
Future continueVerification(
String type, {
Uint8List? qrDataRawBytes,
}) async {
bool qrChecksOut = false;
if (possibleMethods.contains(type)) {
if (qrDataRawBytes != null) {
qrChecksOut = await verifyQrData(qrDataRawBytes);
// after this scanners state is done
}
if (type != EventTypes.Reciprocate || qrChecksOut) {
final method = _method = _makeVerificationMethod(type, this);
await method.sendStart();
if (type == EventTypes.Sas) {
setState(KeyVerificationState.waitingAccept);
}
} else if (type == EventTypes.Reciprocate && !qrChecksOut) {
Logs().e('[KeyVerification] qr did not check out');
await cancel('m.invalid_key');
}
} else {
Logs().e(
'[KeyVerification] tried to continue verification with a unknown method',
);
await cancel('m.unknown_method');
}
}
Future sendRequest() async {
await send(
EventTypes.KeyVerificationRequest,
{
'methods': knownVerificationMethods,
if (room == null) 'timestamp': DateTime.now().millisecondsSinceEpoch,
},
);
startedVerification = true;
setState(KeyVerificationState.waitingAccept);
lastActivity = DateTime.now();
}
Future start() async {
if (room == null) {
transactionId = client.generateUniqueTransactionId();
}
if (encryption.crossSigning.enabled &&
!(await encryption.crossSigning.isCached()) &&
!client.isUnknownSession) {
setState(KeyVerificationState.askSSSS);
_nextAction = 'request';
} else {
await sendRequest();
}
}
bool _handlePayloadLock = false;
QRMode getOurQRMode() {
QRMode mode = QRMode.verifyOtherUser;
if (client.userID == userId) {
if (client.encryption != null &&
client.encryption!.enabled &&
(client.userDeviceKeys[client.userID]?.masterKey?.directVerified ??
false)) {
mode = QRMode.verifySelfTrusted;
} else {
mode = QRMode.verifySelfUntrusted;
}
}
return mode;
}
Future handlePayload(
String type,
Map 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.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.from(
client.userDeviceKeys[userId]?.deviceKeys.values ??
Iterable.empty(),
);
devices.removeWhere(
(d) => {deviceId, client.deviceID}.contains(d.deviceId),
);
final cancelPayload = {
'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.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;
}
}
void otherDeviceAccepted() {
canceled = true;
canceledCode = 'm.accepted';
canceledReason = 'm.accepted';
setState(KeyVerificationState.error);
}
Future openSSSS({
String? passphrase,
String? recoveryKey,
String? keyOrPassphrase,
bool skip = false,
}) async {
Future next() async {
if (_nextAction == 'request') {
await sendRequest();
} else if (_nextAction == 'done') {
// and now let's sign them all in the background
unawaited(encryption.crossSigning.sign(_verifiedDevices));
setState(KeyVerificationState.done);
} else if (_nextAction == 'showQRSuccess') {
setState(KeyVerificationState.showQRSuccess);
}
}
if (skip) {
await next();
return;
}
final handle = encryption.ssss.open(EventTypes.CrossSigningUserSigning);
await handle.unlock(
passphrase: passphrase,
recoveryKey: recoveryKey,
keyOrPassphrase: keyOrPassphrase,
);
await handle.maybeCacheAll();
await next();
}
/// called when the user accepts an incoming verification
Future acceptVerification() async {
if (!(await verifyLastStep([
EventTypes.KeyVerificationRequest,
EventTypes.KeyVerificationStart,
]))) {
return;
}
setState(KeyVerificationState.waitingAccept);
if (lastStep == EventTypes.KeyVerificationRequest) {
final copyKnownVerificationMethods =
List.from(knownVerificationMethods);
// qr code only works when atleast one side has verified master key
if (userId == client.userID) {
if (!(client.userDeviceKeys[client.userID]?.deviceKeys[deviceId]
?.hasValidSignatureChain(verifiedByTheirMasterKey: true) ??
false) &&
!(client.userDeviceKeys[client.userID]?.masterKey?.verified ??
false)) {
copyKnownVerificationMethods
.removeWhere((element) => element.startsWith('m.qr_code'));
copyKnownVerificationMethods.remove(EventTypes.Reciprocate);
// we are removing stuff only using the old possibleMethods should be ok here.
final copyPossibleMethods = List.from(possibleMethods);
possibleMethods = _calculatePossibleMethods(
copyKnownVerificationMethods,
copyPossibleMethods,
);
}
}
// we need to send a ready event
await send(EventTypes.KeyVerificationReady, {
'methods': copyKnownVerificationMethods,
});
// setup QRData from incoming request (outgoing ready)
qrCode = await generateQrCode();
setState(KeyVerificationState.askChoice);
} else {
// we need to send an accept event
await _method!
.handlePayload(EventTypes.KeyVerificationStart, startPayload!);
}
}
/// called when the user rejects an incoming verification
Future rejectVerification() async {
if (isDone) {
return;
}
if (!(await verifyLastStep([
EventTypes.KeyVerificationRequest,
EventTypes.KeyVerificationStart,
]))) {
return;
}
await cancel('m.user');
}
/// call this to confirm that your other device has shown a shield and is in
/// `done` state.
Future acceptQRScanConfirmation() async {
if (_method is _KeyVerificationMethodQRReciprocate &&
state == KeyVerificationState.confirmQRScan) {
await (_method as _KeyVerificationMethodQRReciprocate)
.acceptQRScanConfirmation();
}
}
Future acceptSas() async {
if (_method is _KeyVerificationMethodSas) {
await (_method as _KeyVerificationMethodSas).acceptSas();
}
}
Future rejectSas() async {
if (_method is _KeyVerificationMethodSas) {
await (_method as _KeyVerificationMethodSas).rejectSas();
}
}
List get sasNumbers {
if (_method is _KeyVerificationMethodSas) {
return _bytesToInt((_method as _KeyVerificationMethodSas).makeSas(5), 13)
.map((n) => n + 1000)
.toList();
}
return [];
}
List get sasTypes {
if (_method is _KeyVerificationMethodSas) {
return (_method as _KeyVerificationMethodSas).authenticationTypes ?? [];
}
return [];
}
List get sasEmojis {
if (_method is _KeyVerificationMethodSas) {
final numbers =
_bytesToInt((_method as _KeyVerificationMethodSas).makeSas(6), 6);
return numbers.map((n) => KeyVerificationEmoji(n)).toList().sublist(0, 7);
}
return [];
}
Future maybeRequestSSSSSecrets([int i = 0]) async {
final requestInterval = [10, 60];
if ((!encryption.crossSigning.enabled ||
(encryption.crossSigning.enabled &&
(await encryption.crossSigning.isCached()))) &&
(!encryption.keyManager.enabled ||
(encryption.keyManager.enabled &&
(await encryption.keyManager.isCached())))) {
// no need to request cache, we already have it
return;
}
// ignore: unawaited_futures
encryption.ssss
.maybeRequestAll(_verifiedDevices.whereType().toList());
if (requestInterval.length <= i) {
return;
}
Timer(
Duration(seconds: requestInterval[i]),
() => maybeRequestSSSSSecrets(i + 1),
);
}
Future verifyKeysSAS(
Map keys,
Future Function(String, SignableKey) verifier,
) async {
_verifiedDevices = [];
final userDeviceKey = client.userDeviceKeys[userId];
if (userDeviceKey == null) {
await cancel('m.key_mismatch');
return;
}
for (final entry in keys.entries) {
final keyId = entry.key;
final verifyDeviceId = keyId.substring('ed25519:'.length);
final keyInfo = entry.value;
final key = userDeviceKey.getKey(verifyDeviceId);
if (key != null) {
if (!(await verifier(keyInfo, key))) {
await cancel('m.key_mismatch');
return;
}
_verifiedDevices.add(key);
}
}
// okay, we reached this far, so all the devices are verified!
var verifiedMasterKey = false;
final wasUnknownSession = client.isUnknownSession;
for (final key in _verifiedDevices) {
await key.setVerified(
true,
false,
); // we don't want to sign the keys juuuust yet
if (key is CrossSigningKey && key.usage.contains('master')) {
verifiedMasterKey = true;
}
}
if (verifiedMasterKey && userId == client.userID) {
// it was our own master key, let's request the cross signing keys
// we do it in the background, thus no await needed here
// ignore: unawaited_futures
maybeRequestSSSSSecrets();
}
await send(EventTypes.KeyVerificationDone, {});
var askingSSSS = false;
if (encryption.crossSigning.enabled &&
encryption.crossSigning.signable(_verifiedDevices)) {
// these keys can be signed! Let's do so
if (await encryption.crossSigning.isCached()) {
// we want to make sure the verification state is correct for the other party after this event is handled.
// Otherwise the verification dialog might be stuck in an unverified but done state for a bit.
await encryption.crossSigning.sign(_verifiedDevices);
} else if (!wasUnknownSession) {
askingSSSS = true;
}
}
if (askingSSSS) {
setState(KeyVerificationState.askSSSS);
_nextAction = 'done';
} else {
setState(KeyVerificationState.done);
}
}
/// shower is true only for reciprocated verifications (shower side)
Future verifyKeysQR(SignableKey key, {bool shower = true}) async {
var verifiedMasterKey = false;
final wasUnknownSession = client.isUnknownSession;
key.setDirectVerified(true);
if (key is CrossSigningKey && key.usage.contains('master')) {
verifiedMasterKey = true;
}
if (verifiedMasterKey && userId == client.userID) {
// it was our own master key, let's request the cross signing keys
// we do it in the background, thus no await needed here
// ignore: unawaited_futures
maybeRequestSSSSSecrets();
}
if (shower) {
await send(EventTypes.KeyVerificationDone, {});
}
final keyList = List.from([key]);
var askingSSSS = false;
if (encryption.crossSigning.enabled &&
encryption.crossSigning.signable(keyList)) {
// these keys can be signed! Let's do so
if (await encryption.crossSigning.isCached()) {
// we want to make sure the verification state is correct for the other party after this event is handled.
// Otherwise the verification dialog might be stuck in an unverified but done state for a bit.
await encryption.crossSigning.sign(keyList);
} else if (!wasUnknownSession) {
askingSSSS = true;
}
}
if (askingSSSS) {
// no need to worry about shower/scanner here because if scanner was
// verified, ssss is already
setState(KeyVerificationState.askSSSS);
if (shower) {
_nextAction = 'done';
} else {
_nextAction = 'showQRSuccess';
}
} else {
if (shower) {
setState(KeyVerificationState.done);
} else {
setState(KeyVerificationState.showQRSuccess);
}
}
}
Future verifyActivity() async {
if (lastActivity.add(Duration(minutes: 10)).isAfter(DateTime.now())) {
lastActivity = DateTime.now();
return true;
}
await cancel('m.timeout');
return false;
}
Future verifyLastStep(List checkLastStep) async {
if (!(await verifyActivity())) {
return false;
}
if (checkLastStep.contains(lastStep)) {
return true;
}
Logs().e(
'[KeyVerificaton] lastStep mismatch cancelling, expected from ${checkLastStep.toString()} was ${lastStep.toString()}',
);
await cancel('m.unexpected_message');
return false;
}
Future cancel([String code = 'm.unknown', bool quiet = false]) async {
if (!quiet && (deviceId != null || room != null)) {
await send(EventTypes.KeyVerificationCancel, {
'reason': code,
'code': code,
});
}
canceled = true;
canceledCode = code;
setState(KeyVerificationState.error);
}
void makePayload(Map payload) {
payload['from_device'] = client.deviceID;
if (transactionId != null) {
if (room != null) {
payload['m.relates_to'] = {
'rel_type': 'm.reference',
'event_id': transactionId,
};
} else {
payload['transaction_id'] = transactionId;
}
}
}
Future send(
String type,
Map payload,
) async {
makePayload(payload);
Logs().i('[Key Verification] Sending type $type: $payload');
if (room != null) {
Logs().i('[Key Verification] Sending to $userId in room ${room!.id}...');
if ({EventTypes.KeyVerificationRequest}.contains(type)) {
payload['msgtype'] = type;
payload['to'] = userId;
payload['body'] =
'Attempting verification request. ($type) Apparently your client doesn\'t support this';
type = EventTypes.Message;
}
final newTransactionId = await room!.sendEvent(payload, type: type);
if (transactionId == null) {
transactionId = newTransactionId;
encryption.keyVerificationManager.addRequest(this);
}
} else {
Logs().i('[Key Verification] Sending to $userId device $deviceId...');
if (deviceId == '*') {
if ({
EventTypes.KeyVerificationRequest,
EventTypes.KeyVerificationCancel,
}.contains(type)) {
final deviceKeys =
client.userDeviceKeys[userId]?.deviceKeys.values.where(
(deviceKey) => deviceKey.hasValidSignatureChain(
verifiedByTheirMasterKey: true,
),
);
if (deviceKeys != null) {
await client.sendToDeviceEncrypted(
deviceKeys.toList(),
type,
payload,
);
}
} else {
Logs().e(
'[Key Verification] Tried to broadcast and un-broadcastable type: $type',
);
}
} else {
if (client.userDeviceKeys[userId]?.deviceKeys[deviceId] != null) {
await client.sendToDeviceEncrypted(
[client.userDeviceKeys[userId]!.deviceKeys[deviceId]!],
type,
payload,
);
} else {
Logs().e('[Key Verification] Unknown device');
}
}
}
}
void setState(KeyVerificationState newState) {
if (state != KeyVerificationState.error) {
state = newState;
}
onUpdate?.call();
}
static const String prefix = 'MATRIX';
static const int version = 0x02;
Future verifyQrData(Uint8List qrDataRawBytes) async {
final data = qrDataRawBytes;
// hardcoded stuff + 2 keys + secret
if (data.length < 10 + 32 + 32 + 8 + utf8.encode(transactionId!).length) {
return false;
}
if (data[6] != version) return false;
final remoteQrMode =
QRMode.values.singleWhere((mode) => mode.code == data[7]);
if (ascii.decode(data.sublist(0, 6)) != prefix) return false;
if (data[6] != version) return false;
final tmpBuf = Uint8List.fromList([data[8], data[9]]);
final encodedTxnLen = ByteData.view(tmpBuf.buffer).getUint16(0);
if (utf8.decode(data.sublist(10, 10 + encodedTxnLen)) != transactionId) {
return false;
}
final keys = client.userDeviceKeys;
final ownKeys = keys[client.userID];
final otherUserKeys = keys[userId];
final ownMasterKey = ownKeys?.getCrossSigningKey('master');
final ownDeviceKey = ownKeys?.getKey(client.deviceID!);
final ownOtherDeviceKey = ownKeys?.getKey(deviceId!);
final otherUserMasterKey = otherUserKeys?.masterKey;
final secondKey = encodeBase64Unpadded(
data.sublist(10 + encodedTxnLen + 32, 10 + encodedTxnLen + 32 + 32),
);
final randomSharedSecret =
encodeBase64Unpadded(data.sublist(10 + encodedTxnLen + 32 + 32));
/// `request.randomSharedSecretForQRCode` is overwritten below to send with `sendStart`
if ({QRMode.verifyOtherUser, QRMode.verifySelfUntrusted}
.contains(remoteQrMode)) {
if (!(ownMasterKey?.verified ?? false)) {
Logs().e(
'[KeyVerification] verifyQrData because you were in mode 0/2 and had untrusted msk',
);
return false;
}
}
if (remoteQrMode == QRMode.verifyOtherUser &&
otherUserMasterKey != null &&
ownMasterKey != null) {
if (secondKey == ownMasterKey.ed25519Key) {
randomSharedSecretForQRCode = randomSharedSecret;
await verifyKeysQR(otherUserMasterKey, shower: false);
return true;
}
} else if (remoteQrMode == QRMode.verifySelfTrusted &&
ownMasterKey != null &&
ownDeviceKey != null) {
if (secondKey == ownDeviceKey.ed25519Key) {
randomSharedSecretForQRCode = randomSharedSecret;
await verifyKeysQR(ownMasterKey, shower: false);
return true;
}
} else if (remoteQrMode == QRMode.verifySelfUntrusted &&
ownOtherDeviceKey != null &&
ownMasterKey != null) {
if (secondKey == ownMasterKey.ed25519Key) {
randomSharedSecretForQRCode = randomSharedSecret;
await verifyKeysQR(ownOtherDeviceKey, shower: false);
return true;
}
}
return false;
}
Future<(String, String)?> getKeys(QRMode mode) async {
final keys = client.userDeviceKeys;
final ownKeys = keys[client.userID];
final otherUserKeys = keys[userId];
final ownDeviceKey = ownKeys?.getKey(client.deviceID!);
final ownMasterKey = ownKeys?.getCrossSigningKey('master');
final otherDeviceKey = otherUserKeys?.getKey(deviceId!);
final otherMasterKey = otherUserKeys?.getCrossSigningKey('master');
if (mode == QRMode.verifyOtherUser &&
ownMasterKey != null &&
otherMasterKey != null) {
// we already have this check when sending `knownVerificationMethods`, but
// just to be safe anyway
if (ownMasterKey.verified) {
return (ownMasterKey.ed25519Key!, otherMasterKey.ed25519Key!);
}
} else if (mode == QRMode.verifySelfTrusted &&
ownMasterKey != null &&
otherDeviceKey != null) {
if (ownMasterKey.verified) {
return (ownMasterKey.ed25519Key!, otherDeviceKey.ed25519Key!);
}
} else if (mode == QRMode.verifySelfUntrusted &&
ownMasterKey != null &&
ownDeviceKey != null) {
return (ownDeviceKey.ed25519Key!, ownMasterKey.ed25519Key!);
}
return null;
}
Future generateQrCode() async {
final data = Uint8Buffer();
// why 11? https://github.com/matrix-org/matrix-js-sdk/commit/275ea6aacbfc6623e7559a7649ca5cab207903d9
randomSharedSecretForQRCode =
encodeBase64Unpadded(uc.secureRandomBytes(11));
final mode = getOurQRMode();
data.addAll(ascii.encode(prefix));
data.add(version);
data.add(mode.code);
final encodedTxnId = utf8.encode(transactionId!);
final txnIdLen = encodedTxnId.length;
final tmpBuf = Uint8List(2);
ByteData.view(tmpBuf.buffer).setUint16(0, txnIdLen);
data.addAll(tmpBuf);
data.addAll(encodedTxnId);
final keys = await getKeys(mode);
if (keys != null) {
data.addAll(base64decodeUnpadded(keys.$1));
data.addAll(base64decodeUnpadded(keys.$2));
} else {
return null;
}
data.addAll(base64decodeUnpadded(randomSharedSecretForQRCode!));
return QRCode(randomSharedSecretForQRCode!, data);
}
}
abstract class _KeyVerificationMethod {
KeyVerification request;
Encryption get encryption => request.encryption;
Client get client => request.client;
_KeyVerificationMethod({required this.request});
Future handlePayload(String type, Map payload);
bool validateStart(Map payload) {
return false;
}
late String _type;
String get type => _type;
Future sendStart();
void dispose() {}
}
class _KeyVerificationMethodQRReciprocate extends _KeyVerificationMethod {
_KeyVerificationMethodQRReciprocate({required super.request});
@override
// ignore: overridden_fields
final _type = EventTypes.Reciprocate;
@override
bool validateStart(Map payload) {
if (payload['method'] != type) return false;
if (payload['secret'] != request.randomSharedSecretForQRCode) return false;
return true;
}
@override
Future handlePayload(String type, Map payload) async {
try {
switch (type) {
case EventTypes.KeyVerificationStart:
if (!(await request.verifyLastStep([
EventTypes.KeyVerificationReady,
EventTypes.KeyVerificationRequest,
]))) {
return; // abort
}
if (!validateStart(payload)) {
await request.cancel('m.invalid_message');
return;
}
request.setState(KeyVerificationState.confirmQRScan);
break;
}
} catch (e, s) {
Logs().e('[Key Verification Reciprocate] An error occured', e, s);
if (request.deviceId != null) {
await request.cancel('m.invalid_message');
}
}
}
Future acceptQRScanConfirmation() async {
// secret validation already done in validateStart
final ourQRMode = request.getOurQRMode();
SignableKey? keyToVerify;
if (ourQRMode == QRMode.verifyOtherUser) {
keyToVerify = client.userDeviceKeys[request.userId]?.masterKey;
} else if (ourQRMode == QRMode.verifySelfTrusted) {
keyToVerify =
client.userDeviceKeys[client.userID]?.deviceKeys[request.deviceId];
} else if (ourQRMode == QRMode.verifySelfUntrusted) {
keyToVerify = client.userDeviceKeys[client.userID]?.masterKey;
}
if (keyToVerify != null) {
await request.verifyKeysQR(keyToVerify, shower: true);
} else {
Logs().e('[KeyVerification], verifying keys failed');
await request.cancel('m.invalid_key');
}
}
@override
Future sendStart() async {
final payload = {
'method': type,
'secret': request.randomSharedSecretForQRCode,
};
request.makePayload(payload);
await request.send(EventTypes.KeyVerificationStart, payload);
}
@override
void dispose() {}
}
enum QRMode {
verifyOtherUser(0x00),
verifySelfTrusted(0x01),
verifySelfUntrusted(0x02);
const QRMode(this.code);
final int code;
}
class QRCode {
/// You actually never need this when implementing in a client, its just to
/// make tests easier. Just pass `qrDataRawBytes` in `continueVerifcation()`
final String randomSharedSecret;
final Uint8Buffer qrDataRawBytes;
QRCode(this.randomSharedSecret, this.qrDataRawBytes);
}
const knownKeyAgreementProtocols = ['curve25519-hkdf-sha256', 'curve25519'];
const knownHashes = ['sha256'];
const knownHashesAuthentificationCodes = [
'hkdf-hmac-sha256.v2',
'hkdf-hmac-sha256',
];
class _KeyVerificationMethodSas extends _KeyVerificationMethod {
_KeyVerificationMethodSas({required super.request});
@override
// ignore: overridden_fields
final _type = EventTypes.Sas;
String? keyAgreementProtocol;
String? hash;
String? messageAuthenticationCode;
List? authenticationTypes;
late String startCanonicalJson;
String? commitment;
late String theirPublicKey;
Map? macPayload;
vod.Sas? sas;
vod.EstablishedSas? establishedSas;
List get knownAuthentificationTypes {
final types = [];
if (request.client.verificationMethods
.contains(KeyVerificationMethod.emoji)) {
types.add('emoji');
}
if (request.client.verificationMethods
.contains(KeyVerificationMethod.numbers)) {
types.add('decimal');
}
return types;
}
@override
Future handlePayload(String type, Map payload) async {
try {
switch (type) {
case EventTypes.KeyVerificationStart:
if (!(await request.verifyLastStep([
EventTypes.KeyVerificationReady,
EventTypes.KeyVerificationRequest,
EventTypes.KeyVerificationStart,
]))) {
return; // abort
}
if (!validateStart(payload)) {
await request.cancel('m.unknown_method');
return;
}
await _sendAccept();
break;
case EventTypes.KeyVerificationAccept:
if (!(await request.verifyLastStep([
EventTypes.KeyVerificationReady,
EventTypes.KeyVerificationRequest,
]))) {
return;
}
if (!_handleAccept(payload)) {
await request.cancel('m.unknown_method');
return;
}
await _sendKey();
break;
case 'm.key.verification.key':
if (!(await request.verifyLastStep([
EventTypes.KeyVerificationAccept,
EventTypes.KeyVerificationStart,
]))) {
return;
}
_handleKey(payload);
if (request.lastStep == EventTypes.KeyVerificationStart) {
// we need to send our key
await _sendKey();
} else {
// we already sent our key, time to verify the commitment being valid
if (await _validateCommitment() == false) {
await request.cancel('m.mismatched_commitment');
return;
}
}
request.setState(KeyVerificationState.askSas);
break;
case 'm.key.verification.mac':
if (!(await request.verifyLastStep(['m.key.verification.key']))) {
return;
}
macPayload = payload;
if (request.state == KeyVerificationState.waitingSas) {
await _processMac();
}
break;
}
} catch (err, stacktrace) {
Logs().e('[Key Verification SAS] An error occured', err, stacktrace);
if (request.deviceId != null) {
await request.cancel('m.invalid_message');
}
}
}
Future acceptSas() async {
await _sendMac();
request.setState(KeyVerificationState.waitingSas);
if (macPayload != null) {
await _processMac();
}
}
Future rejectSas() async {
await request.cancel('m.mismatched_sas');
}
@override
Future sendStart() async {
final payload = {
'method': type,
'key_agreement_protocols': knownKeyAgreementProtocols,
'hashes': knownHashes,
'message_authentication_codes': knownHashesAuthentificationCodes,
'short_authentication_string': knownAuthentificationTypes,
};
request.makePayload(payload);
// We just store the canonical json in here for later verification
startCanonicalJson = String.fromCharCodes(canonicalJson.encode(payload));
await request.send(EventTypes.KeyVerificationStart, payload);
}
@override
bool validateStart(Map payload) {
if (payload['method'] != type) {
return false;
}
final possibleKeyAgreementProtocols = _intersect(
knownKeyAgreementProtocols,
payload['key_agreement_protocols'],
);
if (possibleKeyAgreementProtocols.isEmpty) {
return false;
}
keyAgreementProtocol = possibleKeyAgreementProtocols.first;
final possibleHashes = _intersect(knownHashes, payload['hashes']);
if (possibleHashes.isEmpty) {
return false;
}
hash = possibleHashes.first;
final possibleMessageAuthenticationCodes = _intersect(
knownHashesAuthentificationCodes,
payload['message_authentication_codes'],
);
if (possibleMessageAuthenticationCodes.isEmpty) {
return false;
}
// intersect should make sure we choose v2 over the dep'd one
messageAuthenticationCode = possibleMessageAuthenticationCodes.first;
final possibleAuthenticationTypes = _intersect(
knownAuthentificationTypes,
payload['short_authentication_string'],
);
if (possibleAuthenticationTypes.isEmpty) {
return false;
}
authenticationTypes = possibleAuthenticationTypes;
startCanonicalJson = String.fromCharCodes(canonicalJson.encode(payload));
return true;
}
Future _sendAccept() async {
final sas = this.sas = vod.Sas();
commitment = await _makeCommitment(sas.publicKey, startCanonicalJson);
await request.send(EventTypes.KeyVerificationAccept, {
'method': type,
'key_agreement_protocol': keyAgreementProtocol,
'hash': hash,
'message_authentication_code': messageAuthenticationCode,
'short_authentication_string': authenticationTypes,
'commitment': commitment,
});
}
bool _handleAccept(Map payload) {
if (!knownKeyAgreementProtocols
.contains(payload['key_agreement_protocol'])) {
return false;
}
keyAgreementProtocol = payload['key_agreement_protocol'];
if (!knownHashes.contains(payload['hash'])) {
return false;
}
hash = payload['hash'];
if (!knownHashesAuthentificationCodes
.contains(payload['message_authentication_code'])) {
return false;
}
messageAuthenticationCode = payload['message_authentication_code'];
final possibleAuthenticationTypes = _intersect(
knownAuthentificationTypes,
payload['short_authentication_string'],
);
if (possibleAuthenticationTypes.isEmpty) {
return false;
}
authenticationTypes = possibleAuthenticationTypes;
commitment = payload['commitment'];
sas = vod.Sas();
return true;
}
Future _sendKey() async {
await request.send('m.key.verification.key', {
'key': sas!.publicKey,
});
}
void _handleKey(Map payload) {
theirPublicKey = payload['key'];
establishedSas = sas!.establishSasSecret(payload['key']);
}
Future _validateCommitment() async {
final checkCommitment =
await _makeCommitment(theirPublicKey, startCanonicalJson);
return commitment == checkCommitment;
}
Uint8List makeSas(int bytes) {
var sasInfo = '';
if (keyAgreementProtocol == 'curve25519-hkdf-sha256') {
final ourInfo = '${client.userID}|${client.deviceID}|${sas!.publicKey}|';
final theirInfo =
'${request.userId}|${request.deviceId}|$theirPublicKey|';
sasInfo =
'MATRIX_KEY_VERIFICATION_SAS|${request.startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo}${request.transactionId!}';
} else if (keyAgreementProtocol == 'curve25519') {
final ourInfo = client.userID! + client.deviceID!;
final theirInfo = request.userId + request.deviceId!;
sasInfo =
'MATRIX_KEY_VERIFICATION_SAS${request.startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo}${request.transactionId!}';
} else {
throw Exception('Unknown key agreement protocol');
}
return establishedSas!.generateBytes(sasInfo, bytes);
}
Future _sendMac() async {
final baseInfo =
'MATRIX_KEY_VERIFICATION_MAC${client.userID!}${client.deviceID!}${request.userId}${request.deviceId!}${request.transactionId!}';
final mac = {};
final keyList = [];
// now add all the keys we want the other to verify
// for now it is just our device key, once we have cross-signing
// we would also add the cross signing key here
final deviceKeyId = 'ed25519:${client.deviceID}';
mac[deviceKeyId] =
_calculateMac(encryption.fingerprintKey!, baseInfo + deviceKeyId);
keyList.add(deviceKeyId);
final masterKey = client.userDeviceKeys[client.userID]?.masterKey;
if (masterKey != null && masterKey.verified) {
// we have our own master key verified, let's send it!
final masterKeyId = 'ed25519:${masterKey.publicKey}';
mac[masterKeyId] =
_calculateMac(masterKey.publicKey!, baseInfo + masterKeyId);
keyList.add(masterKeyId);
}
keyList.sort();
final keys = _calculateMac(keyList.join(','), '${baseInfo}KEY_IDS');
await request.send('m.key.verification.mac', {
'mac': mac,
'keys': keys,
});
}
Future _processMac() async {
final payload = macPayload!;
final baseInfo =
'MATRIX_KEY_VERIFICATION_MAC${request.userId}${request.deviceId!}${client.userID!}${client.deviceID!}${request.transactionId!}';
final keyList = payload['mac'].keys.toList();
keyList.sort();
if (payload['keys'] !=
_calculateMac(keyList.join(','), '${baseInfo}KEY_IDS')) {
await request.cancel('m.key_mismatch');
return;
}
if (!client.userDeviceKeys.containsKey(request.userId)) {
await request.cancel('m.key_mismatch');
return;
}
final mac = {};
for (final entry in payload['mac'].entries) {
if (entry.value is String) {
mac[entry.key] = entry.value;
}
}
await request.verifyKeysSAS(mac, (String mac, SignableKey key) async {
return mac ==
_calculateMac(
key.ed25519Key!,
'${baseInfo}ed25519:${key.identifier!}',
);
});
}
Future _makeCommitment(String pubKey, String canonicalJson) async {
if (hash == 'sha256') {
final bytes = utf8.encode(pubKey + canonicalJson);
final digest = crypto.sha256.convert(bytes);
return base64.encode(digest.bytes);
}
throw Exception('Unknown hash method');
}
String _calculateMac(String input, String info) {
if (messageAuthenticationCode == 'hkdf-hmac-sha256.v2') {
return establishedSas!.calculateMac(input, info);
} else if (messageAuthenticationCode == 'hkdf-hmac-sha256') {
return establishedSas!.calculateMacDeprecated(input, info);
} else {
throw Exception('Unknown message authentification code');
}
}
}
const _emojiMap = [
{
'emoji': '\u{1F436}',
'name': 'Dog',
},
{
'emoji': '\u{1F431}',
'name': 'Cat',
},
{
'emoji': '\u{1F981}',
'name': 'Lion',
},
{
'emoji': '\u{1F40E}',
'name': 'Horse',
},
{
'emoji': '\u{1F984}',
'name': 'Unicorn',
},
{
'emoji': '\u{1F437}',
'name': 'Pig',
},
{
'emoji': '\u{1F418}',
'name': 'Elephant',
},
{
'emoji': '\u{1F430}',
'name': 'Rabbit',
},
{
'emoji': '\u{1F43C}',
'name': 'Panda',
},
{
'emoji': '\u{1F413}',
'name': 'Rooster',
},
{
'emoji': '\u{1F427}',
'name': 'Penguin',
},
{
'emoji': '\u{1F422}',
'name': 'Turtle',
},
{
'emoji': '\u{1F41F}',
'name': 'Fish',
},
{
'emoji': '\u{1F419}',
'name': 'Octopus',
},
{
'emoji': '\u{1F98B}',
'name': 'Butterfly',
},
{
'emoji': '\u{1F337}',
'name': 'Flower',
},
{
'emoji': '\u{1F333}',
'name': 'Tree',
},
{
'emoji': '\u{1F335}',
'name': 'Cactus',
},
{
'emoji': '\u{1F344}',
'name': 'Mushroom',
},
{
'emoji': '\u{1F30F}',
'name': 'Globe',
},
{
'emoji': '\u{1F319}',
'name': 'Moon',
},
{
'emoji': '\u{2601}\u{FE0F}',
'name': 'Cloud',
},
{
'emoji': '\u{1F525}',
'name': 'Fire',
},
{
'emoji': '\u{1F34C}',
'name': 'Banana',
},
{
'emoji': '\u{1F34E}',
'name': 'Apple',
},
{
'emoji': '\u{1F353}',
'name': 'Strawberry',
},
{
'emoji': '\u{1F33D}',
'name': 'Corn',
},
{
'emoji': '\u{1F355}',
'name': 'Pizza',
},
{
'emoji': '\u{1F382}',
'name': 'Cake',
},
{
'emoji': '\u{2764}\u{FE0F}',
'name': 'Heart',
},
{
'emoji': '\u{1F600}',
'name': 'Smiley',
},
{
'emoji': '\u{1F916}',
'name': 'Robot',
},
{
'emoji': '\u{1F3A9}',
'name': 'Hat',
},
{
'emoji': '\u{1F453}',
'name': 'Glasses',
},
{
'emoji': '\u{1F527}',
'name': 'Spanner',
},
{
'emoji': '\u{1F385}',
'name': 'Santa',
},
{
'emoji': '\u{1F44D}',
'name': 'Thumbs Up',
},
{
'emoji': '\u{2602}\u{FE0F}',
'name': 'Umbrella',
},
{
'emoji': '\u{231B}',
'name': 'Hourglass',
},
{
'emoji': '\u{23F0}',
'name': 'Clock',
},
{
'emoji': '\u{1F381}',
'name': 'Gift',
},
{
'emoji': '\u{1F4A1}',
'name': 'Light Bulb',
},
{
'emoji': '\u{1F4D5}',
'name': 'Book',
},
{
'emoji': '\u{270F}\u{FE0F}',
'name': 'Pencil',
},
{
'emoji': '\u{1F4CE}',
'name': 'Paperclip',
},
{
'emoji': '\u{2702}\u{FE0F}',
'name': 'Scissors',
},
{
'emoji': '\u{1F512}',
'name': 'Lock',
},
{
'emoji': '\u{1F511}',
'name': 'Key',
},
{
'emoji': '\u{1F528}',
'name': 'Hammer',
},
{
'emoji': '\u{260E}\u{FE0F}',
'name': 'Telephone',
},
{
'emoji': '\u{1F3C1}',
'name': 'Flag',
},
{
'emoji': '\u{1F682}',
'name': 'Train',
},
{
'emoji': '\u{1F6B2}',
'name': 'Bicycle',
},
{
'emoji': '\u{2708}\u{FE0F}',
'name': 'Aeroplane',
},
{
'emoji': '\u{1F680}',
'name': 'Rocket',
},
{
'emoji': '\u{1F3C6}',
'name': 'Trophy',
},
{
'emoji': '\u{26BD}',
'name': 'Ball',
},
{
'emoji': '\u{1F3B8}',
'name': 'Guitar',
},
{
'emoji': '\u{1F3BA}',
'name': 'Trumpet',
},
{
'emoji': '\u{1F514}',
'name': 'Bell',
},
{
'emoji': '\u{2693}',
'name': 'Anchor',
},
{
'emoji': '\u{1F3A7}',
'name': 'Headphones',
},
{
'emoji': '\u{1F4C1}',
'name': 'Folder',
},
{
'emoji': '\u{1F4CC}',
'name': 'Pin',
},
];
class KeyVerificationEmoji {
final int number;
KeyVerificationEmoji(this.number);
String get emoji => _emojiMap[number]['emoji'] ?? '';
String get name => _emojiMap[number]['name'] ?? '';
}