LCOV - code coverage report
Current view: top level - lib/encryption/utils - key_verification.dart (source / functions) Coverage Total Hit
Test: merged.info Lines: 87.0 % 660 574
Test Date: 2025-01-14 13:39:53 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:olm/olm.dart' as olm;
      25              : import 'package:typed_data/typed_data.dart';
      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              :     // ignore: unawaited_futures
     751            2 :     encryption.ssss
     752            4 :         .maybeRequestAll(_verifiedDevices.whereType<DeviceKeys>().toList());
     753            2 :     if (requestInterval.length <= i) {
     754              :       return;
     755              :     }
     756            1 :     Timer(
     757            2 :       Duration(seconds: requestInterval[i]),
     758            0 :       () => maybeRequestSSSSSecrets(i + 1),
     759              :     );
     760              :   }
     761              : 
     762            1 :   Future<void> verifyKeysSAS(
     763              :     Map<String, String> keys,
     764              :     Future<bool> Function(String, SignableKey) verifier,
     765              :   ) async {
     766            2 :     _verifiedDevices = <SignableKey>[];
     767              : 
     768            4 :     final userDeviceKey = client.userDeviceKeys[userId];
     769              :     if (userDeviceKey == null) {
     770            0 :       await cancel('m.key_mismatch');
     771              :       return;
     772              :     }
     773            2 :     for (final entry in keys.entries) {
     774            1 :       final keyId = entry.key;
     775            2 :       final verifyDeviceId = keyId.substring('ed25519:'.length);
     776            1 :       final keyInfo = entry.value;
     777            1 :       final key = userDeviceKey.getKey(verifyDeviceId);
     778              :       if (key != null) {
     779            1 :         if (!(await verifier(keyInfo, key))) {
     780            0 :           await cancel('m.key_mismatch');
     781              :           return;
     782              :         }
     783            2 :         _verifiedDevices.add(key);
     784              :       }
     785              :     }
     786              :     // okay, we reached this far, so all the devices are verified!
     787              :     var verifiedMasterKey = false;
     788            2 :     final wasUnknownSession = client.isUnknownSession;
     789            2 :     for (final key in _verifiedDevices) {
     790            1 :       await key.setVerified(
     791              :         true,
     792              :         false,
     793              :       ); // we don't want to sign the keys juuuust yet
     794            3 :       if (key is CrossSigningKey && key.usage.contains('master')) {
     795              :         verifiedMasterKey = true;
     796              :       }
     797              :     }
     798            4 :     if (verifiedMasterKey && userId == client.userID) {
     799              :       // it was our own master key, let's request the cross signing keys
     800              :       // we do it in the background, thus no await needed here
     801              :       // ignore: unawaited_futures
     802            0 :       maybeRequestSSSSSecrets();
     803              :     }
     804            2 :     await send(EventTypes.KeyVerificationDone, {});
     805              : 
     806              :     var askingSSSS = false;
     807            3 :     if (encryption.crossSigning.enabled &&
     808            4 :         encryption.crossSigning.signable(_verifiedDevices)) {
     809              :       // these keys can be signed! Let's do so
     810            3 :       if (await encryption.crossSigning.isCached()) {
     811              :         // we want to make sure the verification state is correct for the other party after this event is handled.
     812              :         // Otherwise the verification dialog might be stuck in an unverified but done state for a bit.
     813            0 :         await encryption.crossSigning.sign(_verifiedDevices);
     814              :       } else if (!wasUnknownSession) {
     815              :         askingSSSS = true;
     816              :       }
     817              :     }
     818              :     if (askingSSSS) {
     819            1 :       setState(KeyVerificationState.askSSSS);
     820            1 :       _nextAction = 'done';
     821              :     } else {
     822            1 :       setState(KeyVerificationState.done);
     823              :     }
     824              :   }
     825              : 
     826              :   /// shower is true only for reciprocated verifications (shower side)
     827            2 :   Future<void> verifyKeysQR(SignableKey key, {bool shower = true}) async {
     828              :     var verifiedMasterKey = false;
     829            4 :     final wasUnknownSession = client.isUnknownSession;
     830              : 
     831            2 :     key.setDirectVerified(true);
     832            6 :     if (key is CrossSigningKey && key.usage.contains('master')) {
     833              :       verifiedMasterKey = true;
     834              :     }
     835              : 
     836            8 :     if (verifiedMasterKey && userId == client.userID) {
     837              :       // it was our own master key, let's request the cross signing keys
     838              :       // we do it in the background, thus no await needed here
     839              :       // ignore: unawaited_futures
     840            1 :       maybeRequestSSSSSecrets();
     841              :     }
     842              :     if (shower) {
     843            4 :       await send(EventTypes.KeyVerificationDone, {});
     844              :     }
     845            4 :     final keyList = List<SignableKey>.from([key]);
     846              :     var askingSSSS = false;
     847            6 :     if (encryption.crossSigning.enabled &&
     848            6 :         encryption.crossSigning.signable(keyList)) {
     849              :       // these keys can be signed! Let's do so
     850            6 :       if (await encryption.crossSigning.isCached()) {
     851              :         // we want to make sure the verification state is correct for the other party after this event is handled.
     852              :         // Otherwise the verification dialog might be stuck in an unverified but done state for a bit.
     853            6 :         await encryption.crossSigning.sign(keyList);
     854              :       } else if (!wasUnknownSession) {
     855              :         askingSSSS = true;
     856              :       }
     857              :     }
     858              :     if (askingSSSS) {
     859              :       // no need to worry about shower/scanner here because if scanner was
     860              :       // verified, ssss is already
     861            1 :       setState(KeyVerificationState.askSSSS);
     862              :       if (shower) {
     863            1 :         _nextAction = 'done';
     864              :       } else {
     865            0 :         _nextAction = 'showQRSuccess';
     866              :       }
     867              :     } else {
     868              :       if (shower) {
     869            2 :         setState(KeyVerificationState.done);
     870              :       } else {
     871            2 :         setState(KeyVerificationState.showQRSuccess);
     872              :       }
     873              :     }
     874              :   }
     875              : 
     876            2 :   Future<bool> verifyActivity() async {
     877           10 :     if (lastActivity.add(Duration(minutes: 10)).isAfter(DateTime.now())) {
     878            4 :       lastActivity = DateTime.now();
     879              :       return true;
     880              :     }
     881            0 :     await cancel('m.timeout');
     882              :     return false;
     883              :   }
     884              : 
     885            2 :   Future<bool> verifyLastStep(List<String?> checkLastStep) async {
     886            2 :     if (!(await verifyActivity())) {
     887              :       return false;
     888              :     }
     889            4 :     if (checkLastStep.contains(lastStep)) {
     890              :       return true;
     891              :     }
     892            0 :     Logs().e(
     893            0 :       '[KeyVerificaton] lastStep mismatch cancelling, expected from ${checkLastStep.toString()} was ${lastStep.toString()}',
     894              :     );
     895            0 :     await cancel('m.unexpected_message');
     896              :     return false;
     897              :   }
     898              : 
     899            2 :   Future<void> cancel([String code = 'm.unknown', bool quiet = false]) async {
     900            3 :     if (!quiet && (deviceId != null || room != null)) {
     901            4 :       await send(EventTypes.KeyVerificationCancel, {
     902              :         'reason': code,
     903              :         'code': code,
     904              :       });
     905              :     }
     906            2 :     canceled = true;
     907            2 :     canceledCode = code;
     908            2 :     setState(KeyVerificationState.error);
     909              :   }
     910              : 
     911            3 :   void makePayload(Map<String, dynamic> payload) {
     912            9 :     payload['from_device'] = client.deviceID;
     913            3 :     if (transactionId != null) {
     914            3 :       if (room != null) {
     915            2 :         payload['m.relates_to'] = {
     916              :           'rel_type': 'm.reference',
     917            1 :           'event_id': transactionId,
     918              :         };
     919              :       } else {
     920            4 :         payload['transaction_id'] = transactionId;
     921              :       }
     922              :     }
     923              :   }
     924              : 
     925            3 :   Future<void> send(
     926              :     String type,
     927              :     Map<String, dynamic> payload,
     928              :   ) async {
     929            3 :     makePayload(payload);
     930            9 :     Logs().i('[Key Verification] Sending type $type: $payload');
     931            3 :     if (room != null) {
     932           12 :       Logs().i('[Key Verification] Sending to $userId in room ${room!.id}...');
     933            4 :       if ({EventTypes.KeyVerificationRequest}.contains(type)) {
     934            2 :         payload['msgtype'] = type;
     935            4 :         payload['to'] = userId;
     936            2 :         payload['body'] =
     937            2 :             'Attempting verification request. ($type) Apparently your client doesn\'t support this';
     938              :         type = EventTypes.Message;
     939              :       }
     940            4 :       final newTransactionId = await room!.sendEvent(payload, type: type);
     941            2 :       if (transactionId == null) {
     942            2 :         transactionId = newTransactionId;
     943            6 :         encryption.keyVerificationManager.addRequest(this);
     944              :       }
     945              :     } else {
     946           10 :       Logs().i('[Key Verification] Sending to $userId device $deviceId...');
     947            4 :       if (deviceId == '*') {
     948              :         if ({
     949            1 :           EventTypes.KeyVerificationRequest,
     950            1 :           EventTypes.KeyVerificationCancel,
     951            1 :         }.contains(type)) {
     952              :           final deviceKeys =
     953            7 :               client.userDeviceKeys[userId]?.deviceKeys.values.where(
     954            2 :             (deviceKey) => deviceKey.hasValidSignatureChain(
     955              :               verifiedByTheirMasterKey: true,
     956              :             ),
     957              :           );
     958              : 
     959              :           if (deviceKeys != null) {
     960            2 :             await client.sendToDeviceEncrypted(
     961            1 :               deviceKeys.toList(),
     962              :               type,
     963              :               payload,
     964              :             );
     965              :           }
     966              :         } else {
     967            0 :           Logs().e(
     968            0 :             '[Key Verification] Tried to broadcast and un-broadcastable type: $type',
     969              :           );
     970              :         }
     971              :       } else {
     972           14 :         if (client.userDeviceKeys[userId]?.deviceKeys[deviceId] != null) {
     973            4 :           await client.sendToDeviceEncrypted(
     974           16 :             [client.userDeviceKeys[userId]!.deviceKeys[deviceId]!],
     975              :             type,
     976              :             payload,
     977              :           );
     978              :         } else {
     979            0 :           Logs().e('[Key Verification] Unknown device');
     980              :         }
     981              :       }
     982              :     }
     983              :   }
     984              : 
     985            3 :   void setState(KeyVerificationState newState) {
     986            6 :     if (state != KeyVerificationState.error) {
     987            3 :       state = newState;
     988              :     }
     989              : 
     990            3 :     onUpdate?.call();
     991              :   }
     992              : 
     993              :   static const String prefix = 'MATRIX';
     994              :   static const int version = 0x02;
     995              : 
     996            2 :   Future<bool> verifyQrData(Uint8List qrDataRawBytes) async {
     997              :     final data = qrDataRawBytes;
     998              :     // hardcoded stuff + 2 keys + secret
     999           18 :     if (data.length < 10 + 32 + 32 + 8 + utf8.encode(transactionId!).length) {
    1000              :       return false;
    1001              :     }
    1002            4 :     if (data[6] != version) return false;
    1003              :     final remoteQrMode =
    1004           10 :         QRMode.values.singleWhere((mode) => mode.code == data[7]);
    1005            6 :     if (ascii.decode(data.sublist(0, 6)) != prefix) return false;
    1006            4 :     if (data[6] != version) return false;
    1007            8 :     final tmpBuf = Uint8List.fromList([data[8], data[9]]);
    1008            6 :     final encodedTxnLen = ByteData.view(tmpBuf.buffer).getUint16(0);
    1009           10 :     if (utf8.decode(data.sublist(10, 10 + encodedTxnLen)) != transactionId) {
    1010              :       return false;
    1011              :     }
    1012            4 :     final keys = client.userDeviceKeys;
    1013              : 
    1014            6 :     final ownKeys = keys[client.userID];
    1015            4 :     final otherUserKeys = keys[userId];
    1016            2 :     final ownMasterKey = ownKeys?.getCrossSigningKey('master');
    1017            6 :     final ownDeviceKey = ownKeys?.getKey(client.deviceID!);
    1018            4 :     final ownOtherDeviceKey = ownKeys?.getKey(deviceId!);
    1019            2 :     final otherUserMasterKey = otherUserKeys?.masterKey;
    1020              : 
    1021            2 :     final secondKey = encodeBase64Unpadded(
    1022           12 :       data.sublist(10 + encodedTxnLen + 32, 10 + encodedTxnLen + 32 + 32),
    1023              :     );
    1024              :     final randomSharedSecret =
    1025           10 :         encodeBase64Unpadded(data.sublist(10 + encodedTxnLen + 32 + 32));
    1026              : 
    1027              :     /// `request.randomSharedSecretForQRCode` is overwritten below to send with `sendStart`
    1028            4 :     if ({QRMode.verifyOtherUser, QRMode.verifySelfUntrusted}
    1029            2 :         .contains(remoteQrMode)) {
    1030            2 :       if (!(ownMasterKey?.verified ?? false)) {
    1031            0 :         Logs().e(
    1032              :           '[KeyVerification] verifyQrData because you were in mode 0/2 and had untrusted msk',
    1033              :         );
    1034              :         return false;
    1035              :       }
    1036              :     }
    1037              : 
    1038            2 :     if (remoteQrMode == QRMode.verifyOtherUser &&
    1039              :         otherUserMasterKey != null &&
    1040              :         ownMasterKey != null) {
    1041            2 :       if (secondKey == ownMasterKey.ed25519Key) {
    1042            1 :         randomSharedSecretForQRCode = randomSharedSecret;
    1043            1 :         await verifyKeysQR(otherUserMasterKey, shower: false);
    1044              :         return true;
    1045              :       }
    1046            1 :     } else if (remoteQrMode == QRMode.verifySelfTrusted &&
    1047              :         ownMasterKey != null &&
    1048              :         ownDeviceKey != null) {
    1049            2 :       if (secondKey == ownDeviceKey.ed25519Key) {
    1050            1 :         randomSharedSecretForQRCode = randomSharedSecret;
    1051            1 :         await verifyKeysQR(ownMasterKey, shower: false);
    1052              :         return true;
    1053              :       }
    1054            1 :     } else if (remoteQrMode == QRMode.verifySelfUntrusted &&
    1055              :         ownOtherDeviceKey != null &&
    1056              :         ownMasterKey != null) {
    1057            2 :       if (secondKey == ownMasterKey.ed25519Key) {
    1058            1 :         randomSharedSecretForQRCode = randomSharedSecret;
    1059            1 :         await verifyKeysQR(ownOtherDeviceKey, shower: false);
    1060              :         return true;
    1061              :       }
    1062              :     }
    1063              : 
    1064              :     return false;
    1065              :   }
    1066              : 
    1067            2 :   Future<(String, String)?> getKeys(QRMode mode) async {
    1068            4 :     final keys = client.userDeviceKeys;
    1069              : 
    1070            6 :     final ownKeys = keys[client.userID];
    1071            4 :     final otherUserKeys = keys[userId];
    1072            6 :     final ownDeviceKey = ownKeys?.getKey(client.deviceID!);
    1073            2 :     final ownMasterKey = ownKeys?.getCrossSigningKey('master');
    1074            4 :     final otherDeviceKey = otherUserKeys?.getKey(deviceId!);
    1075            2 :     final otherMasterKey = otherUserKeys?.getCrossSigningKey('master');
    1076              : 
    1077            2 :     if (mode == QRMode.verifyOtherUser &&
    1078              :         ownMasterKey != null &&
    1079              :         otherMasterKey != null) {
    1080              :       // we already have this check when sending `knownVerificationMethods`, but
    1081              :       // just to be safe anyway
    1082            1 :       if (ownMasterKey.verified) {
    1083            2 :         return (ownMasterKey.ed25519Key!, otherMasterKey.ed25519Key!);
    1084              :       }
    1085            1 :     } else if (mode == QRMode.verifySelfTrusted &&
    1086              :         ownMasterKey != null &&
    1087              :         otherDeviceKey != null) {
    1088            1 :       if (ownMasterKey.verified) {
    1089            2 :         return (ownMasterKey.ed25519Key!, otherDeviceKey.ed25519Key!);
    1090              :       }
    1091            1 :     } else if (mode == QRMode.verifySelfUntrusted &&
    1092              :         ownMasterKey != null &&
    1093              :         ownDeviceKey != null) {
    1094            2 :       return (ownDeviceKey.ed25519Key!, ownMasterKey.ed25519Key!);
    1095              :     }
    1096              :     return null;
    1097              :   }
    1098              : 
    1099            2 :   Future<QRCode?> generateQrCode() async {
    1100            2 :     final data = Uint8Buffer();
    1101              :     // why 11? https://github.com/matrix-org/matrix-js-sdk/commit/275ea6aacbfc6623e7559a7649ca5cab207903d9
    1102            2 :     randomSharedSecretForQRCode =
    1103            4 :         encodeBase64Unpadded(uc.secureRandomBytes(11));
    1104              : 
    1105            2 :     final mode = getOurQRMode();
    1106            4 :     data.addAll(ascii.encode(prefix));
    1107            2 :     data.add(version);
    1108            4 :     data.add(mode.code);
    1109            4 :     final encodedTxnId = utf8.encode(transactionId!);
    1110            2 :     final txnIdLen = encodedTxnId.length;
    1111            2 :     final tmpBuf = Uint8List(2);
    1112            6 :     ByteData.view(tmpBuf.buffer).setUint16(0, txnIdLen);
    1113            2 :     data.addAll(tmpBuf);
    1114            2 :     data.addAll(encodedTxnId);
    1115            2 :     final keys = await getKeys(mode);
    1116              :     if (keys != null) {
    1117            4 :       data.addAll(base64decodeUnpadded(keys.$1));
    1118            4 :       data.addAll(base64decodeUnpadded(keys.$2));
    1119              :     } else {
    1120              :       return null;
    1121              :     }
    1122              : 
    1123            6 :     data.addAll(base64decodeUnpadded(randomSharedSecretForQRCode!));
    1124            4 :     return QRCode(randomSharedSecretForQRCode!, data);
    1125              :   }
    1126              : }
    1127              : 
    1128              : abstract class _KeyVerificationMethod {
    1129              :   KeyVerification request;
    1130            3 :   Encryption get encryption => request.encryption;
    1131            6 :   Client get client => request.client;
    1132            2 :   _KeyVerificationMethod({required this.request});
    1133              : 
    1134              :   Future<void> handlePayload(String type, Map<String, dynamic> payload);
    1135            0 :   bool validateStart(Map<String, dynamic> payload) {
    1136              :     return false;
    1137              :   }
    1138              : 
    1139              :   late String _type;
    1140            4 :   String get type => _type;
    1141              : 
    1142              :   Future<void> sendStart();
    1143            0 :   void dispose() {}
    1144              : }
    1145              : 
    1146              : class _KeyVerificationMethodQRReciprocate extends _KeyVerificationMethod {
    1147            2 :   _KeyVerificationMethodQRReciprocate({required super.request});
    1148              : 
    1149              :   @override
    1150              :   // ignore: overridden_fields
    1151              :   final _type = EventTypes.Reciprocate;
    1152              : 
    1153            2 :   @override
    1154              :   bool validateStart(Map<String, dynamic> payload) {
    1155            6 :     if (payload['method'] != type) return false;
    1156            8 :     if (payload['secret'] != request.randomSharedSecretForQRCode) return false;
    1157              :     return true;
    1158              :   }
    1159              : 
    1160            2 :   @override
    1161              :   Future<void> handlePayload(String type, Map<String, dynamic> payload) async {
    1162              :     try {
    1163              :       switch (type) {
    1164            2 :         case EventTypes.KeyVerificationStart:
    1165            6 :           if (!(await request.verifyLastStep([
    1166              :             EventTypes.KeyVerificationReady,
    1167              :             EventTypes.KeyVerificationRequest,
    1168              :           ]))) {
    1169              :             return; // abort
    1170              :           }
    1171            2 :           if (!validateStart(payload)) {
    1172            2 :             await request.cancel('m.invalid_message');
    1173              :             return;
    1174              :           }
    1175            4 :           request.setState(KeyVerificationState.confirmQRScan);
    1176              :           break;
    1177              :       }
    1178              :     } catch (e, s) {
    1179            0 :       Logs().e('[Key Verification Reciprocate] An error occured', e, s);
    1180            0 :       if (request.deviceId != null) {
    1181            0 :         await request.cancel('m.invalid_message');
    1182              :       }
    1183              :     }
    1184              :   }
    1185              : 
    1186            2 :   Future<void> acceptQRScanConfirmation() async {
    1187              :     // secret validation already done in validateStart
    1188              : 
    1189            4 :     final ourQRMode = request.getOurQRMode();
    1190              :     SignableKey? keyToVerify;
    1191              : 
    1192            2 :     if (ourQRMode == QRMode.verifyOtherUser) {
    1193            6 :       keyToVerify = client.userDeviceKeys[request.userId]?.masterKey;
    1194            1 :     } else if (ourQRMode == QRMode.verifySelfTrusted) {
    1195              :       keyToVerify =
    1196            9 :           client.userDeviceKeys[client.userID]?.deviceKeys[request.deviceId];
    1197            1 :     } else if (ourQRMode == QRMode.verifySelfUntrusted) {
    1198            6 :       keyToVerify = client.userDeviceKeys[client.userID]?.masterKey;
    1199              :     }
    1200              :     if (keyToVerify != null) {
    1201            4 :       await request.verifyKeysQR(keyToVerify, shower: true);
    1202              :     } else {
    1203            0 :       Logs().e('[KeyVerification], verifying keys failed');
    1204            0 :       await request.cancel('m.invalid_key');
    1205              :     }
    1206              :   }
    1207              : 
    1208            2 :   @override
    1209              :   Future<void> sendStart() async {
    1210            2 :     final payload = <String, dynamic>{
    1211            2 :       'method': type,
    1212            4 :       'secret': request.randomSharedSecretForQRCode,
    1213              :     };
    1214            4 :     request.makePayload(payload);
    1215            4 :     await request.send(EventTypes.KeyVerificationStart, payload);
    1216              :   }
    1217              : 
    1218            2 :   @override
    1219              :   void dispose() {}
    1220              : }
    1221              : 
    1222              : enum QRMode {
    1223              :   verifyOtherUser(0x00),
    1224              :   verifySelfTrusted(0x01),
    1225              :   verifySelfUntrusted(0x02);
    1226              : 
    1227              :   const QRMode(this.code);
    1228              :   final int code;
    1229              : }
    1230              : 
    1231              : class QRCode {
    1232              :   /// You actually never need this when implementing in a client, its just to
    1233              :   /// make tests easier. Just pass `qrDataRawBytes` in `continueVerifcation()`
    1234              :   final String randomSharedSecret;
    1235              :   final Uint8Buffer qrDataRawBytes;
    1236            2 :   QRCode(this.randomSharedSecret, this.qrDataRawBytes);
    1237              : }
    1238              : 
    1239              : const knownKeyAgreementProtocols = ['curve25519-hkdf-sha256', 'curve25519'];
    1240              : const knownHashes = ['sha256'];
    1241              : const knownHashesAuthentificationCodes = ['hkdf-hmac-sha256'];
    1242              : 
    1243              : class _KeyVerificationMethodSas extends _KeyVerificationMethod {
    1244            2 :   _KeyVerificationMethodSas({required super.request});
    1245              : 
    1246              :   @override
    1247              :   // ignore: overridden_fields
    1248              :   final _type = EventTypes.Sas;
    1249              : 
    1250              :   String? keyAgreementProtocol;
    1251              :   String? hash;
    1252              :   String? messageAuthenticationCode;
    1253              :   List<String>? authenticationTypes;
    1254              :   late String startCanonicalJson;
    1255              :   String? commitment;
    1256              :   late String theirPublicKey;
    1257              :   Map<String, dynamic>? macPayload;
    1258              :   olm.SAS? sas;
    1259              : 
    1260            2 :   @override
    1261              :   void dispose() {
    1262            3 :     sas?.free();
    1263              :   }
    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            1 :             if (!_validateCommitment()) {
    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            2 :     messageAuthenticationCode = possibleMessageAuthenticationCodes.first;
    1400            1 :     final possibleAuthenticationTypes = _intersect(
    1401            1 :       knownAuthentificationTypes,
    1402            1 :       payload['short_authentication_string'],
    1403              :     );
    1404            1 :     if (possibleAuthenticationTypes.isEmpty) {
    1405              :       return false;
    1406              :     }
    1407            1 :     authenticationTypes = possibleAuthenticationTypes;
    1408            3 :     startCanonicalJson = String.fromCharCodes(canonicalJson.encode(payload));
    1409              :     return true;
    1410              :   }
    1411              : 
    1412            1 :   Future<void> _sendAccept() async {
    1413            2 :     final sas = this.sas = olm.SAS();
    1414            4 :     commitment = _makeCommitment(sas.get_pubkey(), startCanonicalJson);
    1415            3 :     await request.send(EventTypes.KeyVerificationAccept, {
    1416            1 :       'method': type,
    1417            1 :       'key_agreement_protocol': keyAgreementProtocol,
    1418            1 :       'hash': hash,
    1419            1 :       'message_authentication_code': messageAuthenticationCode,
    1420            1 :       'short_authentication_string': authenticationTypes,
    1421            1 :       'commitment': commitment,
    1422              :     });
    1423              :   }
    1424              : 
    1425            1 :   bool _handleAccept(Map<String, dynamic> payload) {
    1426              :     if (!knownKeyAgreementProtocols
    1427            2 :         .contains(payload['key_agreement_protocol'])) {
    1428              :       return false;
    1429              :     }
    1430            2 :     keyAgreementProtocol = payload['key_agreement_protocol'];
    1431            2 :     if (!knownHashes.contains(payload['hash'])) {
    1432              :       return false;
    1433              :     }
    1434            2 :     hash = payload['hash'];
    1435              :     if (!knownHashesAuthentificationCodes
    1436            2 :         .contains(payload['message_authentication_code'])) {
    1437              :       return false;
    1438              :     }
    1439            2 :     messageAuthenticationCode = payload['message_authentication_code'];
    1440            1 :     final possibleAuthenticationTypes = _intersect(
    1441            1 :       knownAuthentificationTypes,
    1442            1 :       payload['short_authentication_string'],
    1443              :     );
    1444            1 :     if (possibleAuthenticationTypes.isEmpty) {
    1445              :       return false;
    1446              :     }
    1447            1 :     authenticationTypes = possibleAuthenticationTypes;
    1448            2 :     commitment = payload['commitment'];
    1449            2 :     sas = olm.SAS();
    1450              :     return true;
    1451              :   }
    1452              : 
    1453            1 :   Future<void> _sendKey() async {
    1454            3 :     await request.send('m.key.verification.key', {
    1455            2 :       'key': sas!.get_pubkey(),
    1456              :     });
    1457              :   }
    1458              : 
    1459            1 :   void _handleKey(Map<String, dynamic> payload) {
    1460            2 :     theirPublicKey = payload['key'];
    1461            3 :     sas!.set_their_key(payload['key']);
    1462              :   }
    1463              : 
    1464            1 :   bool _validateCommitment() {
    1465            3 :     final checkCommitment = _makeCommitment(theirPublicKey, startCanonicalJson);
    1466            2 :     return commitment == checkCommitment;
    1467              :   }
    1468              : 
    1469            1 :   Uint8List makeSas(int bytes) {
    1470              :     var sasInfo = '';
    1471            2 :     if (keyAgreementProtocol == 'curve25519-hkdf-sha256') {
    1472              :       final ourInfo =
    1473            7 :           '${client.userID}|${client.deviceID}|${sas!.get_pubkey()}|';
    1474              :       final theirInfo =
    1475            6 :           '${request.userId}|${request.deviceId}|$theirPublicKey|';
    1476              :       sasInfo =
    1477            7 :           'MATRIX_KEY_VERIFICATION_SAS|${request.startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo}${request.transactionId!}';
    1478            0 :     } else if (keyAgreementProtocol == 'curve25519') {
    1479            0 :       final ourInfo = client.userID! + client.deviceID!;
    1480            0 :       final theirInfo = request.userId + request.deviceId!;
    1481              :       sasInfo =
    1482            0 :           'MATRIX_KEY_VERIFICATION_SAS${request.startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo}${request.transactionId!}';
    1483              :     } else {
    1484            0 :       throw Exception('Unknown key agreement protocol');
    1485              :     }
    1486            2 :     return sas!.generate_bytes(sasInfo, bytes);
    1487              :   }
    1488              : 
    1489            1 :   Future<void> _sendMac() async {
    1490              :     final baseInfo =
    1491           11 :         'MATRIX_KEY_VERIFICATION_MAC${client.userID!}${client.deviceID!}${request.userId}${request.deviceId!}${request.transactionId!}';
    1492            1 :     final mac = <String, String>{};
    1493            1 :     final keyList = <String>[];
    1494              : 
    1495              :     // now add all the keys we want the other to verify
    1496              :     // for now it is just our device key, once we have cross-signing
    1497              :     // we would also add the cross signing key here
    1498            3 :     final deviceKeyId = 'ed25519:${client.deviceID}';
    1499            1 :     mac[deviceKeyId] =
    1500            4 :         _calculateMac(encryption.fingerprintKey!, baseInfo + deviceKeyId);
    1501            1 :     keyList.add(deviceKeyId);
    1502              : 
    1503            6 :     final masterKey = client.userDeviceKeys[client.userID]?.masterKey;
    1504            1 :     if (masterKey != null && masterKey.verified) {
    1505              :       // we have our own master key verified, let's send it!
    1506            2 :       final masterKeyId = 'ed25519:${masterKey.publicKey}';
    1507            1 :       mac[masterKeyId] =
    1508            3 :           _calculateMac(masterKey.publicKey!, baseInfo + masterKeyId);
    1509            1 :       keyList.add(masterKeyId);
    1510              :     }
    1511              : 
    1512            1 :     keyList.sort();
    1513            3 :     final keys = _calculateMac(keyList.join(','), '${baseInfo}KEY_IDS');
    1514            3 :     await request.send('m.key.verification.mac', {
    1515              :       'mac': mac,
    1516              :       'keys': keys,
    1517              :     });
    1518              :   }
    1519              : 
    1520            1 :   Future<void> _processMac() async {
    1521            1 :     final payload = macPayload!;
    1522              :     final baseInfo =
    1523           11 :         'MATRIX_KEY_VERIFICATION_MAC${request.userId}${request.deviceId!}${client.userID!}${client.deviceID!}${request.transactionId!}';
    1524              : 
    1525            3 :     final keyList = payload['mac'].keys.toList();
    1526            1 :     keyList.sort();
    1527            2 :     if (payload['keys'] !=
    1528            3 :         _calculateMac(keyList.join(','), '${baseInfo}KEY_IDS')) {
    1529            0 :       await request.cancel('m.key_mismatch');
    1530              :       return;
    1531              :     }
    1532              : 
    1533            5 :     if (!client.userDeviceKeys.containsKey(request.userId)) {
    1534            0 :       await request.cancel('m.key_mismatch');
    1535              :       return;
    1536              :     }
    1537            1 :     final mac = <String, String>{};
    1538            3 :     for (final entry in payload['mac'].entries) {
    1539            2 :       if (entry.value is String) {
    1540            3 :         mac[entry.key] = entry.value;
    1541              :       }
    1542              :     }
    1543            3 :     await request.verifyKeysSAS(mac, (String mac, SignableKey key) async {
    1544            1 :       return mac ==
    1545            1 :           _calculateMac(
    1546            1 :             key.ed25519Key!,
    1547            2 :             '${baseInfo}ed25519:${key.identifier!}',
    1548              :           );
    1549              :     });
    1550              :   }
    1551              : 
    1552            1 :   String _makeCommitment(String pubKey, String canonicalJson) {
    1553            2 :     if (hash == 'sha256') {
    1554            1 :       final olmutil = olm.Utility();
    1555            2 :       final ret = olmutil.sha256(pubKey + canonicalJson);
    1556            1 :       olmutil.free();
    1557              :       return ret;
    1558              :     }
    1559            0 :     throw Exception('Unknown hash method');
    1560              :   }
    1561              : 
    1562            1 :   String _calculateMac(String input, String info) {
    1563            2 :     if (messageAuthenticationCode == 'hkdf-hmac-sha256') {
    1564            2 :       return sas!.calculate_mac(input, info);
    1565              :     } else {
    1566            0 :       throw Exception('Unknown message authentification code');
    1567              :     }
    1568              :   }
    1569              : }
    1570              : 
    1571              : const _emojiMap = [
    1572              :   {
    1573              :     'emoji': '\u{1F436}',
    1574              :     'name': 'Dog',
    1575              :   },
    1576              :   {
    1577              :     'emoji': '\u{1F431}',
    1578              :     'name': 'Cat',
    1579              :   },
    1580              :   {
    1581              :     'emoji': '\u{1F981}',
    1582              :     'name': 'Lion',
    1583              :   },
    1584              :   {
    1585              :     'emoji': '\u{1F40E}',
    1586              :     'name': 'Horse',
    1587              :   },
    1588              :   {
    1589              :     'emoji': '\u{1F984}',
    1590              :     'name': 'Unicorn',
    1591              :   },
    1592              :   {
    1593              :     'emoji': '\u{1F437}',
    1594              :     'name': 'Pig',
    1595              :   },
    1596              :   {
    1597              :     'emoji': '\u{1F418}',
    1598              :     'name': 'Elephant',
    1599              :   },
    1600              :   {
    1601              :     'emoji': '\u{1F430}',
    1602              :     'name': 'Rabbit',
    1603              :   },
    1604              :   {
    1605              :     'emoji': '\u{1F43C}',
    1606              :     'name': 'Panda',
    1607              :   },
    1608              :   {
    1609              :     'emoji': '\u{1F413}',
    1610              :     'name': 'Rooster',
    1611              :   },
    1612              :   {
    1613              :     'emoji': '\u{1F427}',
    1614              :     'name': 'Penguin',
    1615              :   },
    1616              :   {
    1617              :     'emoji': '\u{1F422}',
    1618              :     'name': 'Turtle',
    1619              :   },
    1620              :   {
    1621              :     'emoji': '\u{1F41F}',
    1622              :     'name': 'Fish',
    1623              :   },
    1624              :   {
    1625              :     'emoji': '\u{1F419}',
    1626              :     'name': 'Octopus',
    1627              :   },
    1628              :   {
    1629              :     'emoji': '\u{1F98B}',
    1630              :     'name': 'Butterfly',
    1631              :   },
    1632              :   {
    1633              :     'emoji': '\u{1F337}',
    1634              :     'name': 'Flower',
    1635              :   },
    1636              :   {
    1637              :     'emoji': '\u{1F333}',
    1638              :     'name': 'Tree',
    1639              :   },
    1640              :   {
    1641              :     'emoji': '\u{1F335}',
    1642              :     'name': 'Cactus',
    1643              :   },
    1644              :   {
    1645              :     'emoji': '\u{1F344}',
    1646              :     'name': 'Mushroom',
    1647              :   },
    1648              :   {
    1649              :     'emoji': '\u{1F30F}',
    1650              :     'name': 'Globe',
    1651              :   },
    1652              :   {
    1653              :     'emoji': '\u{1F319}',
    1654              :     'name': 'Moon',
    1655              :   },
    1656              :   {
    1657              :     'emoji': '\u{2601}\u{FE0F}',
    1658              :     'name': 'Cloud',
    1659              :   },
    1660              :   {
    1661              :     'emoji': '\u{1F525}',
    1662              :     'name': 'Fire',
    1663              :   },
    1664              :   {
    1665              :     'emoji': '\u{1F34C}',
    1666              :     'name': 'Banana',
    1667              :   },
    1668              :   {
    1669              :     'emoji': '\u{1F34E}',
    1670              :     'name': 'Apple',
    1671              :   },
    1672              :   {
    1673              :     'emoji': '\u{1F353}',
    1674              :     'name': 'Strawberry',
    1675              :   },
    1676              :   {
    1677              :     'emoji': '\u{1F33D}',
    1678              :     'name': 'Corn',
    1679              :   },
    1680              :   {
    1681              :     'emoji': '\u{1F355}',
    1682              :     'name': 'Pizza',
    1683              :   },
    1684              :   {
    1685              :     'emoji': '\u{1F382}',
    1686              :     'name': 'Cake',
    1687              :   },
    1688              :   {
    1689              :     'emoji': '\u{2764}\u{FE0F}',
    1690              :     'name': 'Heart',
    1691              :   },
    1692              :   {
    1693              :     'emoji': '\u{1F600}',
    1694              :     'name': 'Smiley',
    1695              :   },
    1696              :   {
    1697              :     'emoji': '\u{1F916}',
    1698              :     'name': 'Robot',
    1699              :   },
    1700              :   {
    1701              :     'emoji': '\u{1F3A9}',
    1702              :     'name': 'Hat',
    1703              :   },
    1704              :   {
    1705              :     'emoji': '\u{1F453}',
    1706              :     'name': 'Glasses',
    1707              :   },
    1708              :   {
    1709              :     'emoji': '\u{1F527}',
    1710              :     'name': 'Spanner',
    1711              :   },
    1712              :   {
    1713              :     'emoji': '\u{1F385}',
    1714              :     'name': 'Santa',
    1715              :   },
    1716              :   {
    1717              :     'emoji': '\u{1F44D}',
    1718              :     'name': 'Thumbs Up',
    1719              :   },
    1720              :   {
    1721              :     'emoji': '\u{2602}\u{FE0F}',
    1722              :     'name': 'Umbrella',
    1723              :   },
    1724              :   {
    1725              :     'emoji': '\u{231B}',
    1726              :     'name': 'Hourglass',
    1727              :   },
    1728              :   {
    1729              :     'emoji': '\u{23F0}',
    1730              :     'name': 'Clock',
    1731              :   },
    1732              :   {
    1733              :     'emoji': '\u{1F381}',
    1734              :     'name': 'Gift',
    1735              :   },
    1736              :   {
    1737              :     'emoji': '\u{1F4A1}',
    1738              :     'name': 'Light Bulb',
    1739              :   },
    1740              :   {
    1741              :     'emoji': '\u{1F4D5}',
    1742              :     'name': 'Book',
    1743              :   },
    1744              :   {
    1745              :     'emoji': '\u{270F}\u{FE0F}',
    1746              :     'name': 'Pencil',
    1747              :   },
    1748              :   {
    1749              :     'emoji': '\u{1F4CE}',
    1750              :     'name': 'Paperclip',
    1751              :   },
    1752              :   {
    1753              :     'emoji': '\u{2702}\u{FE0F}',
    1754              :     'name': 'Scissors',
    1755              :   },
    1756              :   {
    1757              :     'emoji': '\u{1F512}',
    1758              :     'name': 'Lock',
    1759              :   },
    1760              :   {
    1761              :     'emoji': '\u{1F511}',
    1762              :     'name': 'Key',
    1763              :   },
    1764              :   {
    1765              :     'emoji': '\u{1F528}',
    1766              :     'name': 'Hammer',
    1767              :   },
    1768              :   {
    1769              :     'emoji': '\u{260E}\u{FE0F}',
    1770              :     'name': 'Telephone',
    1771              :   },
    1772              :   {
    1773              :     'emoji': '\u{1F3C1}',
    1774              :     'name': 'Flag',
    1775              :   },
    1776              :   {
    1777              :     'emoji': '\u{1F682}',
    1778              :     'name': 'Train',
    1779              :   },
    1780              :   {
    1781              :     'emoji': '\u{1F6B2}',
    1782              :     'name': 'Bicycle',
    1783              :   },
    1784              :   {
    1785              :     'emoji': '\u{2708}\u{FE0F}',
    1786              :     'name': 'Aeroplane',
    1787              :   },
    1788              :   {
    1789              :     'emoji': '\u{1F680}',
    1790              :     'name': 'Rocket',
    1791              :   },
    1792              :   {
    1793              :     'emoji': '\u{1F3C6}',
    1794              :     'name': 'Trophy',
    1795              :   },
    1796              :   {
    1797              :     'emoji': '\u{26BD}',
    1798              :     'name': 'Ball',
    1799              :   },
    1800              :   {
    1801              :     'emoji': '\u{1F3B8}',
    1802              :     'name': 'Guitar',
    1803              :   },
    1804              :   {
    1805              :     'emoji': '\u{1F3BA}',
    1806              :     'name': 'Trumpet',
    1807              :   },
    1808              :   {
    1809              :     'emoji': '\u{1F514}',
    1810              :     'name': 'Bell',
    1811              :   },
    1812              :   {
    1813              :     'emoji': '\u{2693}',
    1814              :     'name': 'Anchor',
    1815              :   },
    1816              :   {
    1817              :     'emoji': '\u{1F3A7}',
    1818              :     'name': 'Headphones',
    1819              :   },
    1820              :   {
    1821              :     'emoji': '\u{1F4C1}',
    1822              :     'name': 'Folder',
    1823              :   },
    1824              :   {
    1825              :     'emoji': '\u{1F4CC}',
    1826              :     'name': 'Pin',
    1827              :   },
    1828              : ];
    1829              : 
    1830              : class KeyVerificationEmoji {
    1831              :   final int number;
    1832            1 :   KeyVerificationEmoji(this.number);
    1833              : 
    1834            4 :   String get emoji => _emojiMap[number]['emoji'] ?? '';
    1835            4 :   String get name => _emojiMap[number]['name'] ?? '';
    1836              : }
        

Generated by: LCOV version 2.0-1