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

Generated by: LCOV version 1.14