/*
 *   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:olm/olm.dart' as olm;
import 'package:typed_data/typed_data.dart';
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);
  String? chosenMethod;
  // 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)
            ? true
            : false)
        : true;
    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 {
    chosenMethod = type;
    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 'm.key.verification.ready':
          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,
            'm.key.verification.ready',
          ]))) {
            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('m.key.verification.ready', {
        '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) {
          Logs().e('[Key Verification] Unknown device');
        }
        await client.sendToDeviceEncrypted(
            [client.userDeviceKeys[userId]!.deviceKeys[deviceId]!],
            type,
            payload);
      }
    }
  }
  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([
            'm.key.verification.ready',
            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'];
class _KeyVerificationMethodSas extends _KeyVerificationMethod {
  _KeyVerificationMethodSas({required KeyVerification request})
      : super(request: 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;
  olm.SAS? sas;
  @override
  void dispose() {
    sas?.free();
  }
  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([
            'm.key.verification.ready',
            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([
            'm.key.verification.ready',
            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 (!_validateCommitment()) {
              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;
    }
    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 = olm.SAS();
    commitment = _makeCommitment(sas.get_pubkey(), 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 = olm.SAS();
    return true;
  }
  Future _sendKey() async {
    await request.send('m.key.verification.key', {
      'key': sas!.get_pubkey(),
    });
  }
  void _handleKey(Map payload) {
    theirPublicKey = payload['key'];
    sas!.set_their_key(payload['key']);
  }
  bool _validateCommitment() {
    final checkCommitment = _makeCommitment(theirPublicKey, startCanonicalJson);
    return commitment == checkCommitment;
  }
  Uint8List makeSas(int bytes) {
    var sasInfo = '';
    if (keyAgreementProtocol == 'curve25519-hkdf-sha256') {
      final ourInfo =
          '${client.userID}|${client.deviceID}|${sas!.get_pubkey()}|';
      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 sas!.generate_bytes(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!}');
    });
  }
  String _makeCommitment(String pubKey, String canonicalJson) {
    if (hash == 'sha256') {
      final olmutil = olm.Utility();
      final ret = olmutil.sha256(pubKey + canonicalJson);
      olmutil.free();
      return ret;
    }
    throw Exception('Unknown hash method');
  }
  String _calculateMac(String input, String info) {
    if (messageAuthenticationCode == 'hkdf-hmac-sha256') {
      return sas!.calculate_mac(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'] ?? '';
}