LCOV - code coverage report
Current view: top level - lib/encryption/utils - key_verification.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 574 660 87.0 %
Date: 2024-11-12 07:37:08 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 1.14