LCOV - code coverage report
Current view: top level - lib/encryption - ssss.dart (source / functions) Coverage Total Hit
Test: merged.info Lines: 88.7 % 381 338
Test Date: 2025-01-14 13:39:53 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              : 
      90            7 :   static Future<EncryptedContent> encryptAes(
      91              :     String data,
      92              :     Uint8List key,
      93              :     String name, [
      94              :     String? ivStr,
      95              :   ]) async {
      96              :     Uint8List iv;
      97              :     if (ivStr != null) {
      98            7 :       iv = base64decodeUnpadded(ivStr);
      99              :     } else {
     100            4 :       iv = Uint8List.fromList(uc.secureRandomBytes(16));
     101              :     }
     102              :     // we need to clear bit 63 of the IV
     103           14 :     iv[8] &= 0x7f;
     104              : 
     105            7 :     final keys = deriveKeys(key, name);
     106              : 
     107           14 :     final plain = Uint8List.fromList(utf8.encode(data));
     108           21 :     final ciphertext = await uc.aesCtr.encrypt(plain, keys.aesKey, iv);
     109              : 
     110           21 :     final hmac = Hmac(sha256, keys.hmacKey).convert(ciphertext);
     111              : 
     112            7 :     return EncryptedContent(
     113            7 :       iv: base64.encode(iv),
     114            7 :       ciphertext: base64.encode(ciphertext),
     115           14 :       mac: base64.encode(hmac.bytes),
     116              :     );
     117              :   }
     118              : 
     119            7 :   static Future<String> decryptAes(
     120              :     EncryptedContent data,
     121              :     Uint8List key,
     122              :     String name,
     123              :   ) async {
     124            7 :     final keys = deriveKeys(key, name);
     125           14 :     final cipher = base64decodeUnpadded(data.ciphertext);
     126              :     final hmac = base64
     127           35 :         .encode(Hmac(sha256, keys.hmacKey).convert(cipher).bytes)
     128           14 :         .replaceAll(RegExp(r'=+$'), '');
     129           28 :     if (hmac != data.mac.replaceAll(RegExp(r'=+$'), '')) {
     130            0 :       throw Exception('Bad MAC');
     131              :     }
     132            7 :     final decipher = await uc.aesCtr
     133           28 :         .encrypt(cipher, keys.aesKey, base64decodeUnpadded(data.iv));
     134            7 :     return String.fromCharCodes(decipher);
     135              :   }
     136              : 
     137            6 :   static Uint8List decodeRecoveryKey(String recoveryKey) {
     138           18 :     final result = base58.decode(recoveryKey.replaceAll(RegExp(r'\s'), ''));
     139              : 
     140           18 :     final parity = result.fold<int>(0, (a, b) => a ^ b);
     141            6 :     if (parity != 0) {
     142            0 :       throw InvalidPassphraseException('Incorrect parity');
     143              :     }
     144              : 
     145           18 :     for (var i = 0; i < olmRecoveryKeyPrefix.length; i++) {
     146           18 :       if (result[i] != olmRecoveryKeyPrefix[i]) {
     147            0 :         throw InvalidPassphraseException('Incorrect prefix');
     148              :       }
     149              :     }
     150              : 
     151           30 :     if (result.length != olmRecoveryKeyPrefix.length + ssssKeyLength + 1) {
     152            0 :       throw InvalidPassphraseException('Incorrect length');
     153              :     }
     154              : 
     155            6 :     return Uint8List.fromList(
     156            6 :       result.sublist(
     157            6 :         olmRecoveryKeyPrefix.length,
     158           12 :         olmRecoveryKeyPrefix.length + ssssKeyLength,
     159              :       ),
     160              :     );
     161              :   }
     162              : 
     163            1 :   static String encodeRecoveryKey(Uint8List recoveryKey) {
     164            2 :     final keyToEncode = <int>[...olmRecoveryKeyPrefix, ...recoveryKey];
     165            3 :     final parity = keyToEncode.fold<int>(0, (a, b) => a ^ b);
     166            1 :     keyToEncode.add(parity);
     167              :     // base58-encode and add a space every four chars
     168              :     return base58
     169            1 :         .encode(keyToEncode)
     170            5 :         .replaceAllMapped(RegExp(r'.{4}'), (s) => '${s.group(0)} ')
     171            1 :         .trim();
     172              :   }
     173              : 
     174            2 :   static Future<Uint8List> keyFromPassphrase(
     175              :     String passphrase,
     176              :     PassphraseInfo info,
     177              :   ) async {
     178            4 :     if (info.algorithm != AlgorithmTypes.pbkdf2) {
     179            0 :       throw InvalidPassphraseException('Unknown algorithm');
     180              :     }
     181            2 :     if (info.iterations == null) {
     182            0 :       throw InvalidPassphraseException('Passphrase info without iterations');
     183              :     }
     184            2 :     if (info.salt == null) {
     185            0 :       throw InvalidPassphraseException('Passphrase info without salt');
     186              :     }
     187            2 :     return await uc.pbkdf2(
     188            4 :       Uint8List.fromList(utf8.encode(passphrase)),
     189            6 :       Uint8List.fromList(utf8.encode(info.salt!)),
     190            2 :       uc.sha512,
     191            2 :       info.iterations!,
     192            2 :       info.bits ?? 256,
     193              :     );
     194              :   }
     195              : 
     196           24 :   void setValidator(String type, FutureOr<bool> Function(String) validator) {
     197           48 :     _validators[type] = validator;
     198              :   }
     199              : 
     200           24 :   void setCacheCallback(String type, FutureOr<void> Function(String) callback) {
     201           48 :     _cacheCallbacks[type] = callback;
     202              :   }
     203              : 
     204           14 :   String? get defaultKeyId => client
     205           14 :       .accountData[EventTypes.SecretStorageDefaultKey]
     206            7 :       ?.parsedSecretStorageDefaultKeyContent
     207            7 :       .key;
     208              : 
     209            1 :   Future<void> setDefaultKeyId(String keyId) async {
     210            2 :     await client.setAccountData(
     211            2 :       client.userID!,
     212              :       EventTypes.SecretStorageDefaultKey,
     213            2 :       SecretStorageDefaultKeyContent(key: keyId).toJson(),
     214              :     );
     215              :   }
     216              : 
     217            7 :   SecretStorageKeyContent? getKey(String keyId) {
     218           28 :     return client.accountData[EventTypes.secretStorageKey(keyId)]
     219            7 :         ?.parsedSecretStorageKeyContent;
     220              :   }
     221              : 
     222            2 :   bool isKeyValid(String keyId) =>
     223            6 :       getKey(keyId)?.algorithm == AlgorithmTypes.secretStorageV1AesHmcSha2;
     224              : 
     225              :   /// Creates a new secret storage key, optional encrypts it with [passphrase]
     226              :   /// and stores it in the user's `accountData`.
     227            2 :   Future<OpenSSSS> createKey([String? passphrase]) async {
     228              :     Uint8List privateKey;
     229            2 :     final content = SecretStorageKeyContent();
     230              :     if (passphrase != null) {
     231              :       // we need to derive the key off of the passphrase
     232            4 :       content.passphrase = PassphraseInfo(
     233              :         iterations: pbkdf2DefaultIterations,
     234            4 :         salt: base64.encode(uc.secureRandomBytes(pbkdf2SaltLength)),
     235              :         algorithm: AlgorithmTypes.pbkdf2,
     236            2 :         bits: ssssKeyLength * 8,
     237              :       );
     238            2 :       privateKey = await Future.value(
     239            6 :         client.nativeImplementations.keyFromPassphrase(
     240            2 :           KeyFromPassphraseArgs(
     241              :             passphrase: passphrase,
     242            2 :             info: content.passphrase!,
     243              :           ),
     244              :         ),
     245            4 :       ).timeout(Duration(seconds: 10));
     246              :     } else {
     247              :       // we need to just generate a new key from scratch
     248            2 :       privateKey = Uint8List.fromList(uc.secureRandomBytes(ssssKeyLength));
     249              :     }
     250              :     // now that we have the private key, let's create the iv and mac
     251            2 :     final encrypted = await encryptAes(zeroStr, privateKey, '');
     252            4 :     content.iv = encrypted.iv;
     253            4 :     content.mac = encrypted.mac;
     254            2 :     content.algorithm = AlgorithmTypes.secretStorageV1AesHmcSha2;
     255              : 
     256              :     const keyidByteLength = 24;
     257              : 
     258              :     // make sure we generate a unique key id
     259            2 :     final keyId = () sync* {
     260              :       for (;;) {
     261            4 :         yield base64.encode(uc.secureRandomBytes(keyidByteLength));
     262              :       }
     263            2 :     }()
     264            6 :         .firstWhere((keyId) => getKey(keyId) == null);
     265              : 
     266            2 :     final accountDataTypeKeyId = EventTypes.secretStorageKey(keyId);
     267              :     // noooow we set the account data
     268              : 
     269            4 :     await client.setAccountData(
     270            4 :       client.userID!,
     271              :       accountDataTypeKeyId,
     272            2 :       content.toJson(),
     273              :     );
     274              : 
     275            6 :     while (!client.accountData.containsKey(accountDataTypeKeyId)) {
     276            0 :       Logs().v('Waiting accountData to have $accountDataTypeKeyId');
     277            0 :       await client.oneShotSync();
     278              :     }
     279              : 
     280            2 :     final key = open(keyId);
     281            2 :     await key.setPrivateKey(privateKey);
     282              :     return key;
     283              :   }
     284              : 
     285            7 :   Future<bool> checkKey(Uint8List key, SecretStorageKeyContent info) async {
     286           14 :     if (info.algorithm == AlgorithmTypes.secretStorageV1AesHmcSha2) {
     287           28 :       if ((info.mac is String) && (info.iv is String)) {
     288           14 :         final encrypted = await encryptAes(zeroStr, key, '', info.iv);
     289           28 :         return info.mac!.replaceAll(RegExp(r'=+$'), '') ==
     290           21 :             encrypted.mac.replaceAll(RegExp(r'=+$'), '');
     291              :       } else {
     292              :         // no real information about the key, assume it is valid
     293              :         return true;
     294              :       }
     295              :     } else {
     296            0 :       throw InvalidPassphraseException('Unknown Algorithm');
     297              :     }
     298              :   }
     299              : 
     300           23 :   bool isSecret(String type) =>
     301          138 :       client.accountData[type]?.content['encrypted'] is Map;
     302              : 
     303           23 :   Future<String?> getCached(String type) async {
     304           46 :     if (client.database == null) {
     305              :       return null;
     306              :     }
     307              :     // check if it is still valid
     308           23 :     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           46 :     final fromCache = _cache[type];
     323            7 :     if (fromCache != null && isValid(fromCache)) {
     324            7 :       return fromCache.content;
     325              :     }
     326           69 :     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) && db != null) {
     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) && db != null) {
     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           24 :   Future<void> periodicallyRequestMissingCache() async {
     517           24 :     if (_isPeriodicallyRequestingMissingCache ||
     518           24 :         (_lastCacheRequest != null &&
     519            1 :             DateTime.now()
     520            2 :                 .subtract(Duration(minutes: 15))
     521            2 :                 .isBefore(_lastCacheRequest!)) ||
     522           48 :         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              :       if (db != null) {
     615            2 :         final keyId = keyIdFromType(request.type);
     616              :         if (keyId != null) {
     617            5 :           final ciphertext = (client.accountData[request.type]!.content
     618            1 :                   .tryGetMap<String, Object?>('encrypted'))
     619            1 :               ?.tryGetMap<String, Object?>(keyId)
     620            1 :               ?.tryGet<String>('ciphertext');
     621              :           if (ciphertext == null) {
     622            0 :             Logs().i('[SSSS] Ciphertext is empty or not a String');
     623              :             return;
     624              :           }
     625            2 :           await db.storeSSSSCache(request.type, keyId, ciphertext, secret);
     626            3 :           if (_cacheCallbacks.containsKey(request.type)) {
     627            4 :             _cacheCallbacks[request.type]!(secret);
     628              :           }
     629            2 :           onSecretStored.add(keyId);
     630              :         }
     631              :       }
     632              :     }
     633              :   }
     634              : 
     635           23 :   Set<String>? keyIdsFromType(String type) {
     636           69 :     final data = client.accountData[type];
     637              :     if (data == null) {
     638              :       return null;
     639              :     }
     640              :     final contentEncrypted =
     641           46 :         data.content.tryGetMap<String, Object?>('encrypted');
     642              :     if (contentEncrypted != null) {
     643           46 :       return contentEncrypted.keys.toSet();
     644              :     }
     645              :     return null;
     646              :   }
     647              : 
     648            7 :   String? keyIdFromType(String type) {
     649            7 :     final keys = keyIdsFromType(type);
     650            4 :     if (keys == null || keys.isEmpty) {
     651              :       return null;
     652              :     }
     653            8 :     if (keys.contains(defaultKeyId)) {
     654            4 :       return defaultKeyId;
     655              :     }
     656            0 :     return keys.first;
     657              :   }
     658              : 
     659            7 :   OpenSSSS open([String? identifier]) {
     660            4 :     identifier ??= defaultKeyId;
     661              :     if (identifier == null) {
     662            0 :       throw Exception('Dont know what to open');
     663              :     }
     664            7 :     final keyToOpen = keyIdFromType(identifier) ?? identifier;
     665            7 :     final key = getKey(keyToOpen);
     666              :     if (key == null) {
     667            0 :       throw Exception('Unknown key to open');
     668              :     }
     669            7 :     return OpenSSSS(ssss: this, keyId: keyToOpen, keyData: key);
     670              :   }
     671              : }
     672              : 
     673              : class _ShareRequest {
     674              :   final String requestId;
     675              :   final String type;
     676              :   final List<DeviceKeys> devices;
     677              :   final DateTime start;
     678              : 
     679            2 :   _ShareRequest({
     680              :     required this.requestId,
     681              :     required this.type,
     682              :     required this.devices,
     683            2 :   }) : start = DateTime.now();
     684              : }
     685              : 
     686              : class EncryptedContent {
     687              :   final String iv;
     688              :   final String ciphertext;
     689              :   final String mac;
     690              : 
     691            7 :   EncryptedContent({
     692              :     required this.iv,
     693              :     required this.ciphertext,
     694              :     required this.mac,
     695              :   });
     696              : }
     697              : 
     698              : class DerivedKeys {
     699              :   final Uint8List aesKey;
     700              :   final Uint8List hmacKey;
     701              : 
     702            7 :   DerivedKeys({required this.aesKey, required this.hmacKey});
     703              : }
     704              : 
     705              : class OpenSSSS {
     706              :   final SSSS ssss;
     707              :   final String keyId;
     708              :   final SecretStorageKeyContent keyData;
     709              : 
     710            7 :   OpenSSSS({required this.ssss, required this.keyId, required this.keyData});
     711              : 
     712              :   Uint8List? privateKey;
     713              : 
     714            4 :   bool get isUnlocked => privateKey != null;
     715              : 
     716            6 :   bool get hasPassphrase => keyData.passphrase != null;
     717              : 
     718            1 :   String? get recoveryKey =>
     719            3 :       isUnlocked ? SSSS.encodeRecoveryKey(privateKey!) : null;
     720              : 
     721            7 :   Future<void> unlock({
     722              :     String? passphrase,
     723              :     String? recoveryKey,
     724              :     String? keyOrPassphrase,
     725              :     bool postUnlock = true,
     726              :   }) async {
     727              :     if (keyOrPassphrase != null) {
     728              :       try {
     729            0 :         await unlock(recoveryKey: keyOrPassphrase, postUnlock: postUnlock);
     730              :       } catch (_) {
     731            0 :         if (hasPassphrase) {
     732            0 :           await unlock(passphrase: keyOrPassphrase, postUnlock: postUnlock);
     733              :         } else {
     734              :           rethrow;
     735              :         }
     736              :       }
     737              :       return;
     738              :     } else if (passphrase != null) {
     739            2 :       if (!hasPassphrase) {
     740            0 :         throw InvalidPassphraseException(
     741              :           'Tried to unlock with passphrase while key does not have a passphrase',
     742              :         );
     743              :       }
     744            4 :       privateKey = await Future.value(
     745            8 :         ssss.client.nativeImplementations.keyFromPassphrase(
     746            2 :           KeyFromPassphraseArgs(
     747              :             passphrase: passphrase,
     748            4 :             info: keyData.passphrase!,
     749              :           ),
     750              :         ),
     751            4 :       ).timeout(Duration(seconds: 10));
     752              :     } else if (recoveryKey != null) {
     753           12 :       privateKey = SSSS.decodeRecoveryKey(recoveryKey);
     754              :     } else {
     755            0 :       throw InvalidPassphraseException('Nothing specified');
     756              :     }
     757              :     // verify the validity of the key
     758           28 :     if (!await ssss.checkKey(privateKey!, keyData)) {
     759            1 :       privateKey = null;
     760            1 :       throw InvalidPassphraseException('Inalid key');
     761              :     }
     762              :     if (postUnlock) {
     763              :       try {
     764            6 :         await _postUnlock();
     765              :       } catch (e, s) {
     766            0 :         Logs().e('Error during post unlock', e, s);
     767              :       }
     768              :     }
     769              :   }
     770              : 
     771            2 :   Future<void> setPrivateKey(Uint8List key) async {
     772            6 :     if (!await ssss.checkKey(key, keyData)) {
     773            0 :       throw Exception('Invalid key');
     774              :     }
     775            2 :     privateKey = key;
     776              :   }
     777              : 
     778            4 :   Future<String> getStored(String type) async {
     779            4 :     final privateKey = this.privateKey;
     780              :     if (privateKey == null) {
     781            0 :       throw Exception('SSSS not unlocked');
     782              :     }
     783           12 :     return await ssss.getStored(type, keyId, privateKey);
     784              :   }
     785              : 
     786            1 :   Future<void> store(String type, String secret, {bool add = false}) async {
     787            1 :     final privateKey = this.privateKey;
     788              :     if (privateKey == null) {
     789            0 :       throw Exception('SSSS not unlocked');
     790              :     }
     791            3 :     await ssss.store(type, secret, keyId, privateKey, add: add);
     792            4 :     while (!ssss.client.accountData.containsKey(type) ||
     793            5 :         !(ssss.client.accountData[type]!.content
     794            1 :             .tryGetMap<String, Object?>('encrypted')!
     795            2 :             .containsKey(keyId)) ||
     796            2 :         await getStored(type) != secret) {
     797            0 :       Logs().d('Wait for secret of $type to match in accountdata');
     798            0 :       await ssss.client.oneShotSync();
     799              :     }
     800              :   }
     801              : 
     802            1 :   Future<void> validateAndStripOtherKeys(String type, String secret) async {
     803            1 :     final privateKey = this.privateKey;
     804              :     if (privateKey == null) {
     805            0 :       throw Exception('SSSS not unlocked');
     806              :     }
     807            3 :     await ssss.validateAndStripOtherKeys(type, secret, keyId, privateKey);
     808              :   }
     809              : 
     810            7 :   Future<void> maybeCacheAll() async {
     811            7 :     final privateKey = this.privateKey;
     812              :     if (privateKey == null) {
     813            0 :       throw Exception('SSSS not unlocked');
     814              :     }
     815           21 :     await ssss.maybeCacheAll(keyId, privateKey);
     816              :   }
     817              : 
     818            6 :   Future<void> _postUnlock() async {
     819              :     // first try to cache all secrets that aren't cached yet
     820            6 :     await maybeCacheAll();
     821              :     // now try to self-sign
     822           24 :     if (ssss.encryption.crossSigning.enabled &&
     823           48 :         ssss.client.userDeviceKeys[ssss.client.userID]?.masterKey != null &&
     824            6 :         (ssss
     825            6 :                 .keyIdsFromType(EventTypes.CrossSigningMasterKey)
     826           12 :                 ?.contains(keyId) ??
     827              :             false) &&
     828           18 :         (ssss.client.isUnknownSession ||
     829           32 :             ssss.client.userDeviceKeys[ssss.client.userID]!.masterKey
     830            8 :                     ?.directVerified !=
     831              :                 true)) {
     832              :       try {
     833           12 :         await ssss.encryption.crossSigning.selfSign(openSsss: this);
     834              :       } catch (e, s) {
     835            0 :         Logs().e('[SSSS] Failed to self-sign', e, s);
     836              :       }
     837              :     }
     838              :   }
     839              : }
     840              : 
     841              : class KeyFromPassphraseArgs {
     842              :   final String passphrase;
     843              :   final PassphraseInfo info;
     844              : 
     845            2 :   KeyFromPassphraseArgs({required this.passphrase, required this.info});
     846              : }
     847              : 
     848              : /// you would likely want to use [NativeImplementations] and
     849              : /// [Client.nativeImplementations] instead
     850            2 : Future<Uint8List> generateKeyFromPassphrase(KeyFromPassphraseArgs args) async {
     851            6 :   return await SSSS.keyFromPassphrase(args.passphrase, args.info);
     852              : }
     853              : 
     854              : class InvalidPassphraseException implements Exception {
     855              :   String cause;
     856            1 :   InvalidPassphraseException(this.cause);
     857              : }
        

Generated by: LCOV version 2.0-1