LCOV - code coverage report
Current view: top level - lib/encryption - ssss.dart (source / functions) Coverage Total Hit
Test: merged.info Lines: 88.7 % 380 337
Test Date: 2025-05-24 18:14:12 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           75 :   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           25 :   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           25 :   void setValidator(String type, FutureOr<bool> Function(String) validator) {
     197           50 :     _validators[type] = validator;
     198              :   }
     199              : 
     200           25 :   void setCacheCallback(String type, FutureOr<void> Function(String) callback) {
     201           50 :     _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           25 :   bool isSecret(String type) =>
     301          150 :       client.accountData[type]?.content['encrypted'] is Map;
     302              : 
     303           25 :   Future<String?> getCached(String type) async {
     304           25 :     final keys = keyIdsFromType(type);
     305              :     if (keys == null) {
     306              :       return null;
     307              :     }
     308            7 :     bool isValid(SSSSCache dbEntry) =>
     309           14 :         keys.contains(dbEntry.keyId) &&
     310            7 :         dbEntry.ciphertext != null &&
     311            7 :         dbEntry.keyId != null &&
     312           28 :         client.accountData[type]?.content
     313            7 :                 .tryGetMap<String, Object?>('encrypted')
     314           14 :                 ?.tryGetMap<String, Object?>(dbEntry.keyId!)
     315           14 :                 ?.tryGet<String>('ciphertext') ==
     316            7 :             dbEntry.ciphertext;
     317              : 
     318           50 :     final fromCache = _cache[type];
     319            7 :     if (fromCache != null && isValid(fromCache)) {
     320            7 :       return fromCache.content;
     321              :     }
     322           75 :     final ret = await client.database.getSSSSCache(type);
     323              :     if (ret == null) {
     324              :       return null;
     325              :     }
     326            7 :     if (isValid(ret)) {
     327           14 :       _cache[type] = ret;
     328            7 :       return ret.content;
     329              :     }
     330              :     return null;
     331              :   }
     332              : 
     333            7 :   Future<String> getStored(String type, String keyId, Uint8List key) async {
     334           21 :     final secretInfo = client.accountData[type];
     335              :     if (secretInfo == null) {
     336            1 :       throw Exception('Not found');
     337              :     }
     338              :     final encryptedContent =
     339           14 :         secretInfo.content.tryGetMap<String, Object?>('encrypted');
     340              :     if (encryptedContent == null) {
     341            0 :       throw Exception('Content is not encrypted');
     342              :     }
     343            7 :     final enc = encryptedContent.tryGetMap<String, Object?>(keyId);
     344              :     if (enc == null) {
     345            0 :       throw Exception('Wrong / unknown key: $type, $keyId');
     346              :     }
     347            7 :     final ciphertext = enc.tryGet<String>('ciphertext');
     348            7 :     final iv = enc.tryGet<String>('iv');
     349            7 :     final mac = enc.tryGet<String>('mac');
     350              :     if (ciphertext == null || iv == null || mac == null) {
     351            0 :       throw Exception('Wrong types for encrypted content or missing keys.');
     352              :     }
     353            7 :     final encryptInfo = EncryptedContent(
     354              :       iv: iv,
     355              :       ciphertext: ciphertext,
     356              :       mac: mac,
     357              :     );
     358            7 :     final decrypted = await decryptAes(encryptInfo, key, type);
     359           14 :     final db = client.database;
     360            7 :     if (cacheTypes.contains(type)) {
     361              :       // cache the thing
     362            7 :       await db.storeSSSSCache(type, keyId, ciphertext, decrypted);
     363           14 :       onSecretStored.add(keyId);
     364           21 :       if (_cacheCallbacks.containsKey(type) && await getCached(type) == null) {
     365            0 :         _cacheCallbacks[type]!(decrypted);
     366              :       }
     367              :     }
     368              :     return decrypted;
     369              :   }
     370              : 
     371            2 :   Future<void> store(
     372              :     String type,
     373              :     String secret,
     374              :     String keyId,
     375              :     Uint8List key, {
     376              :     bool add = false,
     377              :   }) async {
     378            2 :     final encrypted = await encryptAes(secret, key, type);
     379              :     Map<String, dynamic>? content;
     380            3 :     if (add && client.accountData[type] != null) {
     381            5 :       content = client.accountData[type]!.content.copy();
     382            2 :       if (content['encrypted'] is! Map) {
     383            0 :         content['encrypted'] = <String, dynamic>{};
     384              :       }
     385              :     }
     386            2 :     content ??= <String, dynamic>{
     387            2 :       'encrypted': <String, dynamic>{},
     388              :     };
     389            6 :     content['encrypted'][keyId] = <String, dynamic>{
     390            2 :       'iv': encrypted.iv,
     391            2 :       'ciphertext': encrypted.ciphertext,
     392            2 :       'mac': encrypted.mac,
     393              :     };
     394              :     // store the thing in your account data
     395            8 :     await client.setAccountData(client.userID!, type, content);
     396            4 :     final db = client.database;
     397            2 :     if (cacheTypes.contains(type)) {
     398              :       // cache the thing
     399            2 :       await db.storeSSSSCache(type, keyId, encrypted.ciphertext, secret);
     400            2 :       onSecretStored.add(keyId);
     401            3 :       if (_cacheCallbacks.containsKey(type) && await getCached(type) == null) {
     402            0 :         _cacheCallbacks[type]!(secret);
     403              :       }
     404              :     }
     405              :   }
     406              : 
     407            1 :   Future<void> validateAndStripOtherKeys(
     408              :     String type,
     409              :     String secret,
     410              :     String keyId,
     411              :     Uint8List key,
     412              :   ) async {
     413            2 :     if (await getStored(type, keyId, key) != secret) {
     414            0 :       throw Exception('Secrets do not match up!');
     415              :     }
     416              :     // now remove all other keys
     417            5 :     final content = client.accountData[type]?.content.copy();
     418              :     if (content == null) {
     419            0 :       throw InvalidPassphraseException('Key has no content!');
     420              :     }
     421            1 :     final encryptedContent = content.tryGetMap<String, Object?>('encrypted');
     422              :     if (encryptedContent == null) {
     423            0 :       throw Exception('Wrong type for encrypted content!');
     424              :     }
     425              : 
     426              :     final otherKeys =
     427            5 :         Set<String>.from(encryptedContent.keys.where((k) => k != keyId));
     428            3 :     encryptedContent.removeWhere((k, v) => otherKeys.contains(k));
     429              :     // yes, we are paranoid...
     430            2 :     if (await getStored(type, keyId, key) != secret) {
     431            0 :       throw Exception('Secrets do not match up!');
     432              :     }
     433              :     // store the thing in your account data
     434            4 :     await client.setAccountData(client.userID!, type, content);
     435            1 :     if (cacheTypes.contains(type)) {
     436              :       // cache the thing
     437              :       final ciphertext = encryptedContent
     438            1 :           .tryGetMap<String, Object?>(keyId)
     439            1 :           ?.tryGet<String>('ciphertext');
     440              :       if (ciphertext == null) {
     441            0 :         throw Exception('Wrong type for ciphertext!');
     442              :       }
     443            3 :       await client.database.storeSSSSCache(type, keyId, ciphertext, secret);
     444            2 :       onSecretStored.add(keyId);
     445              :     }
     446              :   }
     447              : 
     448            7 :   Future<void> maybeCacheAll(String keyId, Uint8List key) async {
     449           14 :     for (final type in cacheTypes) {
     450            7 :       final secret = await getCached(type);
     451              :       if (secret == null) {
     452              :         try {
     453            7 :           await getStored(type, keyId, key);
     454              :         } catch (_) {
     455              :           // the entry wasn't stored, just ignore it
     456              :         }
     457              :       }
     458              :     }
     459              :   }
     460              : 
     461            2 :   Future<void> maybeRequestAll([List<DeviceKeys>? devices]) async {
     462            4 :     for (final type in cacheTypes) {
     463            2 :       if (keyIdsFromType(type) != null) {
     464            2 :         final secret = await getCached(type);
     465              :         if (secret == null) {
     466            2 :           await request(type, devices);
     467              :         }
     468              :       }
     469              :     }
     470              :   }
     471              : 
     472            2 :   Future<void> request(String type, [List<DeviceKeys>? devices]) async {
     473              :     // only send to own, verified devices
     474            6 :     Logs().i('[SSSS] Requesting type $type...');
     475            2 :     if (devices == null || devices.isEmpty) {
     476            5 :       if (!client.userDeviceKeys.containsKey(client.userID)) {
     477            0 :         Logs().w('[SSSS] User does not have any devices');
     478              :         return;
     479              :       }
     480              :       devices =
     481            8 :           client.userDeviceKeys[client.userID]!.deviceKeys.values.toList();
     482              :     }
     483            2 :     devices.removeWhere(
     484            2 :       (DeviceKeys d) =>
     485            8 :           d.userId != client.userID ||
     486            2 :           !d.verified ||
     487            2 :           d.blocked ||
     488            8 :           d.deviceId == client.deviceID,
     489              :     );
     490            2 :     if (devices.isEmpty) {
     491            0 :       Logs().w('[SSSS] No devices');
     492              :       return;
     493              :     }
     494            4 :     final requestId = client.generateUniqueTransactionId();
     495            2 :     final request = _ShareRequest(
     496              :       requestId: requestId,
     497              :       type: type,
     498              :       devices: devices,
     499              :     );
     500            4 :     pendingShareRequests[requestId] = request;
     501            6 :     await client.sendToDeviceEncrypted(devices, EventTypes.SecretRequest, {
     502              :       'action': 'request',
     503            4 :       'requesting_device_id': client.deviceID,
     504              :       'request_id': requestId,
     505              :       'name': type,
     506              :     });
     507              :   }
     508              : 
     509              :   DateTime? _lastCacheRequest;
     510              :   bool _isPeriodicallyRequestingMissingCache = false;
     511              : 
     512           25 :   Future<void> periodicallyRequestMissingCache() async {
     513           25 :     if (_isPeriodicallyRequestingMissingCache ||
     514           25 :         (_lastCacheRequest != null &&
     515            1 :             DateTime.now()
     516            2 :                 .subtract(Duration(minutes: 15))
     517            2 :                 .isBefore(_lastCacheRequest!)) ||
     518           50 :         client.isUnknownSession) {
     519              :       // we are already requesting right now or we attempted to within the last 15 min
     520              :       return;
     521              :     }
     522            2 :     _lastCacheRequest = DateTime.now();
     523            1 :     _isPeriodicallyRequestingMissingCache = true;
     524              :     try {
     525            1 :       await maybeRequestAll();
     526              :     } finally {
     527            1 :       _isPeriodicallyRequestingMissingCache = false;
     528              :     }
     529              :   }
     530              : 
     531            1 :   Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
     532            2 :     if (event.type == EventTypes.SecretRequest) {
     533              :       // got a request to share a secret
     534            2 :       Logs().i('[SSSS] Received sharing request...');
     535            4 :       if (event.sender != client.userID ||
     536            5 :           !client.userDeviceKeys.containsKey(client.userID)) {
     537            2 :         Logs().i('[SSSS] Not sent by us');
     538              :         return; // we aren't asking for it ourselves, so ignore
     539              :       }
     540            3 :       if (event.content['action'] != 'request') {
     541            2 :         Logs().i('[SSSS] it is actually a cancelation');
     542              :         return; // not actually requesting, so ignore
     543              :       }
     544            5 :       final device = client.userDeviceKeys[client.userID]!
     545            4 :           .deviceKeys[event.content['requesting_device_id']];
     546            2 :       if (device == null || !device.verified || device.blocked) {
     547            2 :         Logs().i('[SSSS] Unknown / unverified devices, ignoring');
     548              :         return; // nope....unknown or untrusted device
     549              :       }
     550              :       // alright, all seems fine...let's check if we actually have the secret they are asking for
     551            2 :       final type = event.content.tryGet<String>('name');
     552              :       if (type == null) {
     553            0 :         Logs().i('[SSSS] Wrong data type for type param, ignoring');
     554              :         return;
     555              :       }
     556            1 :       final secret = await getCached(type);
     557              :       if (secret == null) {
     558            1 :         Logs()
     559            2 :             .i('[SSSS] We don\'t have the secret for $type ourself, ignoring');
     560              :         return; // seems like we don't have this, either
     561              :       }
     562              :       // okay, all checks out...time to share this secret!
     563            3 :       Logs().i('[SSSS] Replying with secret for $type');
     564            2 :       await client.sendToDeviceEncrypted(
     565            1 :           [device],
     566              :           EventTypes.SecretSend,
     567            1 :           {
     568            2 :             'request_id': event.content['request_id'],
     569              :             'secret': secret,
     570              :           });
     571            2 :     } else if (event.type == EventTypes.SecretSend) {
     572              :       // receiving a secret we asked for
     573            2 :       Logs().i('[SSSS] Received shared secret...');
     574            1 :       final encryptedContent = event.encryptedContent;
     575            4 :       if (event.sender != client.userID ||
     576            4 :           !pendingShareRequests.containsKey(event.content['request_id']) ||
     577              :           encryptedContent == null) {
     578            2 :         Logs().i('[SSSS] Not by us or unknown request');
     579              :         return; // we have no idea what we just received
     580              :       }
     581            4 :       final request = pendingShareRequests[event.content['request_id']]!;
     582              :       // alright, as we received a known request id, let's check if the sender is valid
     583            2 :       final device = request.devices.firstWhereOrNull(
     584            1 :         (d) =>
     585            3 :             d.userId == event.sender &&
     586            3 :             d.curve25519Key == encryptedContent['sender_key'],
     587              :       );
     588              :       if (device == null) {
     589            2 :         Logs().i('[SSSS] Someone else replied?');
     590              :         return; // someone replied whom we didn't send the share request to
     591              :       }
     592            2 :       final secret = event.content.tryGet<String>('secret');
     593              :       if (secret == null) {
     594            2 :         Logs().i('[SSSS] Secret wasn\'t a string');
     595              :         return; // the secret wasn't a string....wut?
     596              :       }
     597              :       // let's validate if the secret is, well, valid
     598            3 :       if (_validators.containsKey(request.type) &&
     599            4 :           !(await _validators[request.type]!(secret))) {
     600            2 :         Logs().i('[SSSS] The received secret was invalid');
     601              :         return; // didn't pass the validator
     602              :       }
     603            3 :       pendingShareRequests.remove(request.requestId);
     604            5 :       if (request.start.add(Duration(minutes: 15)).isBefore(DateTime.now())) {
     605            0 :         Logs().i('[SSSS] Request is too far in the past');
     606              :         return; // our request is more than 15min in the past...better not trust it anymore
     607              :       }
     608            4 :       Logs().i('[SSSS] Secret for type ${request.type} is ok, storing it');
     609            2 :       final db = client.database;
     610            2 :       final keyId = keyIdFromType(request.type);
     611              :       if (keyId != null) {
     612            5 :         final ciphertext = (client.accountData[request.type]!.content
     613            1 :                 .tryGetMap<String, Object?>('encrypted'))
     614            1 :             ?.tryGetMap<String, Object?>(keyId)
     615            1 :             ?.tryGet<String>('ciphertext');
     616              :         if (ciphertext == null) {
     617            0 :           Logs().i('[SSSS] Ciphertext is empty or not a String');
     618              :           return;
     619              :         }
     620            2 :         await db.storeSSSSCache(request.type, keyId, ciphertext, secret);
     621            3 :         if (_cacheCallbacks.containsKey(request.type)) {
     622            4 :           _cacheCallbacks[request.type]!(secret);
     623              :         }
     624            2 :         onSecretStored.add(keyId);
     625              :       }
     626              :     }
     627              :   }
     628              : 
     629           25 :   Set<String>? keyIdsFromType(String type) {
     630           75 :     final data = client.accountData[type];
     631              :     if (data == null) {
     632              :       return null;
     633              :     }
     634              :     final contentEncrypted =
     635           50 :         data.content.tryGetMap<String, Object?>('encrypted');
     636              :     if (contentEncrypted != null) {
     637           50 :       return contentEncrypted.keys.toSet();
     638              :     }
     639              :     return null;
     640              :   }
     641              : 
     642            7 :   String? keyIdFromType(String type) {
     643            7 :     final keys = keyIdsFromType(type);
     644            4 :     if (keys == null || keys.isEmpty) {
     645              :       return null;
     646              :     }
     647            8 :     if (keys.contains(defaultKeyId)) {
     648            4 :       return defaultKeyId;
     649              :     }
     650            0 :     return keys.first;
     651              :   }
     652              : 
     653            7 :   OpenSSSS open([String? identifier]) {
     654            4 :     identifier ??= defaultKeyId;
     655              :     if (identifier == null) {
     656            0 :       throw Exception('Dont know what to open');
     657              :     }
     658            7 :     final keyToOpen = keyIdFromType(identifier) ?? identifier;
     659            7 :     final key = getKey(keyToOpen);
     660              :     if (key == null) {
     661            0 :       throw Exception('Unknown key to open');
     662              :     }
     663            7 :     return OpenSSSS(ssss: this, keyId: keyToOpen, keyData: key);
     664              :   }
     665              : }
     666              : 
     667              : class _ShareRequest {
     668              :   final String requestId;
     669              :   final String type;
     670              :   final List<DeviceKeys> devices;
     671              :   final DateTime start;
     672              : 
     673            2 :   _ShareRequest({
     674              :     required this.requestId,
     675              :     required this.type,
     676              :     required this.devices,
     677            2 :   }) : start = DateTime.now();
     678              : }
     679              : 
     680              : class EncryptedContent {
     681              :   final String iv;
     682              :   final String ciphertext;
     683              :   final String mac;
     684              : 
     685            7 :   EncryptedContent({
     686              :     required this.iv,
     687              :     required this.ciphertext,
     688              :     required this.mac,
     689              :   });
     690              : }
     691              : 
     692              : class DerivedKeys {
     693              :   final Uint8List aesKey;
     694              :   final Uint8List hmacKey;
     695              : 
     696            7 :   DerivedKeys({required this.aesKey, required this.hmacKey});
     697              : }
     698              : 
     699              : class OpenSSSS {
     700              :   final SSSS ssss;
     701              :   final String keyId;
     702              :   final SecretStorageKeyContent keyData;
     703              : 
     704            7 :   OpenSSSS({required this.ssss, required this.keyId, required this.keyData});
     705              : 
     706              :   Uint8List? privateKey;
     707              : 
     708            4 :   bool get isUnlocked => privateKey != null;
     709              : 
     710            6 :   bool get hasPassphrase => keyData.passphrase != null;
     711              : 
     712            1 :   String? get recoveryKey =>
     713            3 :       isUnlocked ? SSSS.encodeRecoveryKey(privateKey!) : null;
     714              : 
     715            7 :   Future<void> unlock({
     716              :     String? passphrase,
     717              :     String? recoveryKey,
     718              :     String? keyOrPassphrase,
     719              :     bool postUnlock = true,
     720              :   }) async {
     721              :     if (keyOrPassphrase != null) {
     722              :       try {
     723            0 :         await unlock(recoveryKey: keyOrPassphrase, postUnlock: postUnlock);
     724              :       } catch (_) {
     725            0 :         if (hasPassphrase) {
     726            0 :           await unlock(passphrase: keyOrPassphrase, postUnlock: postUnlock);
     727              :         } else {
     728              :           rethrow;
     729              :         }
     730              :       }
     731              :       return;
     732              :     } else if (passphrase != null) {
     733            2 :       if (!hasPassphrase) {
     734            0 :         throw InvalidPassphraseException(
     735              :           'Tried to unlock with passphrase while key does not have a passphrase',
     736              :         );
     737              :       }
     738            4 :       privateKey = await Future.value(
     739            8 :         ssss.client.nativeImplementations.keyFromPassphrase(
     740            2 :           KeyFromPassphraseArgs(
     741              :             passphrase: passphrase,
     742            4 :             info: keyData.passphrase!,
     743              :           ),
     744              :         ),
     745            4 :       ).timeout(Duration(seconds: 10));
     746              :     } else if (recoveryKey != null) {
     747           12 :       privateKey = SSSS.decodeRecoveryKey(recoveryKey);
     748              :     } else {
     749            0 :       throw InvalidPassphraseException('Nothing specified');
     750              :     }
     751              :     // verify the validity of the key
     752           28 :     if (!await ssss.checkKey(privateKey!, keyData)) {
     753            1 :       privateKey = null;
     754            1 :       throw InvalidPassphraseException('Inalid key');
     755              :     }
     756              :     if (postUnlock) {
     757              :       try {
     758            6 :         await _postUnlock();
     759              :       } catch (e, s) {
     760            0 :         Logs().e('Error during post unlock', e, s);
     761              :       }
     762              :     }
     763              :   }
     764              : 
     765            2 :   Future<void> setPrivateKey(Uint8List key) async {
     766            6 :     if (!await ssss.checkKey(key, keyData)) {
     767            0 :       throw Exception('Invalid key');
     768              :     }
     769            2 :     privateKey = key;
     770              :   }
     771              : 
     772            4 :   Future<String> getStored(String type) async {
     773            4 :     final privateKey = this.privateKey;
     774              :     if (privateKey == null) {
     775            0 :       throw Exception('SSSS not unlocked');
     776              :     }
     777           12 :     return await ssss.getStored(type, keyId, privateKey);
     778              :   }
     779              : 
     780            1 :   Future<void> store(String type, String secret, {bool add = false}) async {
     781            1 :     final privateKey = this.privateKey;
     782              :     if (privateKey == null) {
     783            0 :       throw Exception('SSSS not unlocked');
     784              :     }
     785            3 :     await ssss.store(type, secret, keyId, privateKey, add: add);
     786            4 :     while (!ssss.client.accountData.containsKey(type) ||
     787            5 :         !(ssss.client.accountData[type]!.content
     788            1 :             .tryGetMap<String, Object?>('encrypted')!
     789            2 :             .containsKey(keyId)) ||
     790            2 :         await getStored(type) != secret) {
     791            0 :       Logs().d('Wait for secret of $type to match in accountdata');
     792            0 :       await ssss.client.oneShotSync();
     793              :     }
     794              :   }
     795              : 
     796            1 :   Future<void> validateAndStripOtherKeys(String type, String secret) async {
     797            1 :     final privateKey = this.privateKey;
     798              :     if (privateKey == null) {
     799            0 :       throw Exception('SSSS not unlocked');
     800              :     }
     801            3 :     await ssss.validateAndStripOtherKeys(type, secret, keyId, privateKey);
     802              :   }
     803              : 
     804            7 :   Future<void> maybeCacheAll() async {
     805            7 :     final privateKey = this.privateKey;
     806              :     if (privateKey == null) {
     807            0 :       throw Exception('SSSS not unlocked');
     808              :     }
     809           21 :     await ssss.maybeCacheAll(keyId, privateKey);
     810              :   }
     811              : 
     812            6 :   Future<void> _postUnlock() async {
     813              :     // first try to cache all secrets that aren't cached yet
     814            6 :     await maybeCacheAll();
     815              :     // now try to self-sign
     816           24 :     if (ssss.encryption.crossSigning.enabled &&
     817           48 :         ssss.client.userDeviceKeys[ssss.client.userID]?.masterKey != null &&
     818            6 :         (ssss
     819            6 :                 .keyIdsFromType(EventTypes.CrossSigningMasterKey)
     820           12 :                 ?.contains(keyId) ??
     821              :             false) &&
     822           18 :         (ssss.client.isUnknownSession ||
     823           32 :             ssss.client.userDeviceKeys[ssss.client.userID]!.masterKey
     824            8 :                     ?.directVerified !=
     825              :                 true)) {
     826              :       try {
     827           12 :         await ssss.encryption.crossSigning.selfSign(openSsss: this);
     828              :       } catch (e, s) {
     829            0 :         Logs().e('[SSSS] Failed to self-sign', e, s);
     830              :       }
     831              :     }
     832              :   }
     833              : }
     834              : 
     835              : class KeyFromPassphraseArgs {
     836              :   final String passphrase;
     837              :   final PassphraseInfo info;
     838              : 
     839            2 :   KeyFromPassphraseArgs({required this.passphrase, required this.info});
     840              : }
     841              : 
     842              : /// you would likely want to use [NativeImplementations] and
     843              : /// [Client.nativeImplementations] instead
     844            2 : Future<Uint8List> generateKeyFromPassphrase(KeyFromPassphraseArgs args) async {
     845            6 :   return await SSSS.keyFromPassphrase(args.passphrase, args.info);
     846              : }
     847              : 
     848              : class InvalidPassphraseException implements Exception {
     849              :   String cause;
     850            1 :   InvalidPassphraseException(this.cause);
     851              : }
        

Generated by: LCOV version 2.0-1