LCOV - code coverage report
Current view: top level - lib/encryption/utils - key_verification.dart (source / functions) Coverage Total Hit
Test: merged.info Lines: 86.7 % 664 576
Test Date: 2025-10-13 02:23:18 Functions: - 0 0

            Line data    Source code
       1              : /*
       2              :  *   Famedly Matrix SDK
       3              :  *   Copyright (C) 2020, 2021 Famedly GmbH
       4              :  *
       5              :  *   This program is free software: you can redistribute it and/or modify
       6              :  *   it under the terms of the GNU Affero General Public License as
       7              :  *   published by the Free Software Foundation, either version 3 of the
       8              :  *   License, or (at your option) any later version.
       9              :  *
      10              :  *   This program is distributed in the hope that it will be useful,
      11              :  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
      12              :  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      13              :  *   GNU Affero General Public License for more details.
      14              :  *
      15              :  *   You should have received a copy of the GNU Affero General Public License
      16              :  *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
      17              :  */
      18              : 
      19              : import 'dart:async';
      20              : import 'dart:convert';
      21              : import 'dart:typed_data';
      22              : 
      23              : import 'package:canonical_json/canonical_json.dart';
      24              : import 'package:typed_data/typed_data.dart';
      25              : import 'package:vodozemac/vodozemac.dart' as vod;
      26              : 
      27              : import 'package:matrix/encryption/encryption.dart';
      28              : import 'package:matrix/encryption/utils/base64_unpadded.dart';
      29              : import 'package:matrix/matrix.dart';
      30              : import 'package:matrix/src/utils/crypto/crypto.dart' as uc;
      31              : 
      32              : /*
      33              :     +-------------+                    +-----------+
      34              :     | AliceDevice |                    | BobDevice |
      35              :     +-------------+                    +-----------+
      36              :           |                                 |
      37              :           | (m.key.verification.request)    |
      38              :           |-------------------------------->| (ASK FOR VERIFICATION REQUEST)
      39              :           |                                 |
      40              :           |      (m.key.verification.ready) |
      41              :           |<--------------------------------|
      42              :           |                                 |
      43              :           |      (m.key.verification.start) | we will probably not send this
      44              :           |<--------------------------------| for simplicities sake
      45              :           |                                 |
      46              :           | m.key.verification.start        |
      47              :           |-------------------------------->| (ASK FOR VERIFICATION REQUEST)
      48              :           |                                 |
      49              :           |       m.key.verification.accept |
      50              :           |<--------------------------------|
      51              :           |                                 |
      52              :           | m.key.verification.key          |
      53              :           |-------------------------------->|
      54              :           |                                 |
      55              :           |          m.key.verification.key |
      56              :           |<--------------------------------|
      57              :           |                                 |
      58              :           |     COMPARE EMOJI / NUMBERS     |
      59              :           |                                 |
      60              :           | m.key.verification.mac          |
      61              :           |-------------------------------->|  success
      62              :           |                                 |
      63              :           |          m.key.verification.mac |
      64              :  success  |<--------------------------------|
      65              :           |                                 |
      66              : */
      67              : 
      68              : /// QR key verification
      69              : /// You create possible methods from `client.verificationMethods` on device A
      70              : /// and send a request using `request.start()` which calls `sendRequest()` your client
      71              : /// now is in `waitingAccept` state, where ideally your client would now show some
      72              : /// waiting indicator.
      73              : ///
      74              : /// On device B you now get a `m.key.verification.request`, you check the
      75              : /// `methods` from the request payload and see if anything is possible.
      76              : /// If not you cancel the request. (should this be cancelled? couldn't another device handle this?)
      77              : /// you the set the state to `askAccept`.
      78              : ///
      79              : /// Your client would now show a button to accept/decline the request.
      80              : /// The user can then use `acceptVerification()`to accept the verification which
      81              : /// then sends a `m.key.verification.ready`. This also calls `generateQrCode()`
      82              : /// in it which populates the `request.qrData` depending on the qr mode.
      83              : /// B now sets the state `askChoice`
      84              : ///
      85              : /// On device A you now get the ready event, which setups the `possibleMethods`
      86              : /// and `qrData` on A's side. Similarly A now sets their state to `askChoice`
      87              : ///
      88              : /// At his point both sides are on the `askChoice` state.
      89              : ///
      90              : /// BACKWARDS COMPATIBILITY HACK:
      91              : /// To work well with sdks prior to QR verification (0.20.5 and older), start will
      92              : /// be sent with ready itself if only sas is supported. This avoids weird glare
      93              : /// issues faced with start from both sides if clients are not on the same sdk
      94              : /// version (0.20.5 vs next)
      95              : /// https://matrix.to/#/!KBwfdofYJUmnsVoqwn:famedly.de/$wlHXlLQJdfrqKAF5KkuQrXydwOhY_uyqfH4ReasZqnA?via=neko.dev&via=famedly.de&via=lihotzki.de
      96              : 
      97              : /// Here your clients would ideally show a list of the `possibleMethods` and the
      98              : /// user can choose one. For QR specifically, you can show the QR code on the
      99              : /// device which supports showing the qr code and the device which supports
     100              : /// scanning can scan this code.
     101              : ///
     102              : /// Assuming device A scans device B's code, device A would now send a `m.key.verification.start`,
     103              : /// you do this using the `continueVerificatio()` method. You can pass
     104              : /// `m.reciprocate.v1` or `m.sas.v1` here, and also attach the qrData here.
     105              : /// This then calls `verifyQrData()` internally, which sets the `randomSharedSecretForQRCode`
     106              : /// to the one from the QR code. Device A is now set to `showQRSuccess` state and shows
     107              : /// a green sheild. (Maybe add a text below saying tell device B you scanned the
     108              : /// code successfully.)
     109              : ///
     110              : /// (some keys magic happens here, check `verifyQrData()`, `verifyKeysQR()` to know more)
     111              : ///
     112              : /// On device B you get the `m.key.verification.start` event. The secret sent in
     113              : /// the start request is then verified, device B is then set to the `confirmQRScan`
     114              : /// state. Your device should show a dialog to confirm from B that A's device shows
     115              : /// the green shield (is in the done state). Once B confirms this physically, you
     116              : /// call the `acceptQRScanConfirmation()` function, which then does some keys
     117              : /// magic and sets B's state to `done`.
     118              : ///
     119              : /// A gets the `m.key.verification.done` messsage and sends a done back, both
     120              : /// users can now dismiss the verification dialog safely.
     121              : 
     122              : enum KeyVerificationState {
     123              :   askChoice,
     124              :   askAccept,
     125              :   askSSSS,
     126              :   waitingAccept,
     127              :   askSas,
     128              :   showQRSuccess, // scanner after QR scan was successfull
     129              :   confirmQRScan, // shower after getting start
     130              :   waitingSas,
     131              :   done,
     132              :   error
     133              : }
     134              : 
     135              : enum KeyVerificationMethod { emoji, numbers, qrShow, qrScan, reciprocate }
     136              : 
     137            2 : bool isQrSupported(List knownVerificationMethods, List possibleMethods) {
     138            2 :   return knownVerificationMethods.contains(EventTypes.QRShow) &&
     139            2 :           possibleMethods.contains(EventTypes.QRScan) ||
     140            2 :       knownVerificationMethods.contains(EventTypes.QRScan) &&
     141            2 :           possibleMethods.contains(EventTypes.QRShow);
     142              : }
     143              : 
     144            1 : List<String> _intersect(List<String>? a, List<dynamic>? b) =>
     145            3 :     (b == null || a == null) ? [] : a.where(b.contains).toList();
     146              : 
     147            2 : List<String> _calculatePossibleMethods(
     148              :   List<String> knownMethods,
     149              :   List<dynamic> payloadMethods,
     150              : ) {
     151            2 :   final output = <String>[];
     152            2 :   final copyKnownMethods = List<String>.from(knownMethods);
     153            2 :   final copyPayloadMethods = List.from(payloadMethods);
     154              : 
     155              :   copyKnownMethods
     156            6 :       .removeWhere((element) => !copyPayloadMethods.contains(element));
     157              : 
     158              :   // remove qr modes for now, check if they are possible and add later
     159            6 :   copyKnownMethods.removeWhere((element) => element.startsWith('m.qr_code'));
     160            2 :   output.addAll(copyKnownMethods);
     161              : 
     162            2 :   if (isQrSupported(knownMethods, payloadMethods)) {
     163              :     // scan/show combo found, add whichever is known to us to our possible methods.
     164            2 :     if (payloadMethods.contains(EventTypes.QRScan) &&
     165            2 :         knownMethods.contains(EventTypes.QRShow)) {
     166            2 :       output.add(EventTypes.QRShow);
     167              :     }
     168            2 :     if (payloadMethods.contains(EventTypes.QRShow) &&
     169            2 :         knownMethods.contains(EventTypes.QRScan)) {
     170            2 :       output.add(EventTypes.QRScan);
     171              :     }
     172              :   } else {
     173            2 :     output.remove(EventTypes.Reciprocate);
     174              :   }
     175              : 
     176              :   return output;
     177              : }
     178              : 
     179            1 : List<int> _bytesToInt(Uint8List bytes, int totalBits) {
     180            1 :   final ret = <int>[];
     181              :   var current = 0;
     182              :   var numBits = 0;
     183            2 :   for (final byte in bytes) {
     184            2 :     for (final bit in [7, 6, 5, 4, 3, 2, 1, 0]) {
     185            1 :       numBits++;
     186            5 :       current |= ((byte >> bit) & 1) << (totalBits - numBits);
     187            1 :       if (numBits >= totalBits) {
     188            1 :         ret.add(current);
     189              :         current = 0;
     190              :         numBits = 0;
     191              :       }
     192              :     }
     193              :   }
     194              :   return ret;
     195              : }
     196              : 
     197            2 : _KeyVerificationMethod _makeVerificationMethod(
     198              :   String type,
     199              :   KeyVerification request,
     200              : ) {
     201            2 :   if (type == EventTypes.Sas) {
     202            2 :     return _KeyVerificationMethodSas(request: request);
     203              :   }
     204            2 :   if (type == EventTypes.Reciprocate) {
     205            2 :     return _KeyVerificationMethodQRReciprocate(request: request);
     206              :   }
     207            0 :   throw Exception('Unkown method type');
     208              : }
     209              : 
     210              : class KeyVerification {
     211              :   String? transactionId;
     212              :   final Encryption encryption;
     213            9 :   Client get client => encryption.client;
     214              :   final Room? room;
     215              :   final String userId;
     216              :   void Function()? onUpdate;
     217            6 :   String? get deviceId => _deviceId;
     218              :   String? _deviceId;
     219              :   bool startedVerification = false;
     220              :   _KeyVerificationMethod? _method;
     221              : 
     222              :   List<String> possibleMethods = [];
     223              :   List<String> oppositePossibleMethods = [];
     224              : 
     225              :   Map<String, dynamic>? startPayload;
     226              :   String? _nextAction;
     227              :   List<SignableKey> _verifiedDevices = [];
     228              : 
     229              :   DateTime lastActivity;
     230              :   String? lastStep;
     231              : 
     232              :   KeyVerificationState state = KeyVerificationState.waitingAccept;
     233              :   bool canceled = false;
     234              :   String? canceledCode;
     235              :   String? canceledReason;
     236            2 :   bool get isDone =>
     237            2 :       canceled ||
     238            8 :       {KeyVerificationState.error, KeyVerificationState.done}.contains(state);
     239              : 
     240              :   // qr stuff
     241              :   QRCode? qrCode;
     242              :   String? randomSharedSecretForQRCode;
     243              :   SignableKey? keyToVerify;
     244            3 :   KeyVerification({
     245              :     required this.encryption,
     246              :     this.room,
     247              :     required this.userId,
     248              :     String? deviceId,
     249              :     this.onUpdate,
     250              :   })  : _deviceId = deviceId,
     251            3 :         lastActivity = DateTime.now();
     252              : 
     253            3 :   void dispose() {
     254            6 :     Logs().i('[Key Verification] disposing object...');
     255            3 :     randomSharedSecretForQRCode = null;
     256            5 :     _method?.dispose();
     257              :   }
     258              : 
     259            2 :   static String? getTransactionId(Map<String, dynamic> payload) {
     260            2 :     return payload['transaction_id'] ??
     261            2 :         (payload['m.relates_to'] is Map
     262            2 :             ? payload['m.relates_to']['event_id']
     263              :             : null);
     264              :   }
     265              : 
     266            3 :   List<String> get knownVerificationMethods {
     267              :     final methods = <String>{};
     268            9 :     if (client.verificationMethods.contains(KeyVerificationMethod.numbers) ||
     269            3 :         client.verificationMethods.contains(KeyVerificationMethod.emoji)) {
     270            2 :       methods.add(EventTypes.Sas);
     271              :     }
     272              : 
     273              :     /// `qrCanWork` -  qr cannot work if we are verifying another master key but our own is unverified
     274           12 :     final qrCanWork = (userId == client.userID) ||
     275           14 :         ((client.userDeviceKeys[client.userID]?.masterKey?.verified ?? false));
     276              : 
     277            9 :     if (client.verificationMethods.contains(KeyVerificationMethod.qrShow) &&
     278              :         qrCanWork) {
     279            2 :       methods.add(EventTypes.QRShow);
     280            2 :       methods.add(EventTypes.Reciprocate);
     281              :     }
     282            9 :     if (client.verificationMethods.contains(KeyVerificationMethod.qrScan) &&
     283              :         qrCanWork) {
     284            2 :       methods.add(EventTypes.QRScan);
     285            2 :       methods.add(EventTypes.Reciprocate);
     286              :     }
     287              : 
     288            3 :     return methods.toList();
     289              :   }
     290              : 
     291              :   /// Once you get a ready event, i.e both sides are in a `askChoice` state,
     292              :   /// send either `m.reciprocate.v1` or `m.sas.v1` here. If you continue with
     293              :   /// qr, send the qrData you just scanned
     294            2 :   Future<void> continueVerification(
     295              :     String type, {
     296              :     Uint8List? qrDataRawBytes,
     297              :   }) async {
     298              :     bool qrChecksOut = false;
     299            4 :     if (possibleMethods.contains(type)) {
     300              :       if (qrDataRawBytes != null) {
     301            2 :         qrChecksOut = await verifyQrData(qrDataRawBytes);
     302              :         // after this scanners state is done
     303              :       }
     304            2 :       if (type != EventTypes.Reciprocate || qrChecksOut) {
     305            4 :         final method = _method = _makeVerificationMethod(type, this);
     306            2 :         await method.sendStart();
     307            2 :         if (type == EventTypes.Sas) {
     308            0 :           setState(KeyVerificationState.waitingAccept);
     309              :         }
     310            0 :       } else if (type == EventTypes.Reciprocate && !qrChecksOut) {
     311            0 :         Logs().e('[KeyVerification] qr did not check out');
     312            0 :         await cancel('m.invalid_key');
     313              :       }
     314              :     } else {
     315            4 :       Logs().e(
     316              :         '[KeyVerification] tried to continue verification with a unknown method',
     317              :       );
     318            2 :       await cancel('m.unknown_method');
     319              :     }
     320              :   }
     321              : 
     322            3 :   Future<void> sendRequest() async {
     323            3 :     await send(
     324              :       EventTypes.KeyVerificationRequest,
     325            3 :       {
     326            6 :         'methods': knownVerificationMethods,
     327            9 :         if (room == null) 'timestamp': DateTime.now().millisecondsSinceEpoch,
     328              :       },
     329              :     );
     330            3 :     startedVerification = true;
     331            3 :     setState(KeyVerificationState.waitingAccept);
     332            6 :     lastActivity = DateTime.now();
     333              :   }
     334              : 
     335            3 :   Future<void> start() async {
     336            3 :     if (room == null) {
     337            6 :       transactionId = client.generateUniqueTransactionId();
     338              :     }
     339            9 :     if (encryption.crossSigning.enabled &&
     340            9 :         !(await encryption.crossSigning.isCached()) &&
     341            4 :         !client.isUnknownSession) {
     342            2 :       setState(KeyVerificationState.askSSSS);
     343            2 :       _nextAction = 'request';
     344              :     } else {
     345            3 :       await sendRequest();
     346              :     }
     347              :   }
     348              : 
     349              :   bool _handlePayloadLock = false;
     350              : 
     351            2 :   QRMode getOurQRMode() {
     352              :     QRMode mode = QRMode.verifyOtherUser;
     353            8 :     if (client.userID == userId) {
     354            2 :       if (client.encryption != null &&
     355            3 :           client.encryption!.enabled &&
     356            7 :           (client.userDeviceKeys[client.userID]?.masterKey?.directVerified ??
     357              :               false)) {
     358              :         mode = QRMode.verifySelfTrusted;
     359              :       } else {
     360              :         mode = QRMode.verifySelfUntrusted;
     361              :       }
     362              :     }
     363              :     return mode;
     364              :   }
     365              : 
     366            2 :   Future<void> handlePayload(
     367              :     String type,
     368              :     Map<String, dynamic> payload, [
     369              :     String? eventId,
     370              :   ]) async {
     371            2 :     if (isDone) {
     372              :       return; // no need to do anything with already canceled requests
     373              :     }
     374            2 :     while (_handlePayloadLock) {
     375            0 :       await Future.delayed(Duration(milliseconds: 50));
     376              :     }
     377            2 :     _handlePayloadLock = true;
     378            6 :     Logs().i('[Key Verification] Received type $type: $payload');
     379              :     try {
     380            2 :       var thisLastStep = lastStep;
     381              :       switch (type) {
     382            2 :         case EventTypes.KeyVerificationRequest:
     383            4 :           _deviceId ??= payload['from_device'];
     384            3 :           transactionId ??= eventId ?? payload['transaction_id'];
     385              :           // verify the timestamp
     386            2 :           final now = DateTime.now();
     387              :           final verifyTime =
     388            4 :               DateTime.fromMillisecondsSinceEpoch(payload['timestamp']);
     389            6 :           if (now.subtract(Duration(minutes: 10)).isAfter(verifyTime) ||
     390            6 :               now.add(Duration(minutes: 5)).isBefore(verifyTime)) {
     391              :             // if the request is more than 20min in the past we just silently fail it
     392              :             // to not generate too many cancels
     393            0 :             await cancel(
     394              :               'm.timeout',
     395            0 :               now.subtract(Duration(minutes: 20)).isAfter(verifyTime),
     396              :             );
     397              :             return;
     398              :           }
     399              : 
     400              :           // ensure we have the other sides keys
     401           14 :           if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
     402            0 :             await client.updateUserDeviceKeys(additionalUsers: {userId});
     403            0 :             if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
     404            0 :               await cancel('im.fluffychat.unknown_device');
     405              :               return;
     406              :             }
     407              :           }
     408              : 
     409            6 :           oppositePossibleMethods = List<String>.from(payload['methods']);
     410              :           // verify it has a method we can use
     411            4 :           possibleMethods = _calculatePossibleMethods(
     412            2 :             knownVerificationMethods,
     413            2 :             payload['methods'],
     414              :           );
     415            4 :           if (possibleMethods.isEmpty) {
     416              :             // reject it outright
     417            0 :             await cancel('m.unknown_method');
     418              :             return;
     419              :           }
     420              : 
     421            2 :           setState(KeyVerificationState.askAccept);
     422              :           break;
     423            2 :         case EventTypes.KeyVerificationReady:
     424            4 :           if (deviceId == '*') {
     425            2 :             _deviceId = payload['from_device']; // gotta set the real device id
     426            1 :             transactionId ??= eventId ?? payload['transaction_id'];
     427              :             // and broadcast the cancel to the other devices
     428            1 :             final devices = List<DeviceKeys>.from(
     429            6 :               client.userDeviceKeys[userId]?.deviceKeys.values ??
     430            0 :                   Iterable.empty(),
     431              :             );
     432            1 :             devices.removeWhere(
     433            6 :               (d) => {deviceId, client.deviceID}.contains(d.deviceId),
     434              :             );
     435            1 :             final cancelPayload = <String, dynamic>{
     436              :               'reason': 'Another device accepted the request',
     437              :               'code': 'm.accepted',
     438              :             };
     439            1 :             makePayload(cancelPayload);
     440            2 :             await client.sendToDeviceEncrypted(
     441              :               devices,
     442              :               EventTypes.KeyVerificationCancel,
     443              :               cancelPayload,
     444              :             );
     445              :           }
     446            3 :           _deviceId ??= payload['from_device'];
     447              : 
     448              :           // ensure we have the other sides keys
     449           14 :           if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
     450            0 :             await client.updateUserDeviceKeys(additionalUsers: {userId});
     451            0 :             if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
     452            0 :               await cancel('im.fluffychat.unknown_device');
     453              :               return;
     454              :             }
     455              :           }
     456              : 
     457            6 :           oppositePossibleMethods = List<String>.from(payload['methods']);
     458            4 :           possibleMethods = _calculatePossibleMethods(
     459            2 :             knownVerificationMethods,
     460            2 :             payload['methods'],
     461              :           );
     462            4 :           if (possibleMethods.isEmpty) {
     463              :             // reject it outright
     464            0 :             await cancel('m.unknown_method');
     465              :             return;
     466              :           }
     467              :           // as both parties can send a start, the last step being "ready" is race-condition prone
     468              :           // as such, we better set it *before* we send our start
     469            2 :           lastStep = type;
     470              : 
     471              :           // setup QRData from outgoing request (incoming ready)
     472            4 :           qrCode = await generateQrCode();
     473              : 
     474              :           // play nice with sdks < 0.20.5
     475              :           // https://matrix.to/#/!KBwfdofYJUmnsVoqwn:famedly.de/$wlHXlLQJdfrqKAF5KkuQrXydwOhY_uyqfH4ReasZqnA?via=neko.dev&via=famedly.de&via=lihotzki.de
     476            6 :           if (!isQrSupported(knownVerificationMethods, payload['methods'])) {
     477            4 :             if (knownVerificationMethods.contains(EventTypes.Sas)) {
     478            2 :               final method = _method =
     479            6 :                   _makeVerificationMethod(possibleMethods.first, this);
     480            2 :               await method.sendStart();
     481            2 :               setState(KeyVerificationState.waitingAccept);
     482              :             }
     483              :           } else {
     484              :             // allow user to choose
     485            2 :             setState(KeyVerificationState.askChoice);
     486              :           }
     487              : 
     488              :           break;
     489            2 :         case EventTypes.KeyVerificationStart:
     490            2 :           _deviceId ??= payload['from_device'];
     491            2 :           transactionId ??= eventId ?? payload['transaction_id'];
     492            2 :           if (_method != null) {
     493              :             // the other side sent us a start, even though we already sent one
     494            0 :             if (payload['method'] == _method!.type) {
     495              :               // same method. Determine priority
     496            0 :               final ourEntry = '${client.userID}|${client.deviceID}';
     497            0 :               final entries = [ourEntry, '$userId|$deviceId'];
     498            0 :               entries.sort();
     499            0 :               if (entries.first == ourEntry) {
     500              :                 // our start won, nothing to do
     501              :                 return;
     502              :               } else {
     503              :                 // the other start won, let's hand off
     504            0 :                 startedVerification = false; // it is now as if they started
     505            0 :                 thisLastStep = lastStep =
     506              :                     EventTypes.KeyVerificationRequest; // we fake the last step
     507            0 :                 _method!.dispose(); // in case anything got created already
     508              :               }
     509              :             } else {
     510              :               // methods don't match up, let's cancel this
     511            0 :               await cancel('m.unexpected_message');
     512              :               return;
     513              :             }
     514              :           }
     515            4 :           if (!(await verifyLastStep([
     516              :             EventTypes.KeyVerificationRequest,
     517              :             EventTypes.KeyVerificationReady,
     518              :           ]))) {
     519              :             return; // abort
     520              :           }
     521            6 :           if (!knownVerificationMethods.contains(payload['method'])) {
     522            0 :             await cancel('m.unknown_method');
     523              :             return;
     524              :           }
     525              : 
     526            4 :           if (lastStep == EventTypes.KeyVerificationRequest) {
     527            6 :             if (!possibleMethods.contains(payload['method'])) {
     528            1 :               await cancel('m.unknown_method');
     529              :               return;
     530              :             }
     531              :           }
     532              : 
     533              :           // ensure we have the other sides keys
     534           14 :           if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
     535            0 :             await client.updateUserDeviceKeys(additionalUsers: {userId});
     536            0 :             if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) {
     537            0 :               await cancel('im.fluffychat.unknown_device');
     538              :               return;
     539              :             }
     540              :           }
     541              : 
     542            6 :           _method = _makeVerificationMethod(payload['method'], this);
     543            2 :           if (lastStep == null) {
     544              :             // validate the start time
     545            0 :             if (room != null) {
     546              :               // we just silently ignore in-room-verification starts
     547            0 :               await cancel('m.unknown_method', true);
     548              :               return;
     549              :             }
     550              :             // validate the specific payload
     551            0 :             if (!_method!.validateStart(payload)) {
     552            0 :               await cancel('m.unknown_method');
     553              :               return;
     554              :             }
     555            0 :             startPayload = payload;
     556            0 :             setState(KeyVerificationState.askAccept);
     557              :           } else {
     558            4 :             Logs().i('handling start in method.....');
     559            4 :             await _method!.handlePayload(type, payload);
     560              :           }
     561              :           break;
     562            2 :         case EventTypes.KeyVerificationDone:
     563            4 :           if (state == KeyVerificationState.showQRSuccess) {
     564            4 :             await send(EventTypes.KeyVerificationDone, {});
     565            2 :             setState(KeyVerificationState.done);
     566              :           }
     567              :           break;
     568            1 :         case EventTypes.KeyVerificationCancel:
     569            1 :           canceled = true;
     570            2 :           canceledCode = payload['code'];
     571            2 :           canceledReason = payload['reason'];
     572            1 :           setState(KeyVerificationState.error);
     573              :           break;
     574              :         default:
     575            1 :           final method = _method;
     576              :           if (method != null) {
     577            1 :             await method.handlePayload(type, payload);
     578              :           } else {
     579            0 :             await cancel('m.invalid_message');
     580              :           }
     581              :           break;
     582              :       }
     583            4 :       if (lastStep == thisLastStep) {
     584            2 :         lastStep = type;
     585              :       }
     586              :     } catch (err, stacktrace) {
     587            0 :       Logs().e('[Key Verification] An error occured', err, stacktrace);
     588            0 :       await cancel('m.invalid_message');
     589              :     } finally {
     590            2 :       _handlePayloadLock = false;
     591              :     }
     592              :   }
     593              : 
     594            1 :   void otherDeviceAccepted() {
     595            1 :     canceled = true;
     596            1 :     canceledCode = 'm.accepted';
     597            1 :     canceledReason = 'm.accepted';
     598            1 :     setState(KeyVerificationState.error);
     599              :   }
     600              : 
     601            2 :   Future<void> openSSSS({
     602              :     String? passphrase,
     603              :     String? recoveryKey,
     604              :     String? keyOrPassphrase,
     605              :     bool skip = false,
     606              :   }) async {
     607            2 :     Future<void> next() async {
     608            4 :       if (_nextAction == 'request') {
     609            2 :         await sendRequest();
     610            4 :       } else if (_nextAction == 'done') {
     611              :         // and now let's sign them all in the background
     612           10 :         unawaited(encryption.crossSigning.sign(_verifiedDevices));
     613            2 :         setState(KeyVerificationState.done);
     614            0 :       } else if (_nextAction == 'showQRSuccess') {
     615            0 :         setState(KeyVerificationState.showQRSuccess);
     616              :       }
     617              :     }
     618              : 
     619              :     if (skip) {
     620            0 :       await next();
     621              :       return;
     622              :     }
     623            6 :     final handle = encryption.ssss.open(EventTypes.CrossSigningUserSigning);
     624            2 :     await handle.unlock(
     625              :       passphrase: passphrase,
     626              :       recoveryKey: recoveryKey,
     627              :       keyOrPassphrase: keyOrPassphrase,
     628              :     );
     629            2 :     await handle.maybeCacheAll();
     630            2 :     await next();
     631              :   }
     632              : 
     633              :   /// called when the user accepts an incoming verification
     634            2 :   Future<void> acceptVerification() async {
     635            4 :     if (!(await verifyLastStep([
     636              :       EventTypes.KeyVerificationRequest,
     637              :       EventTypes.KeyVerificationStart,
     638              :     ]))) {
     639              :       return;
     640              :     }
     641            2 :     setState(KeyVerificationState.waitingAccept);
     642            4 :     if (lastStep == EventTypes.KeyVerificationRequest) {
     643              :       final copyKnownVerificationMethods =
     644            4 :           List<String>.from(knownVerificationMethods);
     645              :       // qr code only works when atleast one side has verified master key
     646            8 :       if (userId == client.userID) {
     647            8 :         if (!(client.userDeviceKeys[client.userID]?.deviceKeys[deviceId]
     648            1 :                     ?.hasValidSignatureChain(verifiedByTheirMasterKey: true) ??
     649              :                 false) &&
     650            7 :             !(client.userDeviceKeys[client.userID]?.masterKey?.verified ??
     651              :                 false)) {
     652              :           copyKnownVerificationMethods
     653            3 :               .removeWhere((element) => element.startsWith('m.qr_code'));
     654            1 :           copyKnownVerificationMethods.remove(EventTypes.Reciprocate);
     655              : 
     656              :           // we are removing stuff only using the old possibleMethods should be ok here.
     657            2 :           final copyPossibleMethods = List<String>.from(possibleMethods);
     658            2 :           possibleMethods = _calculatePossibleMethods(
     659              :             copyKnownVerificationMethods,
     660              :             copyPossibleMethods,
     661              :           );
     662              :         }
     663              :       }
     664              :       // we need to send a ready event
     665            4 :       await send(EventTypes.KeyVerificationReady, {
     666              :         'methods': copyKnownVerificationMethods,
     667              :       });
     668              :       // setup QRData from incoming request (outgoing ready)
     669            4 :       qrCode = await generateQrCode();
     670            2 :       setState(KeyVerificationState.askChoice);
     671              :     } else {
     672              :       // we need to send an accept event
     673            0 :       await _method!
     674            0 :           .handlePayload(EventTypes.KeyVerificationStart, startPayload!);
     675              :     }
     676              :   }
     677              : 
     678              :   /// called when the user rejects an incoming verification
     679            1 :   Future<void> rejectVerification() async {
     680            1 :     if (isDone) {
     681              :       return;
     682              :     }
     683            2 :     if (!(await verifyLastStep([
     684              :       EventTypes.KeyVerificationRequest,
     685              :       EventTypes.KeyVerificationStart,
     686              :     ]))) {
     687              :       return;
     688              :     }
     689            1 :     await cancel('m.user');
     690              :   }
     691              : 
     692              :   /// call this to confirm that your other device has shown a shield and is in
     693              :   /// `done` state.
     694            2 :   Future<void> acceptQRScanConfirmation() async {
     695            4 :     if (_method is _KeyVerificationMethodQRReciprocate &&
     696            4 :         state == KeyVerificationState.confirmQRScan) {
     697            2 :       await (_method as _KeyVerificationMethodQRReciprocate)
     698            2 :           .acceptQRScanConfirmation();
     699              :     }
     700              :   }
     701              : 
     702            1 :   Future<void> acceptSas() async {
     703            2 :     if (_method is _KeyVerificationMethodSas) {
     704            2 :       await (_method as _KeyVerificationMethodSas).acceptSas();
     705              :     }
     706              :   }
     707              : 
     708            1 :   Future<void> rejectSas() async {
     709            2 :     if (_method is _KeyVerificationMethodSas) {
     710            2 :       await (_method as _KeyVerificationMethodSas).rejectSas();
     711              :     }
     712              :   }
     713              : 
     714            1 :   List<int> get sasNumbers {
     715            2 :     if (_method is _KeyVerificationMethodSas) {
     716            3 :       return _bytesToInt((_method as _KeyVerificationMethodSas).makeSas(5), 13)
     717            3 :           .map((n) => n + 1000)
     718            1 :           .toList();
     719              :     }
     720            0 :     return [];
     721              :   }
     722              : 
     723            1 :   List<String> get sasTypes {
     724            2 :     if (_method is _KeyVerificationMethodSas) {
     725            2 :       return (_method as _KeyVerificationMethodSas).authenticationTypes ?? [];
     726              :     }
     727            0 :     return [];
     728              :   }
     729              : 
     730            1 :   List<KeyVerificationEmoji> get sasEmojis {
     731            2 :     if (_method is _KeyVerificationMethodSas) {
     732              :       final numbers =
     733            3 :           _bytesToInt((_method as _KeyVerificationMethodSas).makeSas(6), 6);
     734            5 :       return numbers.map((n) => KeyVerificationEmoji(n)).toList().sublist(0, 7);
     735              :     }
     736            0 :     return [];
     737              :   }
     738              : 
     739            1 :   Future<void> maybeRequestSSSSSecrets([int i = 0]) async {
     740            1 :     final requestInterval = <int>[10, 60];
     741            3 :     if ((!encryption.crossSigning.enabled ||
     742            3 :             (encryption.crossSigning.enabled &&
     743            3 :                 (await encryption.crossSigning.isCached()))) &&
     744            0 :         (!encryption.keyManager.enabled ||
     745            0 :             (encryption.keyManager.enabled &&
     746            0 :                 (await encryption.keyManager.isCached())))) {
     747              :       // no need to request cache, we already have it
     748              :       return;
     749              :     }
     750            1 :     unawaited(
     751            2 :       encryption.ssss
     752            4 :           .maybeRequestAll(_verifiedDevices.whereType<DeviceKeys>().toList()),
     753              :     );
     754            2 :     if (requestInterval.length <= i) {
     755              :       return;
     756              :     }
     757            1 :     Timer(
     758            2 :       Duration(seconds: requestInterval[i]),
     759            0 :       () => maybeRequestSSSSSecrets(i + 1),
     760              :     );
     761              :   }
     762              : 
     763            1 :   Future<void> verifyKeysSAS(
     764              :     Map<String, String> keys,
     765              :     Future<bool> Function(String, SignableKey) verifier,
     766              :   ) async {
     767            2 :     _verifiedDevices = <SignableKey>[];
     768              : 
     769            4 :     final userDeviceKey = client.userDeviceKeys[userId];
     770              :     if (userDeviceKey == null) {
     771            0 :       await cancel('m.key_mismatch');
     772              :       return;
     773              :     }
     774            2 :     for (final entry in keys.entries) {
     775            1 :       final keyId = entry.key;
     776            2 :       final verifyDeviceId = keyId.substring('ed25519:'.length);
     777            1 :       final keyInfo = entry.value;
     778            1 :       final key = userDeviceKey.getKey(verifyDeviceId);
     779              :       if (key != null) {
     780            1 :         if (!(await verifier(keyInfo, key))) {
     781            0 :           await cancel('m.key_mismatch');
     782              :           return;
     783              :         }
     784            2 :         _verifiedDevices.add(key);
     785              :       }
     786              :     }
     787              :     // okay, we reached this far, so all the devices are verified!
     788              :     var verifiedMasterKey = false;
     789            2 :     final wasUnknownSession = client.isUnknownSession;
     790            2 :     for (final key in _verifiedDevices) {
     791            1 :       await key.setVerified(
     792              :         true,
     793              :         false,
     794              :       ); // we don't want to sign the keys juuuust yet
     795            3 :       if (key is CrossSigningKey && key.usage.contains('master')) {
     796              :         verifiedMasterKey = true;
     797              :       }
     798              :     }
     799            4 :     if (verifiedMasterKey && userId == client.userID) {
     800              :       // it was our own master key, let's request the cross signing keys
     801              :       // we do it in the background, thus no await needed here
     802              :       // ignore: unawaited_futures
     803            0 :       maybeRequestSSSSSecrets();
     804              :     }
     805            2 :     await send(EventTypes.KeyVerificationDone, {});
     806              : 
     807              :     var askingSSSS = false;
     808            3 :     if (encryption.crossSigning.enabled &&
     809            4 :         encryption.crossSigning.signable(_verifiedDevices)) {
     810              :       // these keys can be signed! Let's do so
     811            3 :       if (await encryption.crossSigning.isCached()) {
     812              :         // we want to make sure the verification state is correct for the other party after this event is handled.
     813              :         // Otherwise the verification dialog might be stuck in an unverified but done state for a bit.
     814            0 :         await encryption.crossSigning.sign(_verifiedDevices);
     815              :       } else if (!wasUnknownSession) {
     816              :         askingSSSS = true;
     817              :       }
     818              :     }
     819              :     if (askingSSSS) {
     820            1 :       setState(KeyVerificationState.askSSSS);
     821            1 :       _nextAction = 'done';
     822              :     } else {
     823            1 :       setState(KeyVerificationState.done);
     824              :     }
     825              :   }
     826              : 
     827              :   /// shower is true only for reciprocated verifications (shower side)
     828            2 :   Future<void> verifyKeysQR(SignableKey key, {bool shower = true}) async {
     829              :     var verifiedMasterKey = false;
     830            4 :     final wasUnknownSession = client.isUnknownSession;
     831              : 
     832            2 :     key.setDirectVerified(true);
     833            6 :     if (key is CrossSigningKey && key.usage.contains('master')) {
     834              :       verifiedMasterKey = true;
     835              :     }
     836              : 
     837            8 :     if (verifiedMasterKey && userId == client.userID) {
     838              :       // it was our own master key, let's request the cross signing keys
     839              :       // we do it in the background, thus no await needed here
     840              :       // ignore: unawaited_futures
     841            1 :       maybeRequestSSSSSecrets();
     842              :     }
     843              :     if (shower) {
     844            4 :       await send(EventTypes.KeyVerificationDone, {});
     845              :     }
     846            4 :     final keyList = List<SignableKey>.from([key]);
     847              :     var askingSSSS = false;
     848            6 :     if (encryption.crossSigning.enabled &&
     849            6 :         encryption.crossSigning.signable(keyList)) {
     850              :       // these keys can be signed! Let's do so
     851            6 :       if (await encryption.crossSigning.isCached()) {
     852              :         // we want to make sure the verification state is correct for the other party after this event is handled.
     853              :         // Otherwise the verification dialog might be stuck in an unverified but done state for a bit.
     854            6 :         await encryption.crossSigning.sign(keyList);
     855              :       } else if (!wasUnknownSession) {
     856              :         askingSSSS = true;
     857              :       }
     858              :     }
     859              :     if (askingSSSS) {
     860              :       // no need to worry about shower/scanner here because if scanner was
     861              :       // verified, ssss is already
     862            1 :       setState(KeyVerificationState.askSSSS);
     863              :       if (shower) {
     864            1 :         _nextAction = 'done';
     865              :       } else {
     866            0 :         _nextAction = 'showQRSuccess';
     867              :       }
     868              :     } else {
     869              :       if (shower) {
     870            2 :         setState(KeyVerificationState.done);
     871              :       } else {
     872            2 :         setState(KeyVerificationState.showQRSuccess);
     873              :       }
     874              :     }
     875              :   }
     876              : 
     877            2 :   Future<bool> verifyActivity() async {
     878           10 :     if (lastActivity.add(Duration(minutes: 10)).isAfter(DateTime.now())) {
     879            4 :       lastActivity = DateTime.now();
     880              :       return true;
     881              :     }
     882            0 :     await cancel('m.timeout');
     883              :     return false;
     884              :   }
     885              : 
     886            2 :   Future<bool> verifyLastStep(List<String?> checkLastStep) async {
     887            2 :     if (!(await verifyActivity())) {
     888              :       return false;
     889              :     }
     890            4 :     if (checkLastStep.contains(lastStep)) {
     891              :       return true;
     892              :     }
     893            0 :     Logs().e(
     894            0 :       '[KeyVerificaton] lastStep mismatch cancelling, expected from ${checkLastStep.toString()} was ${lastStep.toString()}',
     895              :     );
     896            0 :     await cancel('m.unexpected_message');
     897              :     return false;
     898              :   }
     899              : 
     900            2 :   Future<void> cancel([String code = 'm.unknown', bool quiet = false]) async {
     901            3 :     if (!quiet && (deviceId != null || room != null)) {
     902            4 :       await send(EventTypes.KeyVerificationCancel, {
     903              :         'reason': code,
     904              :         'code': code,
     905              :       });
     906              :     }
     907            2 :     canceled = true;
     908            2 :     canceledCode = code;
     909            2 :     setState(KeyVerificationState.error);
     910              :   }
     911              : 
     912            3 :   void makePayload(Map<String, dynamic> payload) {
     913            9 :     payload['from_device'] = client.deviceID;
     914            3 :     if (transactionId != null) {
     915            3 :       if (room != null) {
     916            2 :         payload['m.relates_to'] = {
     917              :           'rel_type': 'm.reference',
     918            1 :           'event_id': transactionId,
     919              :         };
     920              :       } else {
     921            4 :         payload['transaction_id'] = transactionId;
     922              :       }
     923              :     }
     924              :   }
     925              : 
     926            3 :   Future<void> send(
     927              :     String type,
     928              :     Map<String, dynamic> payload,
     929              :   ) async {
     930            3 :     makePayload(payload);
     931            9 :     Logs().i('[Key Verification] Sending type $type: $payload');
     932            3 :     if (room != null) {
     933           12 :       Logs().i('[Key Verification] Sending to $userId in room ${room!.id}...');
     934            4 :       if ({EventTypes.KeyVerificationRequest}.contains(type)) {
     935            2 :         payload['msgtype'] = type;
     936            4 :         payload['to'] = userId;
     937            2 :         payload['body'] =
     938            2 :             'Attempting verification request. ($type) Apparently your client doesn\'t support this';
     939              :         type = EventTypes.Message;
     940              :       }
     941            4 :       final newTransactionId = await room!.sendEvent(payload, type: type);
     942            2 :       if (transactionId == null) {
     943            2 :         transactionId = newTransactionId;
     944            6 :         encryption.keyVerificationManager.addRequest(this);
     945              :       }
     946              :     } else {
     947           10 :       Logs().i('[Key Verification] Sending to $userId device $deviceId...');
     948            4 :       if (deviceId == '*') {
     949              :         if ({
     950            1 :           EventTypes.KeyVerificationRequest,
     951            1 :           EventTypes.KeyVerificationCancel,
     952            1 :         }.contains(type)) {
     953              :           final deviceKeys =
     954            7 :               client.userDeviceKeys[userId]?.deviceKeys.values.where(
     955            2 :             (deviceKey) => deviceKey.hasValidSignatureChain(
     956              :               verifiedByTheirMasterKey: true,
     957              :             ),
     958              :           );
     959              : 
     960              :           if (deviceKeys != null) {
     961            2 :             await client.sendToDeviceEncrypted(
     962            1 :               deviceKeys.toList(),
     963              :               type,
     964              :               payload,
     965              :             );
     966              :           }
     967              :         } else {
     968            0 :           Logs().e(
     969            0 :             '[Key Verification] Tried to broadcast and un-broadcastable type: $type',
     970              :           );
     971              :         }
     972              :       } else {
     973           14 :         if (client.userDeviceKeys[userId]?.deviceKeys[deviceId] != null) {
     974            4 :           await client.sendToDeviceEncrypted(
     975           16 :             [client.userDeviceKeys[userId]!.deviceKeys[deviceId]!],
     976              :             type,
     977              :             payload,
     978              :           );
     979              :         } else {
     980            0 :           Logs().e('[Key Verification] Unknown device');
     981              :         }
     982              :       }
     983              :     }
     984              :   }
     985              : 
     986            3 :   void setState(KeyVerificationState newState) {
     987            6 :     if (state != KeyVerificationState.error) {
     988            3 :       state = newState;
     989              :     }
     990              : 
     991            3 :     onUpdate?.call();
     992              :   }
     993              : 
     994              :   static const String prefix = 'MATRIX';
     995              :   static const int version = 0x02;
     996              : 
     997            2 :   Future<bool> verifyQrData(Uint8List qrDataRawBytes) async {
     998              :     final data = qrDataRawBytes;
     999              :     // hardcoded stuff + 2 keys + secret
    1000           18 :     if (data.length < 10 + 32 + 32 + 8 + utf8.encode(transactionId!).length) {
    1001              :       return false;
    1002              :     }
    1003            4 :     if (data[6] != version) return false;
    1004              :     final remoteQrMode =
    1005           10 :         QRMode.values.singleWhere((mode) => mode.code == data[7]);
    1006            6 :     if (ascii.decode(data.sublist(0, 6)) != prefix) return false;
    1007            4 :     if (data[6] != version) return false;
    1008            8 :     final tmpBuf = Uint8List.fromList([data[8], data[9]]);
    1009            6 :     final encodedTxnLen = ByteData.view(tmpBuf.buffer).getUint16(0);
    1010           10 :     if (utf8.decode(data.sublist(10, 10 + encodedTxnLen)) != transactionId) {
    1011              :       return false;
    1012              :     }
    1013            4 :     final keys = client.userDeviceKeys;
    1014              : 
    1015            6 :     final ownKeys = keys[client.userID];
    1016            4 :     final otherUserKeys = keys[userId];
    1017            2 :     final ownMasterKey = ownKeys?.getCrossSigningKey('master');
    1018            6 :     final ownDeviceKey = ownKeys?.getKey(client.deviceID!);
    1019            4 :     final ownOtherDeviceKey = ownKeys?.getKey(deviceId!);
    1020            2 :     final otherUserMasterKey = otherUserKeys?.masterKey;
    1021              : 
    1022            2 :     final secondKey = encodeBase64Unpadded(
    1023           12 :       data.sublist(10 + encodedTxnLen + 32, 10 + encodedTxnLen + 32 + 32),
    1024              :     );
    1025              :     final randomSharedSecret =
    1026           10 :         encodeBase64Unpadded(data.sublist(10 + encodedTxnLen + 32 + 32));
    1027              : 
    1028              :     /// `request.randomSharedSecretForQRCode` is overwritten below to send with `sendStart`
    1029            4 :     if ({QRMode.verifyOtherUser, QRMode.verifySelfUntrusted}
    1030            2 :         .contains(remoteQrMode)) {
    1031            2 :       if (!(ownMasterKey?.verified ?? false)) {
    1032            0 :         Logs().e(
    1033              :           '[KeyVerification] verifyQrData because you were in mode 0/2 and had untrusted msk',
    1034              :         );
    1035              :         return false;
    1036              :       }
    1037              :     }
    1038              : 
    1039            2 :     if (remoteQrMode == QRMode.verifyOtherUser &&
    1040              :         otherUserMasterKey != null &&
    1041              :         ownMasterKey != null) {
    1042            2 :       if (secondKey == ownMasterKey.ed25519Key) {
    1043            1 :         randomSharedSecretForQRCode = randomSharedSecret;
    1044            1 :         await verifyKeysQR(otherUserMasterKey, shower: false);
    1045              :         return true;
    1046              :       }
    1047            1 :     } else if (remoteQrMode == QRMode.verifySelfTrusted &&
    1048              :         ownMasterKey != null &&
    1049              :         ownDeviceKey != null) {
    1050            2 :       if (secondKey == ownDeviceKey.ed25519Key) {
    1051            1 :         randomSharedSecretForQRCode = randomSharedSecret;
    1052            1 :         await verifyKeysQR(ownMasterKey, shower: false);
    1053              :         return true;
    1054              :       }
    1055            1 :     } else if (remoteQrMode == QRMode.verifySelfUntrusted &&
    1056              :         ownOtherDeviceKey != null &&
    1057              :         ownMasterKey != null) {
    1058            2 :       if (secondKey == ownMasterKey.ed25519Key) {
    1059            1 :         randomSharedSecretForQRCode = randomSharedSecret;
    1060            1 :         await verifyKeysQR(ownOtherDeviceKey, shower: false);
    1061              :         return true;
    1062              :       }
    1063              :     }
    1064              : 
    1065              :     return false;
    1066              :   }
    1067              : 
    1068            2 :   Future<(String, String)?> getKeys(QRMode mode) async {
    1069            4 :     final keys = client.userDeviceKeys;
    1070              : 
    1071            6 :     final ownKeys = keys[client.userID];
    1072            4 :     final otherUserKeys = keys[userId];
    1073            6 :     final ownDeviceKey = ownKeys?.getKey(client.deviceID!);
    1074            2 :     final ownMasterKey = ownKeys?.getCrossSigningKey('master');
    1075            4 :     final otherDeviceKey = otherUserKeys?.getKey(deviceId!);
    1076            2 :     final otherMasterKey = otherUserKeys?.getCrossSigningKey('master');
    1077              : 
    1078            2 :     if (mode == QRMode.verifyOtherUser &&
    1079              :         ownMasterKey != null &&
    1080              :         otherMasterKey != null) {
    1081              :       // we already have this check when sending `knownVerificationMethods`, but
    1082              :       // just to be safe anyway
    1083            1 :       if (ownMasterKey.verified) {
    1084            2 :         return (ownMasterKey.ed25519Key!, otherMasterKey.ed25519Key!);
    1085              :       }
    1086            1 :     } else if (mode == QRMode.verifySelfTrusted &&
    1087              :         ownMasterKey != null &&
    1088              :         otherDeviceKey != null) {
    1089            1 :       if (ownMasterKey.verified) {
    1090            2 :         return (ownMasterKey.ed25519Key!, otherDeviceKey.ed25519Key!);
    1091              :       }
    1092            1 :     } else if (mode == QRMode.verifySelfUntrusted &&
    1093              :         ownMasterKey != null &&
    1094              :         ownDeviceKey != null) {
    1095            2 :       return (ownDeviceKey.ed25519Key!, ownMasterKey.ed25519Key!);
    1096              :     }
    1097              :     return null;
    1098              :   }
    1099              : 
    1100            2 :   Future<QRCode?> generateQrCode() async {
    1101            2 :     final data = Uint8Buffer();
    1102              :     // why 11? https://github.com/matrix-org/matrix-js-sdk/commit/275ea6aacbfc6623e7559a7649ca5cab207903d9
    1103            2 :     randomSharedSecretForQRCode =
    1104            4 :         encodeBase64Unpadded(uc.secureRandomBytes(11));
    1105              : 
    1106            2 :     final mode = getOurQRMode();
    1107            4 :     data.addAll(ascii.encode(prefix));
    1108            2 :     data.add(version);
    1109            4 :     data.add(mode.code);
    1110            4 :     final encodedTxnId = utf8.encode(transactionId!);
    1111            2 :     final txnIdLen = encodedTxnId.length;
    1112            2 :     final tmpBuf = Uint8List(2);
    1113            6 :     ByteData.view(tmpBuf.buffer).setUint16(0, txnIdLen);
    1114            2 :     data.addAll(tmpBuf);
    1115            2 :     data.addAll(encodedTxnId);
    1116            2 :     final keys = await getKeys(mode);
    1117              :     if (keys != null) {
    1118            4 :       data.addAll(base64decodeUnpadded(keys.$1));
    1119            4 :       data.addAll(base64decodeUnpadded(keys.$2));
    1120              :     } else {
    1121              :       return null;
    1122              :     }
    1123              : 
    1124            6 :     data.addAll(base64decodeUnpadded(randomSharedSecretForQRCode!));
    1125            4 :     return QRCode(randomSharedSecretForQRCode!, data);
    1126              :   }
    1127              : }
    1128              : 
    1129              : abstract class _KeyVerificationMethod {
    1130              :   KeyVerification request;
    1131            3 :   Encryption get encryption => request.encryption;
    1132            6 :   Client get client => request.client;
    1133            2 :   _KeyVerificationMethod({required this.request});
    1134              : 
    1135              :   Future<void> handlePayload(String type, Map<String, dynamic> payload);
    1136            0 :   bool validateStart(Map<String, dynamic> payload) {
    1137              :     return false;
    1138              :   }
    1139              : 
    1140              :   late String _type;
    1141            4 :   String get type => _type;
    1142              : 
    1143              :   Future<void> sendStart();
    1144            2 :   void dispose() {}
    1145              : }
    1146              : 
    1147              : class _KeyVerificationMethodQRReciprocate extends _KeyVerificationMethod {
    1148            2 :   _KeyVerificationMethodQRReciprocate({required super.request});
    1149              : 
    1150              :   @override
    1151              :   // ignore: overridden_fields
    1152              :   final _type = EventTypes.Reciprocate;
    1153              : 
    1154            2 :   @override
    1155              :   bool validateStart(Map<String, dynamic> payload) {
    1156            6 :     if (payload['method'] != type) return false;
    1157            8 :     if (payload['secret'] != request.randomSharedSecretForQRCode) return false;
    1158              :     return true;
    1159              :   }
    1160              : 
    1161            2 :   @override
    1162              :   Future<void> handlePayload(String type, Map<String, dynamic> payload) async {
    1163              :     try {
    1164              :       switch (type) {
    1165            2 :         case EventTypes.KeyVerificationStart:
    1166            6 :           if (!(await request.verifyLastStep([
    1167              :             EventTypes.KeyVerificationReady,
    1168              :             EventTypes.KeyVerificationRequest,
    1169              :           ]))) {
    1170              :             return; // abort
    1171              :           }
    1172            2 :           if (!validateStart(payload)) {
    1173            2 :             await request.cancel('m.invalid_message');
    1174              :             return;
    1175              :           }
    1176            4 :           request.setState(KeyVerificationState.confirmQRScan);
    1177              :           break;
    1178              :       }
    1179              :     } catch (e, s) {
    1180            0 :       Logs().e('[Key Verification Reciprocate] An error occured', e, s);
    1181            0 :       if (request.deviceId != null) {
    1182            0 :         await request.cancel('m.invalid_message');
    1183              :       }
    1184              :     }
    1185              :   }
    1186              : 
    1187            2 :   Future<void> acceptQRScanConfirmation() async {
    1188              :     // secret validation already done in validateStart
    1189              : 
    1190            4 :     final ourQRMode = request.getOurQRMode();
    1191              :     SignableKey? keyToVerify;
    1192              : 
    1193            2 :     if (ourQRMode == QRMode.verifyOtherUser) {
    1194            6 :       keyToVerify = client.userDeviceKeys[request.userId]?.masterKey;
    1195            1 :     } else if (ourQRMode == QRMode.verifySelfTrusted) {
    1196              :       keyToVerify =
    1197            9 :           client.userDeviceKeys[client.userID]?.deviceKeys[request.deviceId];
    1198            1 :     } else if (ourQRMode == QRMode.verifySelfUntrusted) {
    1199            6 :       keyToVerify = client.userDeviceKeys[client.userID]?.masterKey;
    1200              :     }
    1201              :     if (keyToVerify != null) {
    1202            4 :       await request.verifyKeysQR(keyToVerify, shower: true);
    1203              :     } else {
    1204            0 :       Logs().e('[KeyVerification], verifying keys failed');
    1205            0 :       await request.cancel('m.invalid_key');
    1206              :     }
    1207              :   }
    1208              : 
    1209            2 :   @override
    1210              :   Future<void> sendStart() async {
    1211            2 :     final payload = <String, dynamic>{
    1212            2 :       'method': type,
    1213            4 :       'secret': request.randomSharedSecretForQRCode,
    1214              :     };
    1215            4 :     request.makePayload(payload);
    1216            4 :     await request.send(EventTypes.KeyVerificationStart, payload);
    1217              :   }
    1218              : 
    1219            2 :   @override
    1220              :   void dispose() {}
    1221              : }
    1222              : 
    1223              : enum QRMode {
    1224              :   verifyOtherUser(0x00),
    1225              :   verifySelfTrusted(0x01),
    1226              :   verifySelfUntrusted(0x02);
    1227              : 
    1228              :   const QRMode(this.code);
    1229              :   final int code;
    1230              : }
    1231              : 
    1232              : class QRCode {
    1233              :   /// You actually never need this when implementing in a client, its just to
    1234              :   /// make tests easier. Just pass `qrDataRawBytes` in `continueVerifcation()`
    1235              :   final String randomSharedSecret;
    1236              :   final Uint8Buffer qrDataRawBytes;
    1237            2 :   QRCode(this.randomSharedSecret, this.qrDataRawBytes);
    1238              : }
    1239              : 
    1240              : const knownKeyAgreementProtocols = ['curve25519-hkdf-sha256', 'curve25519'];
    1241              : const knownHashes = ['sha256'];
    1242              : const knownHashesAuthentificationCodes = [
    1243              :   'hkdf-hmac-sha256.v2',
    1244              :   'hkdf-hmac-sha256',
    1245              : ];
    1246              : 
    1247              : class _KeyVerificationMethodSas extends _KeyVerificationMethod {
    1248            2 :   _KeyVerificationMethodSas({required super.request});
    1249              : 
    1250              :   @override
    1251              :   // ignore: overridden_fields
    1252              :   final _type = EventTypes.Sas;
    1253              : 
    1254              :   String? keyAgreementProtocol;
    1255              :   String? hash;
    1256              :   String? messageAuthenticationCode;
    1257              :   List<String>? authenticationTypes;
    1258              :   late String startCanonicalJson;
    1259              :   String? commitment;
    1260              :   late String theirPublicKey;
    1261              :   Map<String, dynamic>? macPayload;
    1262              :   vod.Sas? sas;
    1263              :   vod.EstablishedSas? establishedSas;
    1264              : 
    1265            2 :   List<String> get knownAuthentificationTypes {
    1266            2 :     final types = <String>[];
    1267            6 :     if (request.client.verificationMethods
    1268            2 :         .contains(KeyVerificationMethod.emoji)) {
    1269            2 :       types.add('emoji');
    1270              :     }
    1271            6 :     if (request.client.verificationMethods
    1272            2 :         .contains(KeyVerificationMethod.numbers)) {
    1273            2 :       types.add('decimal');
    1274              :     }
    1275              :     return types;
    1276              :   }
    1277              : 
    1278            1 :   @override
    1279              :   Future<void> handlePayload(String type, Map<String, dynamic> payload) async {
    1280              :     try {
    1281              :       switch (type) {
    1282            1 :         case EventTypes.KeyVerificationStart:
    1283            3 :           if (!(await request.verifyLastStep([
    1284              :             EventTypes.KeyVerificationReady,
    1285              :             EventTypes.KeyVerificationRequest,
    1286              :             EventTypes.KeyVerificationStart,
    1287              :           ]))) {
    1288              :             return; // abort
    1289              :           }
    1290            1 :           if (!validateStart(payload)) {
    1291            0 :             await request.cancel('m.unknown_method');
    1292              :             return;
    1293              :           }
    1294            1 :           await _sendAccept();
    1295              :           break;
    1296            1 :         case EventTypes.KeyVerificationAccept:
    1297            3 :           if (!(await request.verifyLastStep([
    1298              :             EventTypes.KeyVerificationReady,
    1299              :             EventTypes.KeyVerificationRequest,
    1300              :           ]))) {
    1301              :             return;
    1302              :           }
    1303            1 :           if (!_handleAccept(payload)) {
    1304            0 :             await request.cancel('m.unknown_method');
    1305              :             return;
    1306              :           }
    1307            1 :           await _sendKey();
    1308              :           break;
    1309            1 :         case 'm.key.verification.key':
    1310            3 :           if (!(await request.verifyLastStep([
    1311              :             EventTypes.KeyVerificationAccept,
    1312              :             EventTypes.KeyVerificationStart,
    1313              :           ]))) {
    1314              :             return;
    1315              :           }
    1316            1 :           _handleKey(payload);
    1317            3 :           if (request.lastStep == EventTypes.KeyVerificationStart) {
    1318              :             // we need to send our key
    1319            1 :             await _sendKey();
    1320              :           } else {
    1321              :             // we already sent our key, time to verify the commitment being valid
    1322            2 :             if (await _validateCommitment() == false) {
    1323            0 :               await request.cancel('m.mismatched_commitment');
    1324              :               return;
    1325              :             }
    1326              :           }
    1327            2 :           request.setState(KeyVerificationState.askSas);
    1328              :           break;
    1329            1 :         case 'm.key.verification.mac':
    1330            3 :           if (!(await request.verifyLastStep(['m.key.verification.key']))) {
    1331              :             return;
    1332              :           }
    1333            1 :           macPayload = payload;
    1334            3 :           if (request.state == KeyVerificationState.waitingSas) {
    1335            1 :             await _processMac();
    1336              :           }
    1337              :           break;
    1338              :       }
    1339              :     } catch (err, stacktrace) {
    1340            0 :       Logs().e('[Key Verification SAS] An error occured', err, stacktrace);
    1341            0 :       if (request.deviceId != null) {
    1342            0 :         await request.cancel('m.invalid_message');
    1343              :       }
    1344              :     }
    1345              :   }
    1346              : 
    1347            1 :   Future<void> acceptSas() async {
    1348            1 :     await _sendMac();
    1349            2 :     request.setState(KeyVerificationState.waitingSas);
    1350            1 :     if (macPayload != null) {
    1351            1 :       await _processMac();
    1352              :     }
    1353              :   }
    1354              : 
    1355            1 :   Future<void> rejectSas() async {
    1356            2 :     await request.cancel('m.mismatched_sas');
    1357              :   }
    1358              : 
    1359            2 :   @override
    1360              :   Future<void> sendStart() async {
    1361            2 :     final payload = <String, dynamic>{
    1362            2 :       'method': type,
    1363              :       'key_agreement_protocols': knownKeyAgreementProtocols,
    1364              :       'hashes': knownHashes,
    1365              :       'message_authentication_codes': knownHashesAuthentificationCodes,
    1366            2 :       'short_authentication_string': knownAuthentificationTypes,
    1367              :     };
    1368            4 :     request.makePayload(payload);
    1369              :     // We just store the canonical json in here for later verification
    1370            6 :     startCanonicalJson = String.fromCharCodes(canonicalJson.encode(payload));
    1371            4 :     await request.send(EventTypes.KeyVerificationStart, payload);
    1372              :   }
    1373              : 
    1374            1 :   @override
    1375              :   bool validateStart(Map<String, dynamic> payload) {
    1376            3 :     if (payload['method'] != type) {
    1377              :       return false;
    1378              :     }
    1379            1 :     final possibleKeyAgreementProtocols = _intersect(
    1380              :       knownKeyAgreementProtocols,
    1381            1 :       payload['key_agreement_protocols'],
    1382              :     );
    1383            1 :     if (possibleKeyAgreementProtocols.isEmpty) {
    1384              :       return false;
    1385              :     }
    1386            2 :     keyAgreementProtocol = possibleKeyAgreementProtocols.first;
    1387            2 :     final possibleHashes = _intersect(knownHashes, payload['hashes']);
    1388            1 :     if (possibleHashes.isEmpty) {
    1389              :       return false;
    1390              :     }
    1391            2 :     hash = possibleHashes.first;
    1392            1 :     final possibleMessageAuthenticationCodes = _intersect(
    1393              :       knownHashesAuthentificationCodes,
    1394            1 :       payload['message_authentication_codes'],
    1395              :     );
    1396            1 :     if (possibleMessageAuthenticationCodes.isEmpty) {
    1397              :       return false;
    1398              :     }
    1399              : 
    1400              :     // intersect should make sure we choose v2 over the dep'd one
    1401            2 :     messageAuthenticationCode = possibleMessageAuthenticationCodes.first;
    1402            1 :     final possibleAuthenticationTypes = _intersect(
    1403            1 :       knownAuthentificationTypes,
    1404            1 :       payload['short_authentication_string'],
    1405              :     );
    1406            1 :     if (possibleAuthenticationTypes.isEmpty) {
    1407              :       return false;
    1408              :     }
    1409            1 :     authenticationTypes = possibleAuthenticationTypes;
    1410            3 :     startCanonicalJson = String.fromCharCodes(canonicalJson.encode(payload));
    1411              :     return true;
    1412              :   }
    1413              : 
    1414            1 :   Future<void> _sendAccept() async {
    1415            2 :     final sas = this.sas = vod.Sas();
    1416            4 :     commitment = await _makeCommitment(sas.publicKey, startCanonicalJson);
    1417            3 :     await request.send(EventTypes.KeyVerificationAccept, {
    1418            1 :       'method': type,
    1419            1 :       'key_agreement_protocol': keyAgreementProtocol,
    1420            1 :       'hash': hash,
    1421            1 :       'message_authentication_code': messageAuthenticationCode,
    1422            1 :       'short_authentication_string': authenticationTypes,
    1423            1 :       'commitment': commitment,
    1424              :     });
    1425              :   }
    1426              : 
    1427            1 :   bool _handleAccept(Map<String, dynamic> payload) {
    1428              :     if (!knownKeyAgreementProtocols
    1429            2 :         .contains(payload['key_agreement_protocol'])) {
    1430              :       return false;
    1431              :     }
    1432            2 :     keyAgreementProtocol = payload['key_agreement_protocol'];
    1433            2 :     if (!knownHashes.contains(payload['hash'])) {
    1434              :       return false;
    1435              :     }
    1436            2 :     hash = payload['hash'];
    1437              :     if (!knownHashesAuthentificationCodes
    1438            2 :         .contains(payload['message_authentication_code'])) {
    1439              :       return false;
    1440              :     }
    1441            2 :     messageAuthenticationCode = payload['message_authentication_code'];
    1442            1 :     final possibleAuthenticationTypes = _intersect(
    1443            1 :       knownAuthentificationTypes,
    1444            1 :       payload['short_authentication_string'],
    1445              :     );
    1446            1 :     if (possibleAuthenticationTypes.isEmpty) {
    1447              :       return false;
    1448              :     }
    1449            1 :     authenticationTypes = possibleAuthenticationTypes;
    1450            2 :     commitment = payload['commitment'];
    1451            2 :     sas = vod.Sas();
    1452              :     return true;
    1453              :   }
    1454              : 
    1455            1 :   Future<void> _sendKey() async {
    1456            3 :     await request.send('m.key.verification.key', {
    1457            2 :       'key': sas!.publicKey,
    1458              :     });
    1459              :   }
    1460              : 
    1461            1 :   void _handleKey(Map<String, dynamic> payload) {
    1462            2 :     theirPublicKey = payload['key'];
    1463            1 :     final sas = this.sas;
    1464            1 :     if (sas == null || sas.disposed) {
    1465            0 :       throw Exception('SAS object is disposed');
    1466              :     }
    1467            3 :     establishedSas = sas.establishSasSecret(payload['key']);
    1468              :   }
    1469              : 
    1470            1 :   Future<bool> _validateCommitment() async {
    1471              :     final checkCommitment =
    1472            3 :         await _makeCommitment(theirPublicKey, startCanonicalJson);
    1473            2 :     return commitment == checkCommitment;
    1474              :   }
    1475              : 
    1476            1 :   Uint8List makeSas(int bytes) {
    1477              :     var sasInfo = '';
    1478            2 :     if (keyAgreementProtocol == 'curve25519-hkdf-sha256') {
    1479            7 :       final ourInfo = '${client.userID}|${client.deviceID}|${sas!.publicKey}|';
    1480              :       final theirInfo =
    1481            6 :           '${request.userId}|${request.deviceId}|$theirPublicKey|';
    1482              :       sasInfo =
    1483            7 :           'MATRIX_KEY_VERIFICATION_SAS|${request.startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo}${request.transactionId!}';
    1484            0 :     } else if (keyAgreementProtocol == 'curve25519') {
    1485            0 :       final ourInfo = client.userID! + client.deviceID!;
    1486            0 :       final theirInfo = request.userId + request.deviceId!;
    1487              :       sasInfo =
    1488            0 :           'MATRIX_KEY_VERIFICATION_SAS${request.startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo}${request.transactionId!}';
    1489              :     } else {
    1490            0 :       throw Exception('Unknown key agreement protocol');
    1491              :     }
    1492            2 :     return establishedSas!.generateBytes(sasInfo, bytes);
    1493              :   }
    1494              : 
    1495            1 :   Future<void> _sendMac() async {
    1496              :     final baseInfo =
    1497           11 :         'MATRIX_KEY_VERIFICATION_MAC${client.userID!}${client.deviceID!}${request.userId}${request.deviceId!}${request.transactionId!}';
    1498            1 :     final mac = <String, String>{};
    1499            1 :     final keyList = <String>[];
    1500              : 
    1501              :     // now add all the keys we want the other to verify
    1502              :     // for now it is just our device key, once we have cross-signing
    1503              :     // we would also add the cross signing key here
    1504            3 :     final deviceKeyId = 'ed25519:${client.deviceID}';
    1505            1 :     mac[deviceKeyId] =
    1506            4 :         _calculateMac(encryption.fingerprintKey!, baseInfo + deviceKeyId);
    1507            1 :     keyList.add(deviceKeyId);
    1508              : 
    1509            6 :     final masterKey = client.userDeviceKeys[client.userID]?.masterKey;
    1510            1 :     if (masterKey != null && masterKey.verified) {
    1511              :       // we have our own master key verified, let's send it!
    1512            2 :       final masterKeyId = 'ed25519:${masterKey.publicKey}';
    1513            1 :       mac[masterKeyId] =
    1514            3 :           _calculateMac(masterKey.publicKey!, baseInfo + masterKeyId);
    1515            1 :       keyList.add(masterKeyId);
    1516              :     }
    1517              : 
    1518            1 :     keyList.sort();
    1519            3 :     final keys = _calculateMac(keyList.join(','), '${baseInfo}KEY_IDS');
    1520            3 :     await request.send('m.key.verification.mac', {
    1521              :       'mac': mac,
    1522              :       'keys': keys,
    1523              :     });
    1524              :   }
    1525              : 
    1526            1 :   Future<void> _processMac() async {
    1527            1 :     final payload = macPayload!;
    1528              :     final baseInfo =
    1529           11 :         'MATRIX_KEY_VERIFICATION_MAC${request.userId}${request.deviceId!}${client.userID!}${client.deviceID!}${request.transactionId!}';
    1530              : 
    1531            3 :     final keyList = payload['mac'].keys.toList();
    1532            1 :     keyList.sort();
    1533            2 :     if (payload['keys'] !=
    1534            3 :         _calculateMac(keyList.join(','), '${baseInfo}KEY_IDS')) {
    1535            0 :       await request.cancel('m.key_mismatch');
    1536              :       return;
    1537              :     }
    1538              : 
    1539            5 :     if (!client.userDeviceKeys.containsKey(request.userId)) {
    1540            0 :       await request.cancel('m.key_mismatch');
    1541              :       return;
    1542              :     }
    1543            1 :     final mac = <String, String>{};
    1544            3 :     for (final entry in payload['mac'].entries) {
    1545            2 :       if (entry.value is String) {
    1546            3 :         mac[entry.key] = entry.value;
    1547              :       }
    1548              :     }
    1549            3 :     await request.verifyKeysSAS(mac, (String mac, SignableKey key) async {
    1550            1 :       return mac ==
    1551            1 :           _calculateMac(
    1552            1 :             key.ed25519Key!,
    1553            2 :             '${baseInfo}ed25519:${key.identifier!}',
    1554              :           );
    1555              :     });
    1556              :   }
    1557              : 
    1558            1 :   Future<String> _makeCommitment(String pubKey, String canonicalJson) async {
    1559            2 :     if (hash == 'sha256') {
    1560            3 :       final bytes = utf8.encoder.convert(pubKey + canonicalJson);
    1561            1 :       final digest = vod.CryptoUtils.sha256(input: bytes);
    1562            1 :       return encodeBase64Unpadded(digest);
    1563              :     }
    1564            0 :     throw Exception('Unknown hash method');
    1565              :   }
    1566              : 
    1567            1 :   String _calculateMac(String input, String info) {
    1568            2 :     if (messageAuthenticationCode == 'hkdf-hmac-sha256.v2') {
    1569            2 :       return establishedSas!.calculateMac(input, info);
    1570            0 :     } else if (messageAuthenticationCode == 'hkdf-hmac-sha256') {
    1571            0 :       return establishedSas!.calculateMacDeprecated(input, info);
    1572              :     } else {
    1573            0 :       throw Exception('Unknown message authentification code');
    1574              :     }
    1575              :   }
    1576              : }
    1577              : 
    1578              : const _emojiMap = [
    1579              :   {
    1580              :     'emoji': '\u{1F436}',
    1581              :     'name': 'Dog',
    1582              :   },
    1583              :   {
    1584              :     'emoji': '\u{1F431}',
    1585              :     'name': 'Cat',
    1586              :   },
    1587              :   {
    1588              :     'emoji': '\u{1F981}',
    1589              :     'name': 'Lion',
    1590              :   },
    1591              :   {
    1592              :     'emoji': '\u{1F40E}',
    1593              :     'name': 'Horse',
    1594              :   },
    1595              :   {
    1596              :     'emoji': '\u{1F984}',
    1597              :     'name': 'Unicorn',
    1598              :   },
    1599              :   {
    1600              :     'emoji': '\u{1F437}',
    1601              :     'name': 'Pig',
    1602              :   },
    1603              :   {
    1604              :     'emoji': '\u{1F418}',
    1605              :     'name': 'Elephant',
    1606              :   },
    1607              :   {
    1608              :     'emoji': '\u{1F430}',
    1609              :     'name': 'Rabbit',
    1610              :   },
    1611              :   {
    1612              :     'emoji': '\u{1F43C}',
    1613              :     'name': 'Panda',
    1614              :   },
    1615              :   {
    1616              :     'emoji': '\u{1F413}',
    1617              :     'name': 'Rooster',
    1618              :   },
    1619              :   {
    1620              :     'emoji': '\u{1F427}',
    1621              :     'name': 'Penguin',
    1622              :   },
    1623              :   {
    1624              :     'emoji': '\u{1F422}',
    1625              :     'name': 'Turtle',
    1626              :   },
    1627              :   {
    1628              :     'emoji': '\u{1F41F}',
    1629              :     'name': 'Fish',
    1630              :   },
    1631              :   {
    1632              :     'emoji': '\u{1F419}',
    1633              :     'name': 'Octopus',
    1634              :   },
    1635              :   {
    1636              :     'emoji': '\u{1F98B}',
    1637              :     'name': 'Butterfly',
    1638              :   },
    1639              :   {
    1640              :     'emoji': '\u{1F337}',
    1641              :     'name': 'Flower',
    1642              :   },
    1643              :   {
    1644              :     'emoji': '\u{1F333}',
    1645              :     'name': 'Tree',
    1646              :   },
    1647              :   {
    1648              :     'emoji': '\u{1F335}',
    1649              :     'name': 'Cactus',
    1650              :   },
    1651              :   {
    1652              :     'emoji': '\u{1F344}',
    1653              :     'name': 'Mushroom',
    1654              :   },
    1655              :   {
    1656              :     'emoji': '\u{1F30F}',
    1657              :     'name': 'Globe',
    1658              :   },
    1659              :   {
    1660              :     'emoji': '\u{1F319}',
    1661              :     'name': 'Moon',
    1662              :   },
    1663              :   {
    1664              :     'emoji': '\u{2601}\u{FE0F}',
    1665              :     'name': 'Cloud',
    1666              :   },
    1667              :   {
    1668              :     'emoji': '\u{1F525}',
    1669              :     'name': 'Fire',
    1670              :   },
    1671              :   {
    1672              :     'emoji': '\u{1F34C}',
    1673              :     'name': 'Banana',
    1674              :   },
    1675              :   {
    1676              :     'emoji': '\u{1F34E}',
    1677              :     'name': 'Apple',
    1678              :   },
    1679              :   {
    1680              :     'emoji': '\u{1F353}',
    1681              :     'name': 'Strawberry',
    1682              :   },
    1683              :   {
    1684              :     'emoji': '\u{1F33D}',
    1685              :     'name': 'Corn',
    1686              :   },
    1687              :   {
    1688              :     'emoji': '\u{1F355}',
    1689              :     'name': 'Pizza',
    1690              :   },
    1691              :   {
    1692              :     'emoji': '\u{1F382}',
    1693              :     'name': 'Cake',
    1694              :   },
    1695              :   {
    1696              :     'emoji': '\u{2764}\u{FE0F}',
    1697              :     'name': 'Heart',
    1698              :   },
    1699              :   {
    1700              :     'emoji': '\u{1F600}',
    1701              :     'name': 'Smiley',
    1702              :   },
    1703              :   {
    1704              :     'emoji': '\u{1F916}',
    1705              :     'name': 'Robot',
    1706              :   },
    1707              :   {
    1708              :     'emoji': '\u{1F3A9}',
    1709              :     'name': 'Hat',
    1710              :   },
    1711              :   {
    1712              :     'emoji': '\u{1F453}',
    1713              :     'name': 'Glasses',
    1714              :   },
    1715              :   {
    1716              :     'emoji': '\u{1F527}',
    1717              :     'name': 'Spanner',
    1718              :   },
    1719              :   {
    1720              :     'emoji': '\u{1F385}',
    1721              :     'name': 'Santa',
    1722              :   },
    1723              :   {
    1724              :     'emoji': '\u{1F44D}',
    1725              :     'name': 'Thumbs Up',
    1726              :   },
    1727              :   {
    1728              :     'emoji': '\u{2602}\u{FE0F}',
    1729              :     'name': 'Umbrella',
    1730              :   },
    1731              :   {
    1732              :     'emoji': '\u{231B}',
    1733              :     'name': 'Hourglass',
    1734              :   },
    1735              :   {
    1736              :     'emoji': '\u{23F0}',
    1737              :     'name': 'Clock',
    1738              :   },
    1739              :   {
    1740              :     'emoji': '\u{1F381}',
    1741              :     'name': 'Gift',
    1742              :   },
    1743              :   {
    1744              :     'emoji': '\u{1F4A1}',
    1745              :     'name': 'Light Bulb',
    1746              :   },
    1747              :   {
    1748              :     'emoji': '\u{1F4D5}',
    1749              :     'name': 'Book',
    1750              :   },
    1751              :   {
    1752              :     'emoji': '\u{270F}\u{FE0F}',
    1753              :     'name': 'Pencil',
    1754              :   },
    1755              :   {
    1756              :     'emoji': '\u{1F4CE}',
    1757              :     'name': 'Paperclip',
    1758              :   },
    1759              :   {
    1760              :     'emoji': '\u{2702}\u{FE0F}',
    1761              :     'name': 'Scissors',
    1762              :   },
    1763              :   {
    1764              :     'emoji': '\u{1F512}',
    1765              :     'name': 'Lock',
    1766              :   },
    1767              :   {
    1768              :     'emoji': '\u{1F511}',
    1769              :     'name': 'Key',
    1770              :   },
    1771              :   {
    1772              :     'emoji': '\u{1F528}',
    1773              :     'name': 'Hammer',
    1774              :   },
    1775              :   {
    1776              :     'emoji': '\u{260E}\u{FE0F}',
    1777              :     'name': 'Telephone',
    1778              :   },
    1779              :   {
    1780              :     'emoji': '\u{1F3C1}',
    1781              :     'name': 'Flag',
    1782              :   },
    1783              :   {
    1784              :     'emoji': '\u{1F682}',
    1785              :     'name': 'Train',
    1786              :   },
    1787              :   {
    1788              :     'emoji': '\u{1F6B2}',
    1789              :     'name': 'Bicycle',
    1790              :   },
    1791              :   {
    1792              :     'emoji': '\u{2708}\u{FE0F}',
    1793              :     'name': 'Aeroplane',
    1794              :   },
    1795              :   {
    1796              :     'emoji': '\u{1F680}',
    1797              :     'name': 'Rocket',
    1798              :   },
    1799              :   {
    1800              :     'emoji': '\u{1F3C6}',
    1801              :     'name': 'Trophy',
    1802              :   },
    1803              :   {
    1804              :     'emoji': '\u{26BD}',
    1805              :     'name': 'Ball',
    1806              :   },
    1807              :   {
    1808              :     'emoji': '\u{1F3B8}',
    1809              :     'name': 'Guitar',
    1810              :   },
    1811              :   {
    1812              :     'emoji': '\u{1F3BA}',
    1813              :     'name': 'Trumpet',
    1814              :   },
    1815              :   {
    1816              :     'emoji': '\u{1F514}',
    1817              :     'name': 'Bell',
    1818              :   },
    1819              :   {
    1820              :     'emoji': '\u{2693}',
    1821              :     'name': 'Anchor',
    1822              :   },
    1823              :   {
    1824              :     'emoji': '\u{1F3A7}',
    1825              :     'name': 'Headphones',
    1826              :   },
    1827              :   {
    1828              :     'emoji': '\u{1F4C1}',
    1829              :     'name': 'Folder',
    1830              :   },
    1831              :   {
    1832              :     'emoji': '\u{1F4CC}',
    1833              :     'name': 'Pin',
    1834              :   },
    1835              : ];
    1836              : 
    1837              : class KeyVerificationEmoji {
    1838              :   final int number;
    1839            1 :   KeyVerificationEmoji(this.number);
    1840              : 
    1841            4 :   String get emoji => _emojiMap[number]['emoji'] ?? '';
    1842            4 :   String get name => _emojiMap[number]['name'] ?? '';
    1843              : }
        

Generated by: LCOV version 2.0-1