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

Generated by: LCOV version 2.0-1