LCOV - code coverage report
Current view: top level - lib/encryption - ssss.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 334 376 88.8 %
Date: 2024-09-27 11:38:01 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:core';
      22             : import 'dart:typed_data';
      23             : 
      24             : import 'package:base58check/base58.dart';
      25             : import 'package:collection/collection.dart';
      26             : import 'package:crypto/crypto.dart';
      27             : 
      28             : import 'package:matrix/encryption/encryption.dart';
      29             : import 'package:matrix/encryption/utils/base64_unpadded.dart';
      30             : import 'package:matrix/encryption/utils/ssss_cache.dart';
      31             : import 'package:matrix/matrix.dart';
      32             : import 'package:matrix/src/utils/cached_stream_controller.dart';
      33             : import 'package:matrix/src/utils/crypto/crypto.dart' as uc;
      34             : 
      35             : const cacheTypes = <String>{
      36             :   EventTypes.CrossSigningSelfSigning,
      37             :   EventTypes.CrossSigningUserSigning,
      38             :   EventTypes.MegolmBackup,
      39             : };
      40             : 
      41             : const zeroStr =
      42             :     '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00';
      43             : const base58Alphabet =
      44             :     '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
      45             : const base58 = Base58Codec(base58Alphabet);
      46             : const olmRecoveryKeyPrefix = [0x8B, 0x01];
      47             : const ssssKeyLength = 32;
      48             : const pbkdf2DefaultIterations = 500000;
      49             : const pbkdf2SaltLength = 64;
      50             : 
      51             : /// SSSS: **S**ecure **S**ecret **S**torage and **S**haring
      52             : /// Read more about SSSS at:
      53             : /// https://matrix.org/docs/guides/implementing-more-advanced-e-2-ee-features-such-as-cross-signing#3-implementing-ssss
      54             : class SSSS {
      55             :   final Encryption encryption;
      56             : 
      57          72 :   Client get client => encryption.client;
      58             :   final pendingShareRequests = <String, _ShareRequest>{};
      59             :   final _validators = <String, FutureOr<bool> Function(String)>{};
      60             :   final _cacheCallbacks = <String, FutureOr<void> Function(String)>{};
      61             :   final Map<String, SSSSCache> _cache = <String, SSSSCache>{};
      62             : 
      63             :   /// Will be called when a new secret has been stored in the database
      64             :   final CachedStreamController<String> onSecretStored =
      65             :       CachedStreamController();
      66             : 
      67          24 :   SSSS(this.encryption);
      68             : 
      69             :   // for testing
      70           3 :   Future<void> clearCache() async {
      71           9 :     await client.database?.clearSSSSCache();
      72           6 :     _cache.clear();
      73             :   }
      74             : 
      75           7 :   static DerivedKeys deriveKeys(Uint8List key, String name) {
      76           7 :     final zerosalt = Uint8List(8);
      77          14 :     final prk = Hmac(sha256, zerosalt).convert(key);
      78           7 :     final b = Uint8List(1);
      79           7 :     b[0] = 1;
      80          35 :     final aesKey = Hmac(sha256, prk.bytes).convert(utf8.encode(name) + b);
      81           7 :     b[0] = 2;
      82             :     final hmacKey =
      83          49 :         Hmac(sha256, prk.bytes).convert(aesKey.bytes + utf8.encode(name) + b);
      84           7 :     return DerivedKeys(
      85          14 :         aesKey: Uint8List.fromList(aesKey.bytes),
      86          14 :         hmacKey: Uint8List.fromList(hmacKey.bytes));
      87             :   }
      88             : 
      89           7 :   static Future<EncryptedContent> encryptAes(
      90             :       String data, Uint8List key, String name,
      91             :       [String? ivStr]) async {
      92             :     Uint8List iv;
      93             :     if (ivStr != null) {
      94           7 :       iv = base64decodeUnpadded(ivStr);
      95             :     } else {
      96           4 :       iv = Uint8List.fromList(uc.secureRandomBytes(16));
      97             :     }
      98             :     // we need to clear bit 63 of the IV
      99          14 :     iv[8] &= 0x7f;
     100             : 
     101           7 :     final keys = deriveKeys(key, name);
     102             : 
     103          14 :     final plain = Uint8List.fromList(utf8.encode(data));
     104          21 :     final ciphertext = await uc.aesCtr.encrypt(plain, keys.aesKey, iv);
     105             : 
     106          21 :     final hmac = Hmac(sha256, keys.hmacKey).convert(ciphertext);
     107             : 
     108           7 :     return EncryptedContent(
     109           7 :         iv: base64.encode(iv),
     110           7 :         ciphertext: base64.encode(ciphertext),
     111          14 :         mac: base64.encode(hmac.bytes));
     112             :   }
     113             : 
     114           7 :   static Future<String> decryptAes(
     115             :       EncryptedContent data, Uint8List key, String name) async {
     116           7 :     final keys = deriveKeys(key, name);
     117          14 :     final cipher = base64decodeUnpadded(data.ciphertext);
     118             :     final hmac = base64
     119          35 :         .encode(Hmac(sha256, keys.hmacKey).convert(cipher).bytes)
     120          14 :         .replaceAll(RegExp(r'=+$'), '');
     121          28 :     if (hmac != data.mac.replaceAll(RegExp(r'=+$'), '')) {
     122           0 :       throw Exception('Bad MAC');
     123             :     }
     124           7 :     final decipher = await uc.aesCtr
     125          28 :         .encrypt(cipher, keys.aesKey, base64decodeUnpadded(data.iv));
     126           7 :     return String.fromCharCodes(decipher);
     127             :   }
     128             : 
     129           6 :   static Uint8List decodeRecoveryKey(String recoveryKey) {
     130          18 :     final result = base58.decode(recoveryKey.replaceAll(RegExp(r'\s'), ''));
     131             : 
     132          18 :     final parity = result.fold<int>(0, (a, b) => a ^ b);
     133           6 :     if (parity != 0) {
     134           0 :       throw InvalidPassphraseException('Incorrect parity');
     135             :     }
     136             : 
     137          18 :     for (var i = 0; i < olmRecoveryKeyPrefix.length; i++) {
     138          18 :       if (result[i] != olmRecoveryKeyPrefix[i]) {
     139           0 :         throw InvalidPassphraseException('Incorrect prefix');
     140             :       }
     141             :     }
     142             : 
     143          30 :     if (result.length != olmRecoveryKeyPrefix.length + ssssKeyLength + 1) {
     144           0 :       throw InvalidPassphraseException('Incorrect length');
     145             :     }
     146             : 
     147          18 :     return Uint8List.fromList(result.sublist(olmRecoveryKeyPrefix.length,
     148          12 :         olmRecoveryKeyPrefix.length + ssssKeyLength));
     149             :   }
     150             : 
     151           1 :   static String encodeRecoveryKey(Uint8List recoveryKey) {
     152           2 :     final keyToEncode = <int>[...olmRecoveryKeyPrefix, ...recoveryKey];
     153           3 :     final parity = keyToEncode.fold<int>(0, (a, b) => a ^ b);
     154           1 :     keyToEncode.add(parity);
     155             :     // base58-encode and add a space every four chars
     156             :     return base58
     157           1 :         .encode(keyToEncode)
     158           5 :         .replaceAllMapped(RegExp(r'.{4}'), (s) => '${s.group(0)} ')
     159           1 :         .trim();
     160             :   }
     161             : 
     162           2 :   static Future<Uint8List> keyFromPassphrase(
     163             :       String passphrase, PassphraseInfo info) async {
     164           4 :     if (info.algorithm != AlgorithmTypes.pbkdf2) {
     165           0 :       throw InvalidPassphraseException('Unknown algorithm');
     166             :     }
     167           2 :     if (info.iterations == null) {
     168           0 :       throw InvalidPassphraseException('Passphrase info without iterations');
     169             :     }
     170           2 :     if (info.salt == null) {
     171           0 :       throw InvalidPassphraseException('Passphrase info without salt');
     172             :     }
     173           2 :     return await uc.pbkdf2(
     174           4 :         Uint8List.fromList(utf8.encode(passphrase)),
     175           6 :         Uint8List.fromList(utf8.encode(info.salt!)),
     176           2 :         uc.sha512,
     177           2 :         info.iterations!,
     178           2 :         info.bits ?? 256);
     179             :   }
     180             : 
     181          24 :   void setValidator(String type, FutureOr<bool> Function(String) validator) {
     182          48 :     _validators[type] = validator;
     183             :   }
     184             : 
     185          24 :   void setCacheCallback(String type, FutureOr<void> Function(String) callback) {
     186          48 :     _cacheCallbacks[type] = callback;
     187             :   }
     188             : 
     189          14 :   String? get defaultKeyId => client
     190          14 :       .accountData[EventTypes.SecretStorageDefaultKey]
     191           7 :       ?.parsedSecretStorageDefaultKeyContent
     192           7 :       .key;
     193             : 
     194           1 :   Future<void> setDefaultKeyId(String keyId) async {
     195           2 :     await client.setAccountData(
     196           2 :       client.userID!,
     197             :       EventTypes.SecretStorageDefaultKey,
     198           2 :       SecretStorageDefaultKeyContent(key: keyId).toJson(),
     199             :     );
     200             :   }
     201             : 
     202           7 :   SecretStorageKeyContent? getKey(String keyId) {
     203          28 :     return client.accountData[EventTypes.secretStorageKey(keyId)]
     204           7 :         ?.parsedSecretStorageKeyContent;
     205             :   }
     206             : 
     207           2 :   bool isKeyValid(String keyId) =>
     208           6 :       getKey(keyId)?.algorithm == AlgorithmTypes.secretStorageV1AesHmcSha2;
     209             : 
     210             :   /// Creates a new secret storage key, optional encrypts it with [passphrase]
     211             :   /// and stores it in the user's `accountData`.
     212           2 :   Future<OpenSSSS> createKey([String? passphrase]) async {
     213             :     Uint8List privateKey;
     214           2 :     final content = SecretStorageKeyContent();
     215             :     if (passphrase != null) {
     216             :       // we need to derive the key off of the passphrase
     217           4 :       content.passphrase = PassphraseInfo(
     218             :         iterations: pbkdf2DefaultIterations,
     219           4 :         salt: base64.encode(uc.secureRandomBytes(pbkdf2SaltLength)),
     220             :         algorithm: AlgorithmTypes.pbkdf2,
     221           2 :         bits: ssssKeyLength * 8,
     222             :       );
     223           2 :       privateKey = await Future.value(
     224           6 :         client.nativeImplementations.keyFromPassphrase(
     225           2 :           KeyFromPassphraseArgs(
     226             :             passphrase: passphrase,
     227           2 :             info: content.passphrase!,
     228             :           ),
     229             :         ),
     230           4 :       ).timeout(Duration(seconds: 10));
     231             :     } else {
     232             :       // we need to just generate a new key from scratch
     233           2 :       privateKey = Uint8List.fromList(uc.secureRandomBytes(ssssKeyLength));
     234             :     }
     235             :     // now that we have the private key, let's create the iv and mac
     236           2 :     final encrypted = await encryptAes(zeroStr, privateKey, '');
     237           4 :     content.iv = encrypted.iv;
     238           4 :     content.mac = encrypted.mac;
     239           2 :     content.algorithm = AlgorithmTypes.secretStorageV1AesHmcSha2;
     240             : 
     241             :     const keyidByteLength = 24;
     242             : 
     243             :     // make sure we generate a unique key id
     244           2 :     final keyId = () sync* {
     245             :       for (;;) {
     246           4 :         yield base64.encode(uc.secureRandomBytes(keyidByteLength));
     247             :       }
     248           2 :     }()
     249           6 :         .firstWhere((keyId) => getKey(keyId) == null);
     250             : 
     251           2 :     final accountDataTypeKeyId = EventTypes.secretStorageKey(keyId);
     252             :     // noooow we set the account data
     253             : 
     254           4 :     await client.setAccountData(
     255           6 :         client.userID!, accountDataTypeKeyId, content.toJson());
     256             : 
     257           6 :     while (!client.accountData.containsKey(accountDataTypeKeyId)) {
     258           0 :       Logs().v('Waiting accountData to have $accountDataTypeKeyId');
     259           0 :       await client.oneShotSync();
     260             :     }
     261             : 
     262           2 :     final key = open(keyId);
     263           2 :     await key.setPrivateKey(privateKey);
     264             :     return key;
     265             :   }
     266             : 
     267           7 :   Future<bool> checkKey(Uint8List key, SecretStorageKeyContent info) async {
     268          14 :     if (info.algorithm == AlgorithmTypes.secretStorageV1AesHmcSha2) {
     269          28 :       if ((info.mac is String) && (info.iv is String)) {
     270          14 :         final encrypted = await encryptAes(zeroStr, key, '', info.iv);
     271          28 :         return info.mac!.replaceAll(RegExp(r'=+$'), '') ==
     272          21 :             encrypted.mac.replaceAll(RegExp(r'=+$'), '');
     273             :       } else {
     274             :         // no real information about the key, assume it is valid
     275             :         return true;
     276             :       }
     277             :     } else {
     278           0 :       throw InvalidPassphraseException('Unknown Algorithm');
     279             :     }
     280             :   }
     281             : 
     282          23 :   bool isSecret(String type) =>
     283         138 :       client.accountData[type]?.content['encrypted'] is Map;
     284             : 
     285          23 :   Future<String?> getCached(String type) async {
     286          46 :     if (client.database == null) {
     287             :       return null;
     288             :     }
     289             :     // check if it is still valid
     290          23 :     final keys = keyIdsFromType(type);
     291             :     if (keys == null) {
     292             :       return null;
     293             :     }
     294           7 :     bool isValid(SSSSCache dbEntry) =>
     295          14 :         keys.contains(dbEntry.keyId) &&
     296           7 :         dbEntry.ciphertext != null &&
     297           7 :         dbEntry.keyId != null &&
     298          28 :         client.accountData[type]?.content
     299           7 :                 .tryGetMap<String, Object?>('encrypted')
     300          14 :                 ?.tryGetMap<String, Object?>(dbEntry.keyId!)
     301          14 :                 ?.tryGet<String>('ciphertext') ==
     302           7 :             dbEntry.ciphertext;
     303             : 
     304          46 :     final fromCache = _cache[type];
     305           7 :     if (fromCache != null && isValid(fromCache)) {
     306           7 :       return fromCache.content;
     307             :     }
     308          69 :     final ret = await client.database?.getSSSSCache(type);
     309             :     if (ret == null) {
     310             :       return null;
     311             :     }
     312           7 :     if (isValid(ret)) {
     313          14 :       _cache[type] = ret;
     314           7 :       return ret.content;
     315             :     }
     316             :     return null;
     317             :   }
     318             : 
     319           7 :   Future<String> getStored(String type, String keyId, Uint8List key) async {
     320          21 :     final secretInfo = client.accountData[type];
     321             :     if (secretInfo == null) {
     322           1 :       throw Exception('Not found');
     323             :     }
     324             :     final encryptedContent =
     325          14 :         secretInfo.content.tryGetMap<String, Object?>('encrypted');
     326             :     if (encryptedContent == null) {
     327           0 :       throw Exception('Content is not encrypted');
     328             :     }
     329           7 :     final enc = encryptedContent.tryGetMap<String, Object?>(keyId);
     330             :     if (enc == null) {
     331           0 :       throw Exception('Wrong / unknown key: $type, $keyId');
     332             :     }
     333           7 :     final ciphertext = enc.tryGet<String>('ciphertext');
     334           7 :     final iv = enc.tryGet<String>('iv');
     335           7 :     final mac = enc.tryGet<String>('mac');
     336             :     if (ciphertext == null || iv == null || mac == null) {
     337           0 :       throw Exception('Wrong types for encrypted content or missing keys.');
     338             :     }
     339           7 :     final encryptInfo = EncryptedContent(
     340             :       iv: iv,
     341             :       ciphertext: ciphertext,
     342             :       mac: mac,
     343             :     );
     344           7 :     final decrypted = await decryptAes(encryptInfo, key, type);
     345          14 :     final db = client.database;
     346           7 :     if (cacheTypes.contains(type) && db != null) {
     347             :       // cache the thing
     348           7 :       await db.storeSSSSCache(type, keyId, ciphertext, decrypted);
     349          14 :       onSecretStored.add(keyId);
     350          21 :       if (_cacheCallbacks.containsKey(type) && await getCached(type) == null) {
     351           0 :         _cacheCallbacks[type]!(decrypted);
     352             :       }
     353             :     }
     354             :     return decrypted;
     355             :   }
     356             : 
     357           2 :   Future<void> store(String type, String secret, String keyId, Uint8List key,
     358             :       {bool add = false}) async {
     359           2 :     final encrypted = await encryptAes(secret, key, type);
     360             :     Map<String, dynamic>? content;
     361           3 :     if (add && client.accountData[type] != null) {
     362           5 :       content = client.accountData[type]!.content.copy();
     363           2 :       if (content['encrypted'] is! Map) {
     364           0 :         content['encrypted'] = <String, dynamic>{};
     365             :       }
     366             :     }
     367           2 :     content ??= <String, dynamic>{
     368           2 :       'encrypted': <String, dynamic>{},
     369             :     };
     370           6 :     content['encrypted'][keyId] = <String, dynamic>{
     371           2 :       'iv': encrypted.iv,
     372           2 :       'ciphertext': encrypted.ciphertext,
     373           2 :       'mac': encrypted.mac,
     374             :     };
     375             :     // store the thing in your account data
     376           8 :     await client.setAccountData(client.userID!, type, content);
     377           4 :     final db = client.database;
     378           2 :     if (cacheTypes.contains(type) && db != null) {
     379             :       // cache the thing
     380           2 :       await db.storeSSSSCache(type, keyId, encrypted.ciphertext, secret);
     381           2 :       onSecretStored.add(keyId);
     382           3 :       if (_cacheCallbacks.containsKey(type) && await getCached(type) == null) {
     383           0 :         _cacheCallbacks[type]!(secret);
     384             :       }
     385             :     }
     386             :   }
     387             : 
     388           1 :   Future<void> validateAndStripOtherKeys(
     389             :       String type, String secret, String keyId, Uint8List key) async {
     390           2 :     if (await getStored(type, keyId, key) != secret) {
     391           0 :       throw Exception('Secrets do not match up!');
     392             :     }
     393             :     // now remove all other keys
     394           5 :     final content = client.accountData[type]?.content.copy();
     395             :     if (content == null) {
     396           0 :       throw InvalidPassphraseException('Key has no content!');
     397             :     }
     398           1 :     final encryptedContent = content.tryGetMap<String, Object?>('encrypted');
     399             :     if (encryptedContent == null) {
     400           0 :       throw Exception('Wrong type for encrypted content!');
     401             :     }
     402             : 
     403             :     final otherKeys =
     404           5 :         Set<String>.from(encryptedContent.keys.where((k) => k != keyId));
     405           3 :     encryptedContent.removeWhere((k, v) => otherKeys.contains(k));
     406             :     // yes, we are paranoid...
     407           2 :     if (await getStored(type, keyId, key) != secret) {
     408           0 :       throw Exception('Secrets do not match up!');
     409             :     }
     410             :     // store the thing in your account data
     411           4 :     await client.setAccountData(client.userID!, type, content);
     412           1 :     if (cacheTypes.contains(type)) {
     413             :       // cache the thing
     414             :       final ciphertext = encryptedContent
     415           1 :           .tryGetMap<String, Object?>(keyId)
     416           1 :           ?.tryGet<String>('ciphertext');
     417             :       if (ciphertext == null) {
     418           0 :         throw Exception('Wrong type for ciphertext!');
     419             :       }
     420           3 :       await client.database?.storeSSSSCache(type, keyId, ciphertext, secret);
     421           2 :       onSecretStored.add(keyId);
     422             :     }
     423             :   }
     424             : 
     425           7 :   Future<void> maybeCacheAll(String keyId, Uint8List key) async {
     426          14 :     for (final type in cacheTypes) {
     427           7 :       final secret = await getCached(type);
     428             :       if (secret == null) {
     429             :         try {
     430           7 :           await getStored(type, keyId, key);
     431             :         } catch (_) {
     432             :           // the entry wasn't stored, just ignore it
     433             :         }
     434             :       }
     435             :     }
     436             :   }
     437             : 
     438           2 :   Future<void> maybeRequestAll([List<DeviceKeys>? devices]) async {
     439           4 :     for (final type in cacheTypes) {
     440           2 :       if (keyIdsFromType(type) != null) {
     441           2 :         final secret = await getCached(type);
     442             :         if (secret == null) {
     443           2 :           await request(type, devices);
     444             :         }
     445             :       }
     446             :     }
     447             :   }
     448             : 
     449           2 :   Future<void> request(String type, [List<DeviceKeys>? devices]) async {
     450             :     // only send to own, verified devices
     451           6 :     Logs().i('[SSSS] Requesting type $type...');
     452           2 :     if (devices == null || devices.isEmpty) {
     453           5 :       if (!client.userDeviceKeys.containsKey(client.userID)) {
     454           0 :         Logs().w('[SSSS] User does not have any devices');
     455             :         return;
     456             :       }
     457             :       devices =
     458           8 :           client.userDeviceKeys[client.userID]!.deviceKeys.values.toList();
     459             :     }
     460           4 :     devices.removeWhere((DeviceKeys d) =>
     461           8 :         d.userId != client.userID ||
     462           2 :         !d.verified ||
     463           2 :         d.blocked ||
     464           8 :         d.deviceId == client.deviceID);
     465           2 :     if (devices.isEmpty) {
     466           2 :       Logs().w('[SSSS] No devices');
     467             :       return;
     468             :     }
     469           4 :     final requestId = client.generateUniqueTransactionId();
     470           2 :     final request = _ShareRequest(
     471             :       requestId: requestId,
     472             :       type: type,
     473             :       devices: devices,
     474             :     );
     475           4 :     pendingShareRequests[requestId] = request;
     476           6 :     await client.sendToDeviceEncrypted(devices, EventTypes.SecretRequest, {
     477             :       'action': 'request',
     478           4 :       'requesting_device_id': client.deviceID,
     479             :       'request_id': requestId,
     480             :       'name': type,
     481             :     });
     482             :   }
     483             : 
     484             :   DateTime? _lastCacheRequest;
     485             :   bool _isPeriodicallyRequestingMissingCache = false;
     486             : 
     487          24 :   Future<void> periodicallyRequestMissingCache() async {
     488          24 :     if (_isPeriodicallyRequestingMissingCache ||
     489          24 :         (_lastCacheRequest != null &&
     490           1 :             DateTime.now()
     491           2 :                 .subtract(Duration(minutes: 15))
     492           2 :                 .isBefore(_lastCacheRequest!)) ||
     493          48 :         client.isUnknownSession) {
     494             :       // we are already requesting right now or we attempted to within the last 15 min
     495             :       return;
     496             :     }
     497           2 :     _lastCacheRequest = DateTime.now();
     498           1 :     _isPeriodicallyRequestingMissingCache = true;
     499             :     try {
     500           1 :       await maybeRequestAll();
     501             :     } finally {
     502           1 :       _isPeriodicallyRequestingMissingCache = false;
     503             :     }
     504             :   }
     505             : 
     506           1 :   Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
     507           2 :     if (event.type == EventTypes.SecretRequest) {
     508             :       // got a request to share a secret
     509           2 :       Logs().i('[SSSS] Received sharing request...');
     510           4 :       if (event.sender != client.userID ||
     511           5 :           !client.userDeviceKeys.containsKey(client.userID)) {
     512           2 :         Logs().i('[SSSS] Not sent by us');
     513             :         return; // we aren't asking for it ourselves, so ignore
     514             :       }
     515           3 :       if (event.content['action'] != 'request') {
     516           2 :         Logs().i('[SSSS] it is actually a cancelation');
     517             :         return; // not actually requesting, so ignore
     518             :       }
     519           5 :       final device = client.userDeviceKeys[client.userID]!
     520           4 :           .deviceKeys[event.content['requesting_device_id']];
     521           2 :       if (device == null || !device.verified || device.blocked) {
     522           2 :         Logs().i('[SSSS] Unknown / unverified devices, ignoring');
     523             :         return; // nope....unknown or untrusted device
     524             :       }
     525             :       // alright, all seems fine...let's check if we actually have the secret they are asking for
     526           2 :       final type = event.content.tryGet<String>('name');
     527             :       if (type == null) {
     528           0 :         Logs().i('[SSSS] Wrong data type for type param, ignoring');
     529             :         return;
     530             :       }
     531           1 :       final secret = await getCached(type);
     532             :       if (secret == null) {
     533           1 :         Logs()
     534           2 :             .i('[SSSS] We don\'t have the secret for $type ourself, ignoring');
     535             :         return; // seems like we don't have this, either
     536             :       }
     537             :       // okay, all checks out...time to share this secret!
     538           3 :       Logs().i('[SSSS] Replying with secret for $type');
     539           2 :       await client.sendToDeviceEncrypted(
     540           1 :           [device],
     541             :           EventTypes.SecretSend,
     542           1 :           {
     543           2 :             'request_id': event.content['request_id'],
     544             :             'secret': secret,
     545             :           });
     546           2 :     } else if (event.type == EventTypes.SecretSend) {
     547             :       // receiving a secret we asked for
     548           2 :       Logs().i('[SSSS] Received shared secret...');
     549           1 :       final encryptedContent = event.encryptedContent;
     550           4 :       if (event.sender != client.userID ||
     551           4 :           !pendingShareRequests.containsKey(event.content['request_id']) ||
     552             :           encryptedContent == null) {
     553           2 :         Logs().i('[SSSS] Not by us or unknown request');
     554             :         return; // we have no idea what we just received
     555             :       }
     556           4 :       final request = pendingShareRequests[event.content['request_id']]!;
     557             :       // alright, as we received a known request id, let's check if the sender is valid
     558           3 :       final device = request.devices.firstWhereOrNull((d) =>
     559           3 :           d.userId == event.sender &&
     560           3 :           d.curve25519Key == encryptedContent['sender_key']);
     561             :       if (device == null) {
     562           2 :         Logs().i('[SSSS] Someone else replied?');
     563             :         return; // someone replied whom we didn't send the share request to
     564             :       }
     565           2 :       final secret = event.content.tryGet<String>('secret');
     566             :       if (secret == null) {
     567           2 :         Logs().i('[SSSS] Secret wasn\'t a string');
     568             :         return; // the secret wasn't a string....wut?
     569             :       }
     570             :       // let's validate if the secret is, well, valid
     571           3 :       if (_validators.containsKey(request.type) &&
     572           4 :           !(await _validators[request.type]!(secret))) {
     573           2 :         Logs().i('[SSSS] The received secret was invalid');
     574             :         return; // didn't pass the validator
     575             :       }
     576           3 :       pendingShareRequests.remove(request.requestId);
     577           5 :       if (request.start.add(Duration(minutes: 15)).isBefore(DateTime.now())) {
     578           0 :         Logs().i('[SSSS] Request is too far in the past');
     579             :         return; // our request is more than 15min in the past...better not trust it anymore
     580             :       }
     581           4 :       Logs().i('[SSSS] Secret for type ${request.type} is ok, storing it');
     582           2 :       final db = client.database;
     583             :       if (db != null) {
     584           2 :         final keyId = keyIdFromType(request.type);
     585             :         if (keyId != null) {
     586           5 :           final ciphertext = (client.accountData[request.type]!.content
     587           1 :                   .tryGetMap<String, Object?>('encrypted'))
     588           1 :               ?.tryGetMap<String, Object?>(keyId)
     589           1 :               ?.tryGet<String>('ciphertext');
     590             :           if (ciphertext == null) {
     591           0 :             Logs().i('[SSSS] Ciphertext is empty or not a String');
     592             :             return;
     593             :           }
     594           2 :           await db.storeSSSSCache(request.type, keyId, ciphertext, secret);
     595           3 :           if (_cacheCallbacks.containsKey(request.type)) {
     596           4 :             _cacheCallbacks[request.type]!(secret);
     597             :           }
     598           2 :           onSecretStored.add(keyId);
     599             :         }
     600             :       }
     601             :     }
     602             :   }
     603             : 
     604          23 :   Set<String>? keyIdsFromType(String type) {
     605          69 :     final data = client.accountData[type];
     606             :     if (data == null) {
     607             :       return null;
     608             :     }
     609             :     final contentEncrypted =
     610          46 :         data.content.tryGetMap<String, Object?>('encrypted');
     611             :     if (contentEncrypted != null) {
     612          46 :       return contentEncrypted.keys.toSet();
     613             :     }
     614             :     return null;
     615             :   }
     616             : 
     617           7 :   String? keyIdFromType(String type) {
     618           7 :     final keys = keyIdsFromType(type);
     619           4 :     if (keys == null || keys.isEmpty) {
     620             :       return null;
     621             :     }
     622           8 :     if (keys.contains(defaultKeyId)) {
     623           4 :       return defaultKeyId;
     624             :     }
     625           0 :     return keys.first;
     626             :   }
     627             : 
     628           7 :   OpenSSSS open([String? identifier]) {
     629           4 :     identifier ??= defaultKeyId;
     630             :     if (identifier == null) {
     631           0 :       throw Exception('Dont know what to open');
     632             :     }
     633           7 :     final keyToOpen = keyIdFromType(identifier) ?? identifier;
     634           7 :     final key = getKey(keyToOpen);
     635             :     if (key == null) {
     636           0 :       throw Exception('Unknown key to open');
     637             :     }
     638           7 :     return OpenSSSS(ssss: this, keyId: keyToOpen, keyData: key);
     639             :   }
     640             : }
     641             : 
     642             : class _ShareRequest {
     643             :   final String requestId;
     644             :   final String type;
     645             :   final List<DeviceKeys> devices;
     646             :   final DateTime start;
     647             : 
     648           2 :   _ShareRequest(
     649             :       {required this.requestId, required this.type, required this.devices})
     650           2 :       : start = DateTime.now();
     651             : }
     652             : 
     653             : class EncryptedContent {
     654             :   final String iv;
     655             :   final String ciphertext;
     656             :   final String mac;
     657             : 
     658           7 :   EncryptedContent(
     659             :       {required this.iv, required this.ciphertext, required this.mac});
     660             : }
     661             : 
     662             : class DerivedKeys {
     663             :   final Uint8List aesKey;
     664             :   final Uint8List hmacKey;
     665             : 
     666           7 :   DerivedKeys({required this.aesKey, required this.hmacKey});
     667             : }
     668             : 
     669             : class OpenSSSS {
     670             :   final SSSS ssss;
     671             :   final String keyId;
     672             :   final SecretStorageKeyContent keyData;
     673             : 
     674           7 :   OpenSSSS({required this.ssss, required this.keyId, required this.keyData});
     675             : 
     676             :   Uint8List? privateKey;
     677             : 
     678           4 :   bool get isUnlocked => privateKey != null;
     679             : 
     680           6 :   bool get hasPassphrase => keyData.passphrase != null;
     681             : 
     682           1 :   String? get recoveryKey =>
     683           3 :       isUnlocked ? SSSS.encodeRecoveryKey(privateKey!) : null;
     684             : 
     685           7 :   Future<void> unlock(
     686             :       {String? passphrase,
     687             :       String? recoveryKey,
     688             :       String? keyOrPassphrase,
     689             :       bool postUnlock = true}) async {
     690             :     if (keyOrPassphrase != null) {
     691             :       try {
     692           0 :         await unlock(recoveryKey: keyOrPassphrase, postUnlock: postUnlock);
     693             :       } catch (_) {
     694           0 :         if (hasPassphrase) {
     695           0 :           await unlock(passphrase: keyOrPassphrase, postUnlock: postUnlock);
     696             :         } else {
     697             :           rethrow;
     698             :         }
     699             :       }
     700             :       return;
     701             :     } else if (passphrase != null) {
     702           2 :       if (!hasPassphrase) {
     703           0 :         throw InvalidPassphraseException(
     704             :             'Tried to unlock with passphrase while key does not have a passphrase');
     705             :       }
     706           4 :       privateKey = await Future.value(
     707           8 :         ssss.client.nativeImplementations.keyFromPassphrase(
     708           2 :           KeyFromPassphraseArgs(
     709             :             passphrase: passphrase,
     710           4 :             info: keyData.passphrase!,
     711             :           ),
     712             :         ),
     713           4 :       ).timeout(Duration(seconds: 10));
     714             :     } else if (recoveryKey != null) {
     715          12 :       privateKey = SSSS.decodeRecoveryKey(recoveryKey);
     716             :     } else {
     717           0 :       throw InvalidPassphraseException('Nothing specified');
     718             :     }
     719             :     // verify the validity of the key
     720          28 :     if (!await ssss.checkKey(privateKey!, keyData)) {
     721           1 :       privateKey = null;
     722           1 :       throw InvalidPassphraseException('Inalid key');
     723             :     }
     724             :     if (postUnlock) {
     725             :       try {
     726           6 :         await _postUnlock();
     727             :       } catch (e, s) {
     728           0 :         Logs().e('Error during post unlock', e, s);
     729             :       }
     730             :     }
     731             :   }
     732             : 
     733           2 :   Future<void> setPrivateKey(Uint8List key) async {
     734           6 :     if (!await ssss.checkKey(key, keyData)) {
     735           0 :       throw Exception('Invalid key');
     736             :     }
     737           2 :     privateKey = key;
     738             :   }
     739             : 
     740           4 :   Future<String> getStored(String type) async {
     741           4 :     final privateKey = this.privateKey;
     742             :     if (privateKey == null) {
     743           0 :       throw Exception('SSSS not unlocked');
     744             :     }
     745          12 :     return await ssss.getStored(type, keyId, privateKey);
     746             :   }
     747             : 
     748           1 :   Future<void> store(String type, String secret, {bool add = false}) async {
     749           1 :     final privateKey = this.privateKey;
     750             :     if (privateKey == null) {
     751           0 :       throw Exception('SSSS not unlocked');
     752             :     }
     753           3 :     await ssss.store(type, secret, keyId, privateKey, add: add);
     754           4 :     while (!ssss.client.accountData.containsKey(type) ||
     755           5 :         !(ssss.client.accountData[type]!.content
     756           1 :             .tryGetMap<String, Object?>('encrypted')!
     757           2 :             .containsKey(keyId)) ||
     758           2 :         await getStored(type) != secret) {
     759           0 :       Logs().d('Wait for secret of $type to match in accountdata');
     760           0 :       await ssss.client.oneShotSync();
     761             :     }
     762             :   }
     763             : 
     764           1 :   Future<void> validateAndStripOtherKeys(String type, String secret) async {
     765           1 :     final privateKey = this.privateKey;
     766             :     if (privateKey == null) {
     767           0 :       throw Exception('SSSS not unlocked');
     768             :     }
     769           3 :     await ssss.validateAndStripOtherKeys(type, secret, keyId, privateKey);
     770             :   }
     771             : 
     772           7 :   Future<void> maybeCacheAll() async {
     773           7 :     final privateKey = this.privateKey;
     774             :     if (privateKey == null) {
     775           0 :       throw Exception('SSSS not unlocked');
     776             :     }
     777          21 :     await ssss.maybeCacheAll(keyId, privateKey);
     778             :   }
     779             : 
     780           6 :   Future<void> _postUnlock() async {
     781             :     // first try to cache all secrets that aren't cached yet
     782           6 :     await maybeCacheAll();
     783             :     // now try to self-sign
     784          24 :     if (ssss.encryption.crossSigning.enabled &&
     785          48 :         ssss.client.userDeviceKeys[ssss.client.userID]?.masterKey != null &&
     786           6 :         (ssss
     787           6 :                 .keyIdsFromType(EventTypes.CrossSigningMasterKey)
     788          12 :                 ?.contains(keyId) ??
     789             :             false) &&
     790          18 :         (ssss.client.isUnknownSession ||
     791          32 :             ssss.client.userDeviceKeys[ssss.client.userID]!.masterKey
     792           8 :                     ?.directVerified !=
     793             :                 true)) {
     794             :       try {
     795          12 :         await ssss.encryption.crossSigning.selfSign(openSsss: this);
     796             :       } catch (e, s) {
     797           0 :         Logs().e('[SSSS] Failed to self-sign', e, s);
     798             :       }
     799             :     }
     800             :   }
     801             : }
     802             : 
     803             : class KeyFromPassphraseArgs {
     804             :   final String passphrase;
     805             :   final PassphraseInfo info;
     806             : 
     807           2 :   KeyFromPassphraseArgs({required this.passphrase, required this.info});
     808             : }
     809             : 
     810             : /// you would likely want to use [NativeImplementations] and
     811             : /// [Client.nativeImplementations] instead
     812           2 : Future<Uint8List> generateKeyFromPassphrase(KeyFromPassphraseArgs args) async {
     813           6 :   return await SSSS.keyFromPassphrase(args.passphrase, args.info);
     814             : }
     815             : 
     816             : class InvalidPassphraseException implements Exception {
     817             :   String cause;
     818           1 :   InvalidPassphraseException(this.cause);
     819             : }

Generated by: LCOV version 1.14