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