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 : }
|