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