LCOV - code coverage report
Current view: top level - lib/encryption - ssss.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 338 381 88.7 %
Date: 2024-11-12 07:37:08 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 1.14