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:convert';
20 : import 'dart:typed_data';
21 :
22 : import 'package:canonical_json/canonical_json.dart';
23 : import 'package:olm/olm.dart' as olm;
24 :
25 : import 'package:matrix/encryption/encryption.dart';
26 : import 'package:matrix/encryption/key_manager.dart';
27 : import 'package:matrix/encryption/ssss.dart';
28 : import 'package:matrix/encryption/utils/base64_unpadded.dart';
29 : import 'package:matrix/matrix.dart';
30 :
31 : enum BootstrapState {
32 : /// Is loading.
33 : loading,
34 :
35 : /// Existing SSSS found, should we wipe it?
36 : askWipeSsss,
37 :
38 : /// Ask if an existing SSSS should be userDeviceKeys
39 : askUseExistingSsss,
40 :
41 : /// Ask to unlock all the SSSS keys
42 : askUnlockSsss,
43 :
44 : /// SSSS is in a bad state, continue with potential dataloss?
45 : askBadSsss,
46 :
47 : /// Ask for new SSSS key / passphrase
48 : askNewSsss,
49 :
50 : /// Open an existing SSSS key
51 : openExistingSsss,
52 :
53 : /// Ask if cross signing should be wiped
54 : askWipeCrossSigning,
55 :
56 : /// Ask if cross signing should be set up
57 : askSetupCrossSigning,
58 :
59 : /// Ask if online key backup should be wiped
60 : askWipeOnlineKeyBackup,
61 :
62 : /// Ask if the online key backup should be set up
63 : askSetupOnlineKeyBackup,
64 :
65 : /// An error has been occured.
66 : error,
67 :
68 : /// done
69 : done,
70 : }
71 :
72 : /// Bootstrapping SSSS and cross-signing
73 : class Bootstrap {
74 : final Encryption encryption;
75 3 : Client get client => encryption.client;
76 : void Function(Bootstrap)? onUpdate;
77 2 : BootstrapState get state => _state;
78 : BootstrapState _state = BootstrapState.loading;
79 : Map<String, OpenSSSS>? oldSsssKeys;
80 : OpenSSSS? newSsssKey;
81 : Map<String, String>? secretMap;
82 :
83 1 : Bootstrap({required this.encryption, this.onUpdate}) {
84 2 : if (analyzeSecrets().isNotEmpty) {
85 1 : state = BootstrapState.askWipeSsss;
86 : } else {
87 1 : state = BootstrapState.askNewSsss;
88 : }
89 : }
90 :
91 : // cache the secret analyzing so that we don't drop stuff a different client sets during bootstrapping
92 : Map<String, Set<String>>? _secretsCache;
93 :
94 : /// returns ssss from accountdata, eg: m.megolm_backup.v1, or your m.cross_signing stuff
95 1 : Map<String, Set<String>> analyzeSecrets() {
96 1 : final secretsCache = _secretsCache;
97 : if (secretsCache != null) {
98 : // deep-copy so that we can do modifications
99 1 : final newSecrets = <String, Set<String>>{};
100 2 : for (final s in secretsCache.entries) {
101 4 : newSecrets[s.key] = Set<String>.from(s.value);
102 : }
103 : return newSecrets;
104 : }
105 1 : final secrets = <String, Set<String>>{};
106 4 : for (final entry in client.accountData.entries) {
107 1 : final type = entry.key;
108 1 : final event = entry.value;
109 : final encryptedContent =
110 2 : event.content.tryGetMap<String, Object?>('encrypted');
111 : if (encryptedContent == null) {
112 : continue;
113 : }
114 : final validKeys = <String>{};
115 : final invalidKeys = <String>{};
116 2 : for (final keyEntry in encryptedContent.entries) {
117 1 : final key = keyEntry.key;
118 1 : final value = keyEntry.value;
119 1 : if (value is! Map) {
120 : // we don't add the key to invalidKeys as this was not a proper secret anyways!
121 : continue;
122 : }
123 2 : if (value['iv'] is! String ||
124 2 : value['ciphertext'] is! String ||
125 2 : value['mac'] is! String) {
126 0 : invalidKeys.add(key);
127 : continue;
128 : }
129 3 : if (!encryption.ssss.isKeyValid(key)) {
130 1 : invalidKeys.add(key);
131 : continue;
132 : }
133 1 : validKeys.add(key);
134 : }
135 2 : if (validKeys.isEmpty && invalidKeys.isEmpty) {
136 : continue; // this didn't contain any keys anyways!
137 : }
138 : // if there are no valid keys and only invalid keys then the validKeys set will be empty
139 : // from that we know that there were errors with this secret and that we won't be able to migrate it
140 1 : secrets[type] = validKeys;
141 : }
142 1 : _secretsCache = secrets;
143 1 : return analyzeSecrets();
144 : }
145 :
146 1 : Set<String> badSecrets() {
147 1 : final secrets = analyzeSecrets();
148 3 : secrets.removeWhere((k, v) => v.isNotEmpty);
149 2 : return Set<String>.from(secrets.keys);
150 : }
151 :
152 1 : String mostUsedKey(Map<String, Set<String>> secrets) {
153 1 : final usage = <String, int>{};
154 2 : for (final keys in secrets.values) {
155 2 : for (final key in keys) {
156 2 : usage.update(key, (i) => i + 1, ifAbsent: () => 1);
157 : }
158 : }
159 2 : final entriesList = usage.entries.toList();
160 1 : entriesList.sort((a, b) => a.value.compareTo(b.value));
161 2 : return entriesList.first.key;
162 : }
163 :
164 1 : Set<String> allNeededKeys() {
165 1 : final secrets = analyzeSecrets();
166 1 : secrets.removeWhere(
167 2 : (k, v) => v.isEmpty,
168 : ); // we don't care about the failed secrets here
169 : final keys = <String>{};
170 3 : final defaultKeyId = encryption.ssss.defaultKeyId;
171 1 : int removeKey(String key) {
172 1 : final sizeBefore = secrets.length;
173 3 : secrets.removeWhere((k, v) => v.contains(key));
174 2 : return sizeBefore - secrets.length;
175 : }
176 :
177 : // first we want to try the default key id
178 : if (defaultKeyId != null) {
179 2 : if (removeKey(defaultKeyId) > 0) {
180 1 : keys.add(defaultKeyId);
181 : }
182 : }
183 : // now we re-try as long as we have keys for all secrets
184 1 : while (secrets.isNotEmpty) {
185 1 : final key = mostUsedKey(secrets);
186 1 : removeKey(key);
187 1 : keys.add(key);
188 : }
189 : return keys;
190 : }
191 :
192 1 : void wipeSsss(bool wipe) {
193 2 : if (state != BootstrapState.askWipeSsss) {
194 0 : throw BootstrapBadStateException('Wrong State');
195 : }
196 : if (wipe) {
197 1 : state = BootstrapState.askNewSsss;
198 3 : } else if (encryption.ssss.defaultKeyId != null &&
199 6 : encryption.ssss.isKeyValid(encryption.ssss.defaultKeyId!)) {
200 1 : state = BootstrapState.askUseExistingSsss;
201 2 : } else if (badSecrets().isNotEmpty) {
202 1 : state = BootstrapState.askBadSsss;
203 : } else {
204 0 : migrateOldSsss();
205 : }
206 : }
207 :
208 1 : void useExistingSsss(bool use) {
209 2 : if (state != BootstrapState.askUseExistingSsss) {
210 0 : throw BootstrapBadStateException('Wrong State');
211 : }
212 : if (use) {
213 : try {
214 0 : newSsssKey = encryption.ssss.open(encryption.ssss.defaultKeyId);
215 0 : state = BootstrapState.openExistingSsss;
216 : } catch (e, s) {
217 0 : Logs().e('[Bootstrapping] Error open SSSS', e, s);
218 0 : state = BootstrapState.error;
219 : return;
220 : }
221 2 : } else if (badSecrets().isNotEmpty) {
222 0 : state = BootstrapState.askBadSsss;
223 : } else {
224 1 : migrateOldSsss();
225 : }
226 : }
227 :
228 1 : void ignoreBadSecrets(bool ignore) {
229 2 : if (state != BootstrapState.askBadSsss) {
230 0 : throw BootstrapBadStateException('Wrong State');
231 : }
232 : if (ignore) {
233 0 : migrateOldSsss();
234 : } else {
235 : // that's it, folks. We can't do anything here
236 1 : state = BootstrapState.error;
237 : }
238 : }
239 :
240 1 : void migrateOldSsss() {
241 1 : final keys = allNeededKeys();
242 2 : final oldSsssKeys = this.oldSsssKeys = {};
243 : try {
244 2 : for (final key in keys) {
245 4 : oldSsssKeys[key] = encryption.ssss.open(key);
246 : }
247 : } catch (e, s) {
248 0 : Logs().e('[Bootstrapping] Error construction ssss key', e, s);
249 0 : state = BootstrapState.error;
250 : return;
251 : }
252 1 : state = BootstrapState.askUnlockSsss;
253 : }
254 :
255 1 : void unlockedSsss() {
256 2 : if (state != BootstrapState.askUnlockSsss) {
257 0 : throw BootstrapBadStateException('Wrong State');
258 : }
259 1 : state = BootstrapState.askNewSsss;
260 : }
261 :
262 1 : Future<void> newSsss([String? passphrase]) async {
263 2 : if (state != BootstrapState.askNewSsss) {
264 0 : throw BootstrapBadStateException('Wrong State');
265 : }
266 1 : state = BootstrapState.loading;
267 : try {
268 2 : Logs().v('Create key...');
269 4 : newSsssKey = await encryption.ssss.createKey(passphrase);
270 1 : if (oldSsssKeys != null) {
271 : // alright, we have to re-encrypt old secrets with the new key
272 1 : final secrets = analyzeSecrets();
273 1 : Set<String> removeKey(String key) {
274 1 : final s = secrets.entries
275 4 : .where((e) => e.value.contains(key))
276 3 : .map((e) => e.key)
277 1 : .toSet();
278 3 : secrets.removeWhere((k, v) => v.contains(key));
279 : return s;
280 : }
281 :
282 2 : secretMap = <String, String>{};
283 3 : for (final entry in oldSsssKeys!.entries) {
284 1 : final key = entry.value;
285 1 : final keyId = entry.key;
286 1 : if (!key.isUnlocked) {
287 : continue;
288 : }
289 2 : for (final s in removeKey(keyId)) {
290 3 : Logs().v('Get stored key of type $s...');
291 3 : secretMap![s] = await key.getStored(s);
292 2 : Logs().v('Store new secret with this key...');
293 4 : await newSsssKey!.store(s, secretMap![s]!, add: true);
294 : }
295 : }
296 : // alright, we re-encrypted all the secrets. We delete the dead weight only *after* we set our key to the default key
297 : }
298 5 : await encryption.ssss.setDefaultKeyId(newSsssKey!.keyId);
299 6 : while (encryption.ssss.defaultKeyId != newSsssKey!.keyId) {
300 0 : Logs().v(
301 : 'Waiting accountData to have the correct m.secret_storage.default_key',
302 : );
303 0 : await client.oneShotSync();
304 : }
305 1 : if (oldSsssKeys != null) {
306 3 : for (final entry in secretMap!.entries) {
307 4 : Logs().v('Validate and stripe other keys ${entry.key}...');
308 4 : await newSsssKey!.validateAndStripOtherKeys(entry.key, entry.value);
309 : }
310 2 : Logs().v('And make super sure we have everything cached...');
311 2 : await newSsssKey!.maybeCacheAll();
312 : }
313 : } catch (e, s) {
314 0 : Logs().e('[Bootstrapping] Error trying to migrate old secrets', e, s);
315 0 : state = BootstrapState.error;
316 : return;
317 : }
318 : // alright, we successfully migrated all secrets, if needed
319 :
320 1 : checkCrossSigning();
321 : }
322 :
323 0 : Future<void> openExistingSsss() async {
324 0 : final newSsssKey = this.newSsssKey;
325 0 : if (state != BootstrapState.openExistingSsss || newSsssKey == null) {
326 0 : throw BootstrapBadStateException();
327 : }
328 0 : if (!newSsssKey.isUnlocked) {
329 0 : throw BootstrapBadStateException('Key not unlocked');
330 : }
331 0 : Logs().v('Maybe cache all...');
332 0 : await newSsssKey.maybeCacheAll();
333 0 : checkCrossSigning();
334 : }
335 :
336 1 : void checkCrossSigning() {
337 : // so, let's see if we have cross signing set up
338 3 : if (encryption.crossSigning.enabled) {
339 : // cross signing present, ask for wipe
340 1 : state = BootstrapState.askWipeCrossSigning;
341 : return;
342 : }
343 : // no cross signing present
344 1 : state = BootstrapState.askSetupCrossSigning;
345 : }
346 :
347 1 : Future<void> wipeCrossSigning(bool wipe) async {
348 2 : if (state != BootstrapState.askWipeCrossSigning) {
349 0 : throw BootstrapBadStateException();
350 : }
351 : if (wipe) {
352 1 : state = BootstrapState.askSetupCrossSigning;
353 : } else {
354 3 : await client.dehydratedDeviceSetup(newSsssKey!);
355 1 : checkOnlineKeyBackup();
356 : }
357 : }
358 :
359 1 : Future<void> askSetupCrossSigning({
360 : bool setupMasterKey = false,
361 : bool setupSelfSigningKey = false,
362 : bool setupUserSigningKey = false,
363 : }) async {
364 2 : if (state != BootstrapState.askSetupCrossSigning) {
365 0 : throw BootstrapBadStateException();
366 : }
367 : if (!setupMasterKey && !setupSelfSigningKey && !setupUserSigningKey) {
368 3 : await client.dehydratedDeviceSetup(newSsssKey!);
369 1 : checkOnlineKeyBackup();
370 : return;
371 : }
372 2 : final userID = client.userID!;
373 : try {
374 : Uint8List masterSigningKey;
375 1 : final secretsToStore = <String, String>{};
376 : MatrixCrossSigningKey? masterKey;
377 : MatrixCrossSigningKey? selfSigningKey;
378 : MatrixCrossSigningKey? userSigningKey;
379 : String? masterPub;
380 : if (setupMasterKey) {
381 1 : final master = olm.PkSigning();
382 : try {
383 1 : masterSigningKey = master.generate_seed();
384 1 : masterPub = master.init_with_seed(masterSigningKey);
385 1 : final json = <String, dynamic>{
386 : 'user_id': userID,
387 1 : 'usage': ['master'],
388 1 : 'keys': <String, dynamic>{
389 1 : 'ed25519:$masterPub': masterPub,
390 : },
391 : };
392 1 : masterKey = MatrixCrossSigningKey.fromJson(json);
393 1 : secretsToStore[EventTypes.CrossSigningMasterKey] =
394 1 : base64.encode(masterSigningKey);
395 : } finally {
396 1 : master.free();
397 : }
398 : } else {
399 0 : Logs().v('Get stored key...');
400 0 : masterSigningKey = base64decodeUnpadded(
401 0 : await newSsssKey?.getStored(EventTypes.CrossSigningMasterKey) ?? '',
402 : );
403 0 : if (masterSigningKey.isEmpty) {
404 : // no master signing key :(
405 0 : throw BootstrapBadStateException('No master key');
406 : }
407 0 : final master = olm.PkSigning();
408 : try {
409 0 : masterPub = master.init_with_seed(masterSigningKey);
410 : } finally {
411 0 : master.free();
412 : }
413 : }
414 1 : String? sign(Map<String, dynamic> object) {
415 1 : final keyObj = olm.PkSigning();
416 : try {
417 1 : keyObj.init_with_seed(masterSigningKey);
418 : return keyObj
419 3 : .sign(String.fromCharCodes(canonicalJson.encode(object)));
420 : } finally {
421 1 : keyObj.free();
422 : }
423 : }
424 :
425 : if (setupSelfSigningKey) {
426 1 : final selfSigning = olm.PkSigning();
427 : try {
428 1 : final selfSigningPriv = selfSigning.generate_seed();
429 1 : final selfSigningPub = selfSigning.init_with_seed(selfSigningPriv);
430 1 : final json = <String, dynamic>{
431 : 'user_id': userID,
432 1 : 'usage': ['self_signing'],
433 1 : 'keys': <String, dynamic>{
434 1 : 'ed25519:$selfSigningPub': selfSigningPub,
435 : },
436 : };
437 1 : final signature = sign(json);
438 2 : json['signatures'] = <String, dynamic>{
439 1 : userID: <String, dynamic>{
440 1 : 'ed25519:$masterPub': signature,
441 : },
442 : };
443 1 : selfSigningKey = MatrixCrossSigningKey.fromJson(json);
444 1 : secretsToStore[EventTypes.CrossSigningSelfSigning] =
445 1 : base64.encode(selfSigningPriv);
446 : } finally {
447 1 : selfSigning.free();
448 : }
449 : }
450 : if (setupUserSigningKey) {
451 1 : final userSigning = olm.PkSigning();
452 : try {
453 1 : final userSigningPriv = userSigning.generate_seed();
454 1 : final userSigningPub = userSigning.init_with_seed(userSigningPriv);
455 1 : final json = <String, dynamic>{
456 : 'user_id': userID,
457 1 : 'usage': ['user_signing'],
458 1 : 'keys': <String, dynamic>{
459 1 : 'ed25519:$userSigningPub': userSigningPub,
460 : },
461 : };
462 1 : final signature = sign(json);
463 2 : json['signatures'] = <String, dynamic>{
464 1 : userID: <String, dynamic>{
465 1 : 'ed25519:$masterPub': signature,
466 : },
467 : };
468 1 : userSigningKey = MatrixCrossSigningKey.fromJson(json);
469 1 : secretsToStore[EventTypes.CrossSigningUserSigning] =
470 1 : base64.encode(userSigningPriv);
471 : } finally {
472 1 : userSigning.free();
473 : }
474 : }
475 : // upload the keys!
476 1 : state = BootstrapState.loading;
477 2 : Logs().v('Upload device signing keys.');
478 2 : await client.uiaRequestBackground(
479 3 : (AuthenticationData? auth) => client.uploadCrossSigningKeys(
480 : masterKey: masterKey,
481 : selfSigningKey: selfSigningKey,
482 : userSigningKey: userSigningKey,
483 : auth: auth,
484 : ),
485 : );
486 2 : Logs().v('Device signing keys have been uploaded.');
487 : // aaaand set the SSSS secrets
488 : if (masterKey != null) {
489 1 : while (!(masterKey.publicKey != null &&
490 8 : client.userDeviceKeys[client.userID]?.masterKey?.ed25519Key ==
491 1 : masterKey.publicKey)) {
492 0 : Logs().v('Waiting for master to be created');
493 0 : await client.oneShotSync();
494 : }
495 : }
496 1 : if (newSsssKey != null) {
497 1 : final storeFutures = <Future<void>>[];
498 2 : for (final entry in secretsToStore.entries) {
499 5 : storeFutures.add(newSsssKey!.store(entry.key, entry.value));
500 : }
501 2 : Logs().v('Store new SSSS key entries...');
502 1 : await Future.wait(storeFutures);
503 : }
504 :
505 1 : final keysToSign = <SignableKey>[];
506 : if (masterKey != null) {
507 8 : if (client.userDeviceKeys[client.userID]?.masterKey?.ed25519Key !=
508 1 : masterKey.publicKey) {
509 0 : throw BootstrapBadStateException(
510 : 'ERROR: New master key does not match up!',
511 : );
512 : }
513 2 : Logs().v('Set own master key to verified...');
514 6 : await client.userDeviceKeys[client.userID]!.masterKey!
515 1 : .setVerified(true, false);
516 7 : keysToSign.add(client.userDeviceKeys[client.userID]!.masterKey!);
517 : }
518 : if (selfSigningKey != null) {
519 1 : keysToSign.add(
520 9 : client.userDeviceKeys[client.userID]!.deviceKeys[client.deviceID]!,
521 : );
522 : }
523 2 : Logs().v('Sign ourself...');
524 3 : await encryption.crossSigning.sign(keysToSign);
525 : } catch (e, s) {
526 0 : Logs().e('[Bootstrapping] Error setting up cross signing', e, s);
527 0 : state = BootstrapState.error;
528 : return;
529 : }
530 :
531 3 : await client.dehydratedDeviceSetup(newSsssKey!);
532 1 : checkOnlineKeyBackup();
533 : }
534 :
535 1 : void checkOnlineKeyBackup() {
536 : // check if we have online key backup set up
537 3 : if (encryption.keyManager.enabled) {
538 1 : state = BootstrapState.askWipeOnlineKeyBackup;
539 : return;
540 : }
541 1 : state = BootstrapState.askSetupOnlineKeyBackup;
542 : }
543 :
544 1 : void wipeOnlineKeyBackup(bool wipe) {
545 2 : if (state != BootstrapState.askWipeOnlineKeyBackup) {
546 0 : throw BootstrapBadStateException();
547 : }
548 : if (wipe) {
549 1 : state = BootstrapState.askSetupOnlineKeyBackup;
550 : } else {
551 1 : state = BootstrapState.done;
552 : }
553 : }
554 :
555 1 : Future<void> askSetupOnlineKeyBackup(bool setup) async {
556 2 : if (state != BootstrapState.askSetupOnlineKeyBackup) {
557 0 : throw BootstrapBadStateException();
558 : }
559 : if (!setup) {
560 1 : state = BootstrapState.done;
561 : return;
562 : }
563 : try {
564 1 : final keyObj = olm.PkDecryption();
565 : String pubKey;
566 : Uint8List privKey;
567 : try {
568 1 : pubKey = keyObj.generate_key();
569 1 : privKey = keyObj.get_private_key();
570 : } finally {
571 1 : keyObj.free();
572 : }
573 2 : Logs().v('Create the new backup version...');
574 2 : await client.postRoomKeysVersion(
575 : BackupAlgorithm.mMegolmBackupV1Curve25519AesSha2,
576 1 : <String, dynamic>{
577 : 'public_key': pubKey,
578 : },
579 : );
580 2 : Logs().v('Store the secret...');
581 3 : await newSsssKey?.store(megolmKey, base64.encode(privKey));
582 :
583 2 : Logs().v(
584 : 'And finally set all megolm keys as needing to be uploaded again...',
585 : );
586 3 : await client.database?.markInboundGroupSessionsAsNeedingUpload();
587 2 : Logs().v('And uploading keys...');
588 4 : await client.encryption?.keyManager.uploadInboundGroupSessions();
589 : } catch (e, s) {
590 0 : Logs().e('[Bootstrapping] Error setting up online key backup', e, s);
591 0 : state = BootstrapState.error;
592 0 : encryption.client.onEncryptionError.add(
593 0 : SdkError(exception: e, stackTrace: s),
594 : );
595 : return;
596 : }
597 1 : state = BootstrapState.done;
598 : }
599 :
600 1 : set state(BootstrapState newState) {
601 3 : Logs().v('BootstrapState: $newState');
602 2 : if (state != BootstrapState.error) {
603 1 : _state = newState;
604 : }
605 :
606 2 : onUpdate?.call(this);
607 : }
608 : }
609 :
610 : class BootstrapBadStateException implements Exception {
611 : String cause;
612 0 : BootstrapBadStateException([this.cause = 'Bad state']);
613 :
614 0 : @override
615 0 : String toString() => 'BootstrapBadStateException: $cause';
616 : }
|