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); // we don't care about the failed secrets here
168 : final keys = <String>{};
169 3 : final defaultKeyId = encryption.ssss.defaultKeyId;
170 1 : int removeKey(String key) {
171 1 : final sizeBefore = secrets.length;
172 3 : secrets.removeWhere((k, v) => v.contains(key));
173 2 : return sizeBefore - secrets.length;
174 : }
175 :
176 : // first we want to try the default key id
177 : if (defaultKeyId != null) {
178 2 : if (removeKey(defaultKeyId) > 0) {
179 1 : keys.add(defaultKeyId);
180 : }
181 : }
182 : // now we re-try as long as we have keys for all secrets
183 1 : while (secrets.isNotEmpty) {
184 1 : final key = mostUsedKey(secrets);
185 1 : removeKey(key);
186 1 : keys.add(key);
187 : }
188 : return keys;
189 : }
190 :
191 1 : void wipeSsss(bool wipe) {
192 2 : if (state != BootstrapState.askWipeSsss) {
193 0 : throw BootstrapBadStateException('Wrong State');
194 : }
195 : if (wipe) {
196 1 : state = BootstrapState.askNewSsss;
197 3 : } else if (encryption.ssss.defaultKeyId != null &&
198 6 : encryption.ssss.isKeyValid(encryption.ssss.defaultKeyId!)) {
199 1 : state = BootstrapState.askUseExistingSsss;
200 2 : } else if (badSecrets().isNotEmpty) {
201 1 : state = BootstrapState.askBadSsss;
202 : } else {
203 0 : migrateOldSsss();
204 : }
205 : }
206 :
207 1 : void useExistingSsss(bool use) {
208 2 : if (state != BootstrapState.askUseExistingSsss) {
209 0 : throw BootstrapBadStateException('Wrong State');
210 : }
211 : if (use) {
212 : try {
213 0 : newSsssKey = encryption.ssss.open(encryption.ssss.defaultKeyId);
214 0 : state = BootstrapState.openExistingSsss;
215 : } catch (e, s) {
216 0 : Logs().e('[Bootstrapping] Error open SSSS', e, s);
217 0 : state = BootstrapState.error;
218 : return;
219 : }
220 2 : } else if (badSecrets().isNotEmpty) {
221 0 : state = BootstrapState.askBadSsss;
222 : } else {
223 1 : migrateOldSsss();
224 : }
225 : }
226 :
227 1 : void ignoreBadSecrets(bool ignore) {
228 2 : if (state != BootstrapState.askBadSsss) {
229 0 : throw BootstrapBadStateException('Wrong State');
230 : }
231 : if (ignore) {
232 0 : migrateOldSsss();
233 : } else {
234 : // that's it, folks. We can't do anything here
235 1 : state = BootstrapState.error;
236 : }
237 : }
238 :
239 1 : void migrateOldSsss() {
240 1 : final keys = allNeededKeys();
241 2 : final oldSsssKeys = this.oldSsssKeys = {};
242 : try {
243 2 : for (final key in keys) {
244 4 : oldSsssKeys[key] = encryption.ssss.open(key);
245 : }
246 : } catch (e, s) {
247 0 : Logs().e('[Bootstrapping] Error construction ssss key', e, s);
248 0 : state = BootstrapState.error;
249 : return;
250 : }
251 1 : state = BootstrapState.askUnlockSsss;
252 : }
253 :
254 1 : void unlockedSsss() {
255 2 : if (state != BootstrapState.askUnlockSsss) {
256 0 : throw BootstrapBadStateException('Wrong State');
257 : }
258 1 : state = BootstrapState.askNewSsss;
259 : }
260 :
261 1 : Future<void> newSsss([String? passphrase]) async {
262 2 : if (state != BootstrapState.askNewSsss) {
263 0 : throw BootstrapBadStateException('Wrong State');
264 : }
265 1 : state = BootstrapState.loading;
266 : try {
267 2 : Logs().v('Create key...');
268 4 : newSsssKey = await encryption.ssss.createKey(passphrase);
269 1 : if (oldSsssKeys != null) {
270 : // alright, we have to re-encrypt old secrets with the new key
271 1 : final secrets = analyzeSecrets();
272 1 : Set<String> removeKey(String key) {
273 1 : final s = secrets.entries
274 4 : .where((e) => e.value.contains(key))
275 3 : .map((e) => e.key)
276 1 : .toSet();
277 3 : secrets.removeWhere((k, v) => v.contains(key));
278 : return s;
279 : }
280 :
281 2 : secretMap = <String, String>{};
282 3 : for (final entry in oldSsssKeys!.entries) {
283 1 : final key = entry.value;
284 1 : final keyId = entry.key;
285 1 : if (!key.isUnlocked) {
286 : continue;
287 : }
288 2 : for (final s in removeKey(keyId)) {
289 3 : Logs().v('Get stored key of type $s...');
290 3 : secretMap![s] = await key.getStored(s);
291 2 : Logs().v('Store new secret with this key...');
292 4 : await newSsssKey!.store(s, secretMap![s]!, add: true);
293 : }
294 : }
295 : // alright, we re-encrypted all the secrets. We delete the dead weight only *after* we set our key to the default key
296 : }
297 5 : await encryption.ssss.setDefaultKeyId(newSsssKey!.keyId);
298 6 : while (encryption.ssss.defaultKeyId != newSsssKey!.keyId) {
299 0 : Logs().v(
300 : 'Waiting accountData to have the correct m.secret_storage.default_key');
301 0 : await client.oneShotSync();
302 : }
303 1 : if (oldSsssKeys != null) {
304 3 : for (final entry in secretMap!.entries) {
305 4 : Logs().v('Validate and stripe other keys ${entry.key}...');
306 4 : await newSsssKey!.validateAndStripOtherKeys(entry.key, entry.value);
307 : }
308 2 : Logs().v('And make super sure we have everything cached...');
309 2 : await newSsssKey!.maybeCacheAll();
310 : }
311 : } catch (e, s) {
312 0 : Logs().e('[Bootstrapping] Error trying to migrate old secrets', e, s);
313 0 : state = BootstrapState.error;
314 : return;
315 : }
316 : // alright, we successfully migrated all secrets, if needed
317 :
318 1 : checkCrossSigning();
319 : }
320 :
321 0 : Future<void> openExistingSsss() async {
322 0 : final newSsssKey = this.newSsssKey;
323 0 : if (state != BootstrapState.openExistingSsss || newSsssKey == null) {
324 0 : throw BootstrapBadStateException();
325 : }
326 0 : if (!newSsssKey.isUnlocked) {
327 0 : throw BootstrapBadStateException('Key not unlocked');
328 : }
329 0 : Logs().v('Maybe cache all...');
330 0 : await newSsssKey.maybeCacheAll();
331 0 : checkCrossSigning();
332 : }
333 :
334 1 : void checkCrossSigning() {
335 : // so, let's see if we have cross signing set up
336 3 : if (encryption.crossSigning.enabled) {
337 : // cross signing present, ask for wipe
338 1 : state = BootstrapState.askWipeCrossSigning;
339 : return;
340 : }
341 : // no cross signing present
342 1 : state = BootstrapState.askSetupCrossSigning;
343 : }
344 :
345 1 : Future<void> wipeCrossSigning(bool wipe) async {
346 2 : if (state != BootstrapState.askWipeCrossSigning) {
347 0 : throw BootstrapBadStateException();
348 : }
349 : if (wipe) {
350 1 : state = BootstrapState.askSetupCrossSigning;
351 : } else {
352 3 : await client.dehydratedDeviceSetup(newSsssKey!);
353 1 : checkOnlineKeyBackup();
354 : }
355 : }
356 :
357 1 : Future<void> askSetupCrossSigning(
358 : {bool setupMasterKey = false,
359 : bool setupSelfSigningKey = false,
360 : bool setupUserSigningKey = false}) async {
361 2 : if (state != BootstrapState.askSetupCrossSigning) {
362 0 : throw BootstrapBadStateException();
363 : }
364 : if (!setupMasterKey && !setupSelfSigningKey && !setupUserSigningKey) {
365 3 : await client.dehydratedDeviceSetup(newSsssKey!);
366 1 : checkOnlineKeyBackup();
367 : return;
368 : }
369 2 : final userID = client.userID!;
370 : try {
371 : Uint8List masterSigningKey;
372 1 : final secretsToStore = <String, String>{};
373 : MatrixCrossSigningKey? masterKey;
374 : MatrixCrossSigningKey? selfSigningKey;
375 : MatrixCrossSigningKey? userSigningKey;
376 : String? masterPub;
377 : if (setupMasterKey) {
378 1 : final master = olm.PkSigning();
379 : try {
380 1 : masterSigningKey = master.generate_seed();
381 1 : masterPub = master.init_with_seed(masterSigningKey);
382 1 : final json = <String, dynamic>{
383 : 'user_id': userID,
384 1 : 'usage': ['master'],
385 1 : 'keys': <String, dynamic>{
386 1 : 'ed25519:$masterPub': masterPub,
387 : },
388 : };
389 1 : masterKey = MatrixCrossSigningKey.fromJson(json);
390 1 : secretsToStore[EventTypes.CrossSigningMasterKey] =
391 1 : base64.encode(masterSigningKey);
392 : } finally {
393 1 : master.free();
394 : }
395 : } else {
396 0 : Logs().v('Get stored key...');
397 0 : masterSigningKey = base64decodeUnpadded(
398 0 : await newSsssKey?.getStored(EventTypes.CrossSigningMasterKey) ??
399 : '');
400 0 : if (masterSigningKey.isEmpty) {
401 : // no master signing key :(
402 0 : throw BootstrapBadStateException('No master key');
403 : }
404 0 : final master = olm.PkSigning();
405 : try {
406 0 : masterPub = master.init_with_seed(masterSigningKey);
407 : } finally {
408 0 : master.free();
409 : }
410 : }
411 1 : String? sign(Map<String, dynamic> object) {
412 1 : final keyObj = olm.PkSigning();
413 : try {
414 1 : keyObj.init_with_seed(masterSigningKey);
415 : return keyObj
416 3 : .sign(String.fromCharCodes(canonicalJson.encode(object)));
417 : } finally {
418 1 : keyObj.free();
419 : }
420 : }
421 :
422 : if (setupSelfSigningKey) {
423 1 : final selfSigning = olm.PkSigning();
424 : try {
425 1 : final selfSigningPriv = selfSigning.generate_seed();
426 1 : final selfSigningPub = selfSigning.init_with_seed(selfSigningPriv);
427 1 : final json = <String, dynamic>{
428 : 'user_id': userID,
429 1 : 'usage': ['self_signing'],
430 1 : 'keys': <String, dynamic>{
431 1 : 'ed25519:$selfSigningPub': selfSigningPub,
432 : },
433 : };
434 1 : final signature = sign(json);
435 2 : json['signatures'] = <String, dynamic>{
436 1 : userID: <String, dynamic>{
437 1 : 'ed25519:$masterPub': signature,
438 : },
439 : };
440 1 : selfSigningKey = MatrixCrossSigningKey.fromJson(json);
441 1 : secretsToStore[EventTypes.CrossSigningSelfSigning] =
442 1 : base64.encode(selfSigningPriv);
443 : } finally {
444 1 : selfSigning.free();
445 : }
446 : }
447 : if (setupUserSigningKey) {
448 1 : final userSigning = olm.PkSigning();
449 : try {
450 1 : final userSigningPriv = userSigning.generate_seed();
451 1 : final userSigningPub = userSigning.init_with_seed(userSigningPriv);
452 1 : final json = <String, dynamic>{
453 : 'user_id': userID,
454 1 : 'usage': ['user_signing'],
455 1 : 'keys': <String, dynamic>{
456 1 : 'ed25519:$userSigningPub': userSigningPub,
457 : },
458 : };
459 1 : final signature = sign(json);
460 2 : json['signatures'] = <String, dynamic>{
461 1 : userID: <String, dynamic>{
462 1 : 'ed25519:$masterPub': signature,
463 : },
464 : };
465 1 : userSigningKey = MatrixCrossSigningKey.fromJson(json);
466 1 : secretsToStore[EventTypes.CrossSigningUserSigning] =
467 1 : base64.encode(userSigningPriv);
468 : } finally {
469 1 : userSigning.free();
470 : }
471 : }
472 : // upload the keys!
473 1 : state = BootstrapState.loading;
474 2 : Logs().v('Upload device signing keys.');
475 2 : await client.uiaRequestBackground(
476 3 : (AuthenticationData? auth) => client.uploadCrossSigningKeys(
477 : masterKey: masterKey,
478 : selfSigningKey: selfSigningKey,
479 : userSigningKey: userSigningKey,
480 : auth: auth,
481 : ));
482 2 : Logs().v('Device signing keys have been uploaded.');
483 : // aaaand set the SSSS secrets
484 : if (masterKey != null) {
485 1 : while (!(masterKey.publicKey != null &&
486 8 : client.userDeviceKeys[client.userID]?.masterKey?.ed25519Key ==
487 1 : masterKey.publicKey)) {
488 0 : Logs().v('Waiting for master to be created');
489 0 : await client.oneShotSync();
490 : }
491 : }
492 1 : if (newSsssKey != null) {
493 1 : final storeFutures = <Future<void>>[];
494 2 : for (final entry in secretsToStore.entries) {
495 5 : storeFutures.add(newSsssKey!.store(entry.key, entry.value));
496 : }
497 2 : Logs().v('Store new SSSS key entries...');
498 1 : await Future.wait(storeFutures);
499 : }
500 :
501 1 : final keysToSign = <SignableKey>[];
502 : if (masterKey != null) {
503 8 : if (client.userDeviceKeys[client.userID]?.masterKey?.ed25519Key !=
504 1 : masterKey.publicKey) {
505 0 : throw BootstrapBadStateException(
506 : 'ERROR: New master key does not match up!');
507 : }
508 2 : Logs().v('Set own master key to verified...');
509 6 : await client.userDeviceKeys[client.userID]!.masterKey!
510 1 : .setVerified(true, false);
511 7 : keysToSign.add(client.userDeviceKeys[client.userID]!.masterKey!);
512 : }
513 : if (selfSigningKey != null) {
514 1 : keysToSign.add(
515 9 : client.userDeviceKeys[client.userID]!.deviceKeys[client.deviceID]!);
516 : }
517 2 : Logs().v('Sign ourself...');
518 3 : await encryption.crossSigning.sign(keysToSign);
519 : } catch (e, s) {
520 0 : Logs().e('[Bootstrapping] Error setting up cross signing', e, s);
521 0 : state = BootstrapState.error;
522 : return;
523 : }
524 :
525 3 : await client.dehydratedDeviceSetup(newSsssKey!);
526 1 : checkOnlineKeyBackup();
527 : }
528 :
529 1 : void checkOnlineKeyBackup() {
530 : // check if we have online key backup set up
531 3 : if (encryption.keyManager.enabled) {
532 1 : state = BootstrapState.askWipeOnlineKeyBackup;
533 : return;
534 : }
535 1 : state = BootstrapState.askSetupOnlineKeyBackup;
536 : }
537 :
538 1 : void wipeOnlineKeyBackup(bool wipe) {
539 2 : if (state != BootstrapState.askWipeOnlineKeyBackup) {
540 0 : throw BootstrapBadStateException();
541 : }
542 : if (wipe) {
543 1 : state = BootstrapState.askSetupOnlineKeyBackup;
544 : } else {
545 1 : state = BootstrapState.done;
546 : }
547 : }
548 :
549 1 : Future<void> askSetupOnlineKeyBackup(bool setup) async {
550 2 : if (state != BootstrapState.askSetupOnlineKeyBackup) {
551 0 : throw BootstrapBadStateException();
552 : }
553 : if (!setup) {
554 1 : state = BootstrapState.done;
555 : return;
556 : }
557 : try {
558 1 : final keyObj = olm.PkDecryption();
559 : String pubKey;
560 : Uint8List privKey;
561 : try {
562 1 : pubKey = keyObj.generate_key();
563 1 : privKey = keyObj.get_private_key();
564 : } finally {
565 1 : keyObj.free();
566 : }
567 2 : Logs().v('Create the new backup version...');
568 2 : await client.postRoomKeysVersion(
569 : BackupAlgorithm.mMegolmBackupV1Curve25519AesSha2,
570 1 : <String, dynamic>{
571 : 'public_key': pubKey,
572 : },
573 : );
574 2 : Logs().v('Store the secret...');
575 3 : await newSsssKey?.store(megolmKey, base64.encode(privKey));
576 :
577 2 : Logs().v(
578 : 'And finally set all megolm keys as needing to be uploaded again...');
579 3 : await client.database?.markInboundGroupSessionsAsNeedingUpload();
580 2 : Logs().v('And uploading keys...');
581 4 : await client.encryption?.keyManager.uploadInboundGroupSessions();
582 : } catch (e, s) {
583 0 : Logs().e('[Bootstrapping] Error setting up online key backup', e, s);
584 0 : state = BootstrapState.error;
585 0 : encryption.client.onEncryptionError.add(
586 0 : SdkError(exception: e, stackTrace: s),
587 : );
588 : return;
589 : }
590 1 : state = BootstrapState.done;
591 : }
592 :
593 1 : set state(BootstrapState newState) {
594 3 : Logs().v('BootstrapState: $newState');
595 2 : if (state != BootstrapState.error) {
596 1 : _state = newState;
597 : }
598 :
599 2 : onUpdate?.call(this);
600 : }
601 : }
602 :
603 : class BootstrapBadStateException implements Exception {
604 : String cause;
605 0 : BootstrapBadStateException([this.cause = 'Bad state']);
606 :
607 0 : @override
608 0 : String toString() => 'BootstrapBadStateException: $cause';
609 : }
|