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