LCOV - code coverage report
Current view: top level - lib/src/utils - device_keys_list.dart (source / functions) Coverage Total Hit
Test: merged.info Lines: 94.0 % 248 233
Test Date: 2025-10-13 02:23:18 Functions: - 0 0

            Line data    Source code
       1              : /*
       2              :  *   Famedly Matrix SDK
       3              :  *   Copyright (C) 2020, 2021 Famedly GmbH
       4              :  *
       5              :  *   This program is free software: you can redistribute it and/or modify
       6              :  *   it under the terms of the GNU Affero General Public License as
       7              :  *   published by the Free Software Foundation, either version 3 of the
       8              :  *   License, or (at your option) any later version.
       9              :  *
      10              :  *   This program is distributed in the hope that it will be useful,
      11              :  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
      12              :  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      13              :  *   GNU Affero General Public License for more details.
      14              :  *
      15              :  *   You should have received a copy of the GNU Affero General Public License
      16              :  *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
      17              :  */
      18              : 
      19              : import 'dart:convert';
      20              : 
      21              : import 'package:canonical_json/canonical_json.dart';
      22              : import 'package:collection/collection.dart' show IterableExtension;
      23              : import 'package:vodozemac/vodozemac.dart' as vod;
      24              : 
      25              : import 'package:matrix/encryption.dart';
      26              : import 'package:matrix/matrix.dart';
      27              : 
      28              : enum UserVerifiedStatus { verified, unknown, unknownDevice }
      29              : 
      30              : class DeviceKeysList {
      31              :   Client client;
      32              :   String userId;
      33              :   bool outdated = true;
      34              :   Map<String, DeviceKeys> deviceKeys = {};
      35              :   Map<String, CrossSigningKey> crossSigningKeys = {};
      36              : 
      37           13 :   SignableKey? getKey(String id) => deviceKeys[id] ?? crossSigningKeys[id];
      38              : 
      39           33 :   CrossSigningKey? getCrossSigningKey(String type) => crossSigningKeys.values
      40           38 :       .firstWhereOrNull((key) => key.usage.contains(type));
      41              : 
      42           22 :   CrossSigningKey? get masterKey => getCrossSigningKey('master');
      43           12 :   CrossSigningKey? get selfSigningKey => getCrossSigningKey('self_signing');
      44            8 :   CrossSigningKey? get userSigningKey => getCrossSigningKey('user_signing');
      45              : 
      46            2 :   UserVerifiedStatus get verified {
      47            2 :     if (masterKey == null) {
      48              :       return UserVerifiedStatus.unknown;
      49              :     }
      50            4 :     if (masterKey!.verified) {
      51            3 :       for (final key in deviceKeys.values) {
      52            1 :         if (!key.verified) {
      53              :           return UserVerifiedStatus.unknownDevice;
      54              :         }
      55              :       }
      56              :       return UserVerifiedStatus.verified;
      57              :     } else {
      58            6 :       for (final key in deviceKeys.values) {
      59            2 :         if (!key.verified) {
      60              :           return UserVerifiedStatus.unknown;
      61              :         }
      62              :       }
      63              :       return UserVerifiedStatus.verified;
      64              :     }
      65              :   }
      66              : 
      67              :   /// Starts a verification with this device. This might need to create a new
      68              :   /// direct chat to send the verification request over this room. For this you
      69              :   /// can set parameters here.
      70            3 :   Future<KeyVerification> startVerification({
      71              :     bool? newDirectChatEnableEncryption,
      72              :     List<StateEvent>? newDirectChatInitialState,
      73              :   }) async {
      74            6 :     final encryption = client.encryption;
      75              :     if (encryption == null) {
      76            0 :       throw Exception('Encryption not enabled');
      77              :     }
      78           12 :     if (userId != client.userID) {
      79              :       // in-room verification with someone else
      80              :       Room? room;
      81              :       // we check if there's already a direct chat with the user
      82           13 :       for (final directChatRoomId in client.directChats[userId] ?? []) {
      83            2 :         final tempRoom = client.getRoomById(directChatRoomId);
      84              :         if (tempRoom != null &&
      85              :             // check if the room is a direct chat and has less than 2 members
      86              :             // (including the invited users)
      87            3 :             (tempRoom.summary.mInvitedMemberCount ?? 0) +
      88            3 :                     (tempRoom.summary.mJoinedMemberCount ?? 1) <=
      89              :                 2) {
      90              :           // Now we check if the users in the room are none other than the current
      91              :           // user and the user we want to verify
      92            2 :           final members = tempRoom.getParticipants([
      93              :             Membership.invite,
      94              :             Membership.join,
      95              :           ]);
      96            7 :           if (members.every((m) => {userId, client.userID}.contains(m.id))) {
      97              :             // if so, we use that room
      98              :             room = tempRoom;
      99              :             break;
     100              :           }
     101              :         }
     102              :       }
     103              :       // if there's no direct chat that satisfies the conditions, we create a new one
     104              :       if (room == null) {
     105            4 :         final newRoomId = await client.startDirectChat(
     106            2 :           userId,
     107              :           enableEncryption: newDirectChatEnableEncryption,
     108              :           initialState: newDirectChatInitialState,
     109              :           waitForSync: false,
     110              :           skipExistingChat: true, // to create a new room directly
     111              :         );
     112            4 :         room = client.getRoomById(newRoomId) ??
     113            4 :             Room(id: newRoomId, client: client);
     114              :       }
     115              : 
     116              :       final request =
     117            4 :           KeyVerification(encryption: encryption, room: room, userId: userId);
     118            2 :       await request.start();
     119              :       // no need to add to the request client object. As we are doing a room
     120              :       // verification request that'll happen automatically once we know the transaction id
     121              :       return request;
     122              :     } else {
     123              :       // start verification with verified devices
     124            1 :       final request = KeyVerification(
     125              :         encryption: encryption,
     126            1 :         userId: userId,
     127              :         deviceId: '*',
     128              :       );
     129            1 :       await request.start();
     130            2 :       encryption.keyVerificationManager.addRequest(request);
     131              :       return request;
     132              :     }
     133              :   }
     134              : 
     135            2 :   DeviceKeysList.fromDbJson(
     136              :     Map<String, dynamic> dbEntry,
     137              :     List<Map<String, dynamic>> childEntries,
     138              :     List<Map<String, dynamic>> crossSigningEntries,
     139              :     this.client,
     140            2 :   ) : userId = dbEntry['user_id'] ?? '' {
     141            4 :     outdated = dbEntry['outdated'];
     142            4 :     deviceKeys = {};
     143            3 :     for (final childEntry in childEntries) {
     144              :       try {
     145            2 :         final entry = DeviceKeys.fromDb(childEntry, client);
     146            1 :         if (!entry.isValid) throw Exception('Invalid device keys');
     147            3 :         deviceKeys[childEntry['device_id']] = entry;
     148              :       } catch (e, s) {
     149            0 :         Logs().w('Skipping invalid user device key', e, s);
     150            0 :         outdated = true;
     151              :       }
     152              :     }
     153            4 :     for (final crossSigningEntry in crossSigningEntries) {
     154              :       try {
     155            4 :         final entry = CrossSigningKey.fromDbJson(crossSigningEntry, client);
     156            2 :         if (!entry.isValid) throw Exception('Invalid device keys');
     157            6 :         crossSigningKeys[crossSigningEntry['public_key']] = entry;
     158              :       } catch (e, s) {
     159            0 :         Logs().w('Skipping invalid cross siging key', e, s);
     160            0 :         outdated = true;
     161              :       }
     162              :     }
     163              :   }
     164              : 
     165           40 :   DeviceKeysList(this.userId, this.client);
     166              : }
     167              : 
     168              : class SimpleSignableKey extends MatrixSignableKey {
     169              :   @override
     170              :   String? identifier;
     171              : 
     172            7 :   SimpleSignableKey.fromJson(Map<String, dynamic> super.json)
     173            7 :       : super.fromJson();
     174              : }
     175              : 
     176              : abstract class SignableKey extends MatrixSignableKey {
     177              :   Client client;
     178              :   Map<String, dynamic>? validSignatures;
     179              :   bool? _verified;
     180              :   bool? _blocked;
     181              : 
     182          200 :   String? get ed25519Key => keys['ed25519:$identifier'];
     183            8 :   bool get verified =>
     184           30 :       identifier != null && (directVerified || crossVerified) && !(blocked);
     185           80 :   bool get blocked => _blocked ?? false;
     186            6 :   set blocked(bool isBlocked) => _blocked = isBlocked;
     187              : 
     188            5 :   bool get encryptToDevice {
     189            5 :     if (blocked) return false;
     190              : 
     191           10 :     if (identifier == null || ed25519Key == null) return false;
     192              : 
     193           10 :     switch (client.shareKeysWith) {
     194            5 :       case ShareKeysWith.all:
     195              :         return true;
     196            5 :       case ShareKeysWith.crossVerifiedIfEnabled:
     197           24 :         if (client.userDeviceKeys[userId]?.masterKey == null) return true;
     198            2 :         return hasValidSignatureChain(verifiedByTheirMasterKey: true);
     199            1 :       case ShareKeysWith.crossVerified:
     200            1 :         return hasValidSignatureChain(verifiedByTheirMasterKey: true);
     201            1 :       case ShareKeysWith.directlyVerifiedOnly:
     202            1 :         return directVerified;
     203              :     }
     204              :   }
     205              : 
     206           27 :   void setDirectVerified(bool isVerified) {
     207           27 :     _verified = isVerified;
     208              :   }
     209              : 
     210           80 :   bool get directVerified => _verified ?? false;
     211           14 :   bool get crossVerified => hasValidSignatureChain();
     212           22 :   bool get signed => hasValidSignatureChain(verifiedOnly: false);
     213              : 
     214           40 :   SignableKey.fromJson(Map<String, dynamic> super.json, this.client)
     215           40 :       : super.fromJson() {
     216           40 :     _verified = false;
     217           40 :     _blocked = false;
     218              :   }
     219              : 
     220            7 :   SimpleSignableKey cloneForSigning() {
     221           21 :     final newKey = SimpleSignableKey.fromJson(toJson().copy());
     222           14 :     newKey.identifier = identifier;
     223           14 :     (newKey.signatures ??= {}).clear();
     224              :     return newKey;
     225              :   }
     226              : 
     227           28 :   String get signingContent {
     228           56 :     final data = super.toJson().copy();
     229              :     // some old data might have the custom verified and blocked keys
     230           28 :     data.remove('verified');
     231           28 :     data.remove('blocked');
     232              :     // remove the keys not needed for signing
     233           28 :     data.remove('unsigned');
     234           28 :     data.remove('signatures');
     235           56 :     return String.fromCharCodes(canonicalJson.encode(data));
     236              :   }
     237              : 
     238           40 :   bool _verifySignature(
     239              :     String pubKey,
     240              :     String signature, {
     241              :     bool isSignatureWithoutLibolmValid = false,
     242              :   }) {
     243              :     var valid = false;
     244              :     try {
     245           68 :       vod.Ed25519PublicKey.fromBase64(pubKey).verify(
     246           28 :         message: signingContent,
     247           28 :         signature: vod.Ed25519Signature.fromBase64(signature),
     248              :       );
     249              :       valid = true;
     250              :     } catch (e) {
     251           26 :       Logs().d('Invalid Ed25519 signature', e);
     252              :       // bad signature
     253              :       valid = false;
     254              :     }
     255              :     return valid;
     256              :   }
     257              : 
     258           12 :   bool hasValidSignatureChain({
     259              :     bool verifiedOnly = true,
     260              :     Set<String>? visited,
     261              :     Set<String>? onlyValidateUserIds,
     262              : 
     263              :     /// Only check if this key is verified by their Master key.
     264              :     bool verifiedByTheirMasterKey = false,
     265              :   }) {
     266           24 :     if (!client.encryptionEnabled) {
     267              :       return false;
     268              :     }
     269              : 
     270              :     final visited_ = visited ?? <String>{};
     271              :     final onlyValidateUserIds_ = onlyValidateUserIds ?? <String>{};
     272              : 
     273           36 :     final setKey = '$userId;$identifier';
     274           12 :     if (visited_.contains(setKey) ||
     275           12 :         (onlyValidateUserIds_.isNotEmpty &&
     276            0 :             !onlyValidateUserIds_.contains(userId))) {
     277              :       return false; // prevent recursion & validate hasValidSignatureChain
     278              :     }
     279           12 :     visited_.add(setKey);
     280              : 
     281           12 :     if (signatures == null) return false;
     282              : 
     283           36 :     for (final signatureEntries in signatures!.entries) {
     284           12 :       final otherUserId = signatureEntries.key;
     285           36 :       if (!client.userDeviceKeys.containsKey(otherUserId)) {
     286              :         continue;
     287              :       }
     288              :       // we don't allow transitive trust unless it is for ourself
     289           24 :       if (otherUserId != userId && otherUserId != client.userID) {
     290              :         continue;
     291              :       }
     292           36 :       for (final signatureEntry in signatureEntries.value.entries) {
     293           12 :         final fullKeyId = signatureEntry.key;
     294           12 :         final signature = signatureEntry.value;
     295           24 :         final keyId = fullKeyId.substring('ed25519:'.length);
     296              :         // we ignore self-signatures here
     297           48 :         if (otherUserId == userId && keyId == identifier) {
     298              :           continue;
     299              :         }
     300              : 
     301           60 :         final key = client.userDeviceKeys[otherUserId]?.deviceKeys[keyId] ??
     302           60 :             client.userDeviceKeys[otherUserId]?.crossSigningKeys[keyId];
     303              :         if (key == null) {
     304              :           continue;
     305              :         }
     306              : 
     307           11 :         if (onlyValidateUserIds_.isNotEmpty &&
     308            0 :             !onlyValidateUserIds_.contains(key.userId)) {
     309              :           // we don't want to verify keys from this user
     310              :           continue;
     311              :         }
     312              : 
     313           11 :         if (key.blocked) {
     314              :           continue; // we can't be bothered about this keys signatures
     315              :         }
     316              :         var haveValidSignature = false;
     317              :         var gotSignatureFromCache = false;
     318           11 :         final fullKeyIdBool = validSignatures
     319            7 :             ?.tryGetMap<String, Object?>(otherUserId)
     320            7 :             ?.tryGet<bool>(fullKeyId);
     321           11 :         if (fullKeyIdBool == true) {
     322              :           haveValidSignature = true;
     323              :           gotSignatureFromCache = true;
     324           11 :         } else if (fullKeyIdBool == false) {
     325              :           haveValidSignature = false;
     326              :           gotSignatureFromCache = true;
     327              :         }
     328              : 
     329           11 :         if (!gotSignatureFromCache && key.ed25519Key != null) {
     330              :           // validate the signature manually
     331           22 :           haveValidSignature = _verifySignature(key.ed25519Key!, signature);
     332           22 :           final validSignatures = this.validSignatures ??= <String, dynamic>{};
     333           11 :           if (!validSignatures.containsKey(otherUserId)) {
     334           22 :             validSignatures[otherUserId] = <String, dynamic>{};
     335              :           }
     336           22 :           validSignatures[otherUserId][fullKeyId] = haveValidSignature;
     337              :         }
     338              :         if (!haveValidSignature) {
     339              :           // no valid signature, this key is useless
     340              :           continue;
     341              :         }
     342              : 
     343            5 :         if ((verifiedOnly && key.directVerified) ||
     344           11 :             (key is CrossSigningKey &&
     345           22 :                 key.usage.contains('master') &&
     346              :                 (verifiedByTheirMasterKey ||
     347           26 :                     (key.directVerified && key.userId == client.userID)))) {
     348              :           return true; // we verified this key and it is valid...all checks out!
     349              :         }
     350              :         // or else we just recurse into that key and check if it works out
     351           11 :         final haveChain = key.hasValidSignatureChain(
     352              :           verifiedOnly: verifiedOnly,
     353              :           visited: visited_,
     354              :           onlyValidateUserIds: onlyValidateUserIds,
     355              :           verifiedByTheirMasterKey: verifiedByTheirMasterKey,
     356              :         );
     357              :         if (haveChain) {
     358              :           return true;
     359              :         }
     360              :       }
     361              :     }
     362              :     return false;
     363              :   }
     364              : 
     365            7 :   Future<void> setVerified(bool newVerified, [bool sign = true]) async {
     366            7 :     _verified = newVerified;
     367           14 :     final encryption = client.encryption;
     368              :     if (newVerified &&
     369              :         sign &&
     370              :         encryption != null &&
     371            4 :         client.encryptionEnabled &&
     372            6 :         encryption.crossSigning.signable([this])) {
     373              :       // sign the key!
     374              :       // ignore: unawaited_futures
     375            6 :       encryption.crossSigning.sign([this]);
     376              :     }
     377              :   }
     378              : 
     379              :   Future<void> setBlocked(bool newBlocked);
     380              : 
     381           40 :   @override
     382              :   Map<String, dynamic> toJson() {
     383           80 :     final data = super.toJson().copy();
     384              :     // some old data may have the verified and blocked keys which are unneeded now
     385           40 :     data.remove('verified');
     386           40 :     data.remove('blocked');
     387              :     return data;
     388              :   }
     389              : 
     390            0 :   @override
     391            0 :   String toString() => json.encode(toJson());
     392              : 
     393            9 :   @override
     394            9 :   bool operator ==(Object other) => (other is SignableKey &&
     395           27 :       other.userId == userId &&
     396           27 :       other.identifier == identifier);
     397              : 
     398            9 :   @override
     399           27 :   int get hashCode => Object.hash(userId, identifier);
     400              : }
     401              : 
     402              : class CrossSigningKey extends SignableKey {
     403              :   @override
     404              :   String? identifier;
     405              : 
     406           80 :   String? get publicKey => identifier;
     407              :   late List<String> usage;
     408              : 
     409           40 :   bool get isValid =>
     410           80 :       userId.isNotEmpty &&
     411           40 :       publicKey != null &&
     412           80 :       keys.isNotEmpty &&
     413           40 :       ed25519Key != null;
     414              : 
     415            5 :   @override
     416              :   Future<void> setVerified(bool newVerified, [bool sign = true]) async {
     417            5 :     if (!isValid) {
     418            0 :       throw Exception('setVerified called on invalid key');
     419              :     }
     420            5 :     await super.setVerified(newVerified, sign);
     421           10 :     await client.database
     422           15 :         .setVerifiedUserCrossSigningKey(newVerified, userId, publicKey!);
     423              :   }
     424              : 
     425            2 :   @override
     426              :   Future<void> setBlocked(bool newBlocked) async {
     427            2 :     if (!isValid) {
     428            0 :       throw Exception('setBlocked called on invalid key');
     429              :     }
     430            2 :     _blocked = newBlocked;
     431            4 :     await client.database
     432            6 :         .setBlockedUserCrossSigningKey(newBlocked, userId, publicKey!);
     433              :   }
     434              : 
     435           40 :   CrossSigningKey.fromMatrixCrossSigningKey(
     436              :     MatrixCrossSigningKey key,
     437              :     Client client,
     438          120 :   ) : super.fromJson(key.toJson().copy(), client) {
     439           40 :     final json = toJson();
     440           80 :     identifier = key.publicKey;
     441          120 :     usage = json['usage'].cast<String>();
     442              :   }
     443              : 
     444            2 :   CrossSigningKey.fromDbJson(Map<String, dynamic> dbEntry, Client client)
     445            6 :       : super.fromJson(Event.getMapFromPayload(dbEntry['content']), client) {
     446            2 :     final json = toJson();
     447            4 :     identifier = dbEntry['public_key'];
     448            6 :     usage = json['usage'].cast<String>();
     449            4 :     _verified = dbEntry['verified'];
     450            4 :     _blocked = dbEntry['blocked'];
     451              :   }
     452              : 
     453            2 :   CrossSigningKey.fromJson(Map<String, dynamic> json, Client client)
     454            4 :       : super.fromJson(json.copy(), client) {
     455            2 :     final json = toJson();
     456            6 :     usage = json['usage'].cast<String>();
     457            4 :     if (keys.isNotEmpty) {
     458            8 :       identifier = keys.values.first;
     459              :     }
     460              :   }
     461              : }
     462              : 
     463              : class DeviceKeys extends SignableKey {
     464              :   @override
     465              :   String? identifier;
     466              : 
     467           80 :   String? get deviceId => identifier;
     468              :   late List<String> algorithms;
     469              :   late DateTime lastActive;
     470              : 
     471          200 :   String? get curve25519Key => keys['curve25519:$deviceId'];
     472            0 :   String? get deviceDisplayName =>
     473            0 :       unsigned?.tryGet<String>('device_display_name');
     474              : 
     475              :   bool? _validSelfSignature;
     476           40 :   bool get selfSigned =>
     477           40 :       _validSelfSignature ??
     478           80 :       (_validSelfSignature = deviceId != null &&
     479           40 :           signatures
     480           80 :                   ?.tryGetMap<String, Object?>(userId)
     481          120 :                   ?.tryGet<String>('ed25519:$deviceId') !=
     482              :               null &&
     483              :           // without libolm we still want to be able to add devices. In that case we ofc just can't
     484              :           // verify the signature
     485           40 :           _verifySignature(
     486           40 :             ed25519Key!,
     487          240 :             signatures![userId]!['ed25519:$deviceId']!,
     488              :             isSignatureWithoutLibolmValid: true,
     489              :           ));
     490              : 
     491           28 :   @override
     492           56 :   bool get blocked => super.blocked || !selfSigned;
     493              : 
     494           40 :   bool get isValid =>
     495           40 :       deviceId != null &&
     496           80 :       keys.isNotEmpty &&
     497           40 :       curve25519Key != null &&
     498           40 :       ed25519Key != null &&
     499           40 :       selfSigned;
     500              : 
     501            3 :   @override
     502              :   Future<void> setVerified(bool newVerified, [bool sign = true]) async {
     503            3 :     if (!isValid) {
     504              :       //throw Exception('setVerified called on invalid key');
     505              :       return;
     506              :     }
     507            3 :     await super.setVerified(newVerified, sign);
     508            6 :     await client.database
     509            9 :         .setVerifiedUserDeviceKey(newVerified, userId, deviceId!);
     510              :   }
     511              : 
     512            2 :   @override
     513              :   Future<void> setBlocked(bool newBlocked) async {
     514            2 :     if (!isValid) {
     515              :       //throw Exception('setBlocked called on invalid key');
     516              :       return;
     517              :     }
     518            2 :     _blocked = newBlocked;
     519            4 :     await client.database
     520            6 :         .setBlockedUserDeviceKey(newBlocked, userId, deviceId!);
     521              :   }
     522              : 
     523           40 :   DeviceKeys.fromMatrixDeviceKeys(
     524              :     MatrixDeviceKeys keys,
     525              :     Client client, [
     526              :     DateTime? lastActiveTs,
     527          120 :   ]) : super.fromJson(keys.toJson().copy(), client) {
     528           40 :     final json = toJson();
     529           80 :     identifier = keys.deviceId;
     530          120 :     algorithms = json['algorithms'].cast<String>();
     531           80 :     lastActive = lastActiveTs ?? DateTime.now();
     532              :   }
     533              : 
     534            1 :   DeviceKeys.fromDb(Map<String, dynamic> dbEntry, Client client)
     535            3 :       : super.fromJson(Event.getMapFromPayload(dbEntry['content']), client) {
     536            1 :     final json = toJson();
     537            2 :     identifier = dbEntry['device_id'];
     538            3 :     algorithms = json['algorithms'].cast<String>();
     539            2 :     _verified = dbEntry['verified'];
     540            2 :     _blocked = dbEntry['blocked'];
     541            1 :     lastActive =
     542            2 :         DateTime.fromMillisecondsSinceEpoch(dbEntry['last_active'] ?? 0);
     543              :   }
     544              : 
     545            4 :   DeviceKeys.fromJson(Map<String, dynamic> json, Client client)
     546            8 :       : super.fromJson(json.copy(), client) {
     547            4 :     final json = toJson();
     548            8 :     identifier = json['device_id'];
     549           12 :     algorithms = json['algorithms'].cast<String>();
     550            8 :     lastActive = DateTime.fromMillisecondsSinceEpoch(0);
     551              :   }
     552              : 
     553            1 :   Future<KeyVerification> startVerification() async {
     554            1 :     if (!isValid) {
     555            0 :       throw Exception('setVerification called on invalid key');
     556              :     }
     557            2 :     final encryption = client.encryption;
     558              :     if (encryption == null) {
     559            0 :       throw Exception('setVerification called with disabled encryption');
     560              :     }
     561              : 
     562            1 :     final request = KeyVerification(
     563              :       encryption: encryption,
     564            1 :       userId: userId,
     565            1 :       deviceId: deviceId!,
     566              :     );
     567              : 
     568            1 :     await request.start();
     569            2 :     encryption.keyVerificationManager.addRequest(request);
     570              :     return request;
     571              :   }
     572              : }
        

Generated by: LCOV version 2.0-1