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:vodozemac/vodozemac.dart' as vod;
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/matrix.dart';
29 :
30 : enum BootstrapState {
31 : /// Is loading.
32 : loading,
33 :
34 : /// Existing SSSS found, should we wipe it?
35 : askWipeSsss,
36 :
37 : /// Ask if an existing SSSS should be userDeviceKeys
38 : askUseExistingSsss,
39 :
40 : /// Ask to unlock all the SSSS keys
41 : askUnlockSsss,
42 :
43 : /// SSSS is in a bad state, continue with potential dataloss?
44 : askBadSsss,
45 :
46 : /// Ask for new SSSS key / passphrase
47 : askNewSsss,
48 :
49 : /// Open an existing SSSS key
50 : openExistingSsss,
51 :
52 : /// Ask if cross signing should be wiped
53 : askWipeCrossSigning,
54 :
55 : /// Ask if cross signing should be set up
56 : askSetupCrossSigning,
57 :
58 : /// Ask if online key backup should be wiped
59 : askWipeOnlineKeyBackup,
60 :
61 : /// Ask if the online key backup should be set up
62 : askSetupOnlineKeyBackup,
63 :
64 : /// An error has been occured.
65 : error,
66 :
67 : /// done
68 : done,
69 : }
70 :
71 : /// Bootstrapping SSSS and cross-signing
72 : class Bootstrap {
73 : final Encryption encryption;
74 3 : Client get client => encryption.client;
75 : void Function(Bootstrap)? onUpdate;
76 2 : BootstrapState get state => _state;
77 : BootstrapState _state = BootstrapState.loading;
78 : Map<String, OpenSSSS>? oldSsssKeys;
79 : OpenSSSS? newSsssKey;
80 : Map<String, String>? secretMap;
81 :
82 1 : Bootstrap({required this.encryption, this.onUpdate}) {
83 2 : if (analyzeSecrets().isNotEmpty) {
84 1 : state = BootstrapState.askWipeSsss;
85 : } else {
86 1 : state = BootstrapState.askNewSsss;
87 : }
88 : }
89 :
90 : // cache the secret analyzing so that we don't drop stuff a different client sets during bootstrapping
91 : Map<String, Set<String>>? _secretsCache;
92 :
93 : /// returns ssss from accountdata, eg: m.megolm_backup.v1, or your m.cross_signing stuff
94 1 : Map<String, Set<String>> analyzeSecrets() {
95 1 : final secretsCache = _secretsCache;
96 : if (secretsCache != null) {
97 : // deep-copy so that we can do modifications
98 1 : final newSecrets = <String, Set<String>>{};
99 2 : for (final s in secretsCache.entries) {
100 4 : newSecrets[s.key] = Set<String>.from(s.value);
101 : }
102 : return newSecrets;
103 : }
104 1 : final secrets = <String, Set<String>>{};
105 4 : for (final entry in client.accountData.entries) {
106 1 : final type = entry.key;
107 1 : final event = entry.value;
108 : final encryptedContent =
109 2 : event.content.tryGetMap<String, Object?>('encrypted');
110 : if (encryptedContent == null) {
111 : continue;
112 : }
113 : final validKeys = <String>{};
114 : final invalidKeys = <String>{};
115 2 : for (final keyEntry in encryptedContent.entries) {
116 1 : final key = keyEntry.key;
117 1 : final value = keyEntry.value;
118 1 : if (value is! Map) {
119 : // we don't add the key to invalidKeys as this was not a proper secret anyways!
120 : continue;
121 : }
122 2 : if (value['iv'] is! String ||
123 2 : value['ciphertext'] is! String ||
124 2 : value['mac'] is! String) {
125 0 : invalidKeys.add(key);
126 : continue;
127 : }
128 3 : if (!encryption.ssss.isKeyValid(key)) {
129 1 : invalidKeys.add(key);
130 : continue;
131 : }
132 1 : validKeys.add(key);
133 : }
134 2 : if (validKeys.isEmpty && invalidKeys.isEmpty) {
135 : continue; // this didn't contain any keys anyways!
136 : }
137 : // if there are no valid keys and only invalid keys then the validKeys set will be empty
138 : // from that we know that there were errors with this secret and that we won't be able to migrate it
139 1 : secrets[type] = validKeys;
140 : }
141 1 : _secretsCache = secrets;
142 1 : return analyzeSecrets();
143 : }
144 :
145 1 : Set<String> badSecrets() {
146 1 : final secrets = analyzeSecrets();
147 3 : secrets.removeWhere((k, v) => v.isNotEmpty);
148 2 : return Set<String>.from(secrets.keys);
149 : }
150 :
151 1 : String mostUsedKey(Map<String, Set<String>> secrets) {
152 1 : final usage = <String, int>{};
153 2 : for (final keys in secrets.values) {
154 2 : for (final key in keys) {
155 2 : usage.update(key, (i) => i + 1, ifAbsent: () => 1);
156 : }
157 : }
158 2 : final entriesList = usage.entries.toList();
159 1 : entriesList.sort((a, b) => a.value.compareTo(b.value));
160 2 : return entriesList.first.key;
161 : }
162 :
163 1 : Set<String> allNeededKeys() {
164 1 : final secrets = analyzeSecrets();
165 1 : secrets.removeWhere(
166 2 : (k, v) => v.isEmpty,
167 : ); // 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 : );
302 0 : await client.oneShotSync();
303 : }
304 1 : if (oldSsssKeys != null) {
305 3 : for (final entry in secretMap!.entries) {
306 4 : Logs().v('Validate and stripe other keys ${entry.key}...');
307 4 : await newSsssKey!.validateAndStripOtherKeys(entry.key, entry.value);
308 : }
309 2 : Logs().v('And make super sure we have everything cached...');
310 2 : await newSsssKey!.maybeCacheAll();
311 : }
312 : } catch (e, s) {
313 0 : Logs().e('[Bootstrapping] Error trying to migrate old secrets', e, s);
314 0 : state = BootstrapState.error;
315 : return;
316 : }
317 : // alright, we successfully migrated all secrets, if needed
318 :
319 1 : checkCrossSigning();
320 : }
321 :
322 0 : Future<void> openExistingSsss() async {
323 0 : final newSsssKey = this.newSsssKey;
324 0 : if (state != BootstrapState.openExistingSsss || newSsssKey == null) {
325 0 : throw BootstrapBadStateException();
326 : }
327 0 : if (!newSsssKey.isUnlocked) {
328 0 : throw BootstrapBadStateException('Key not unlocked');
329 : }
330 0 : Logs().v('Maybe cache all...');
331 0 : await newSsssKey.maybeCacheAll();
332 0 : checkCrossSigning();
333 : }
334 :
335 1 : void checkCrossSigning() {
336 : // so, let's see if we have cross signing set up
337 3 : if (encryption.crossSigning.enabled) {
338 : // cross signing present, ask for wipe
339 1 : state = BootstrapState.askWipeCrossSigning;
340 : return;
341 : }
342 : // no cross signing present
343 1 : state = BootstrapState.askSetupCrossSigning;
344 : }
345 :
346 1 : Future<void> wipeCrossSigning(bool wipe) async {
347 2 : if (state != BootstrapState.askWipeCrossSigning) {
348 0 : throw BootstrapBadStateException();
349 : }
350 : if (wipe) {
351 1 : state = BootstrapState.askSetupCrossSigning;
352 : } else {
353 3 : await client.dehydratedDeviceSetup(newSsssKey!);
354 1 : checkOnlineKeyBackup();
355 : }
356 : }
357 :
358 1 : Future<void> askSetupCrossSigning({
359 : bool setupMasterKey = false,
360 : bool setupSelfSigningKey = false,
361 : bool setupUserSigningKey = false,
362 : }) async {
363 2 : if (state != BootstrapState.askSetupCrossSigning) {
364 0 : throw BootstrapBadStateException();
365 : }
366 : if (!setupMasterKey && !setupSelfSigningKey && !setupUserSigningKey) {
367 3 : await client.dehydratedDeviceSetup(newSsssKey!);
368 1 : checkOnlineKeyBackup();
369 : return;
370 : }
371 2 : final userID = client.userID!;
372 : try {
373 : String masterSigningKey;
374 1 : final secretsToStore = <String, String>{};
375 : MatrixCrossSigningKey? masterKey;
376 : MatrixCrossSigningKey? selfSigningKey;
377 : MatrixCrossSigningKey? userSigningKey;
378 : String? masterPub;
379 : if (setupMasterKey) {
380 1 : final master = vod.PkSigning();
381 1 : masterSigningKey = master.secretKey;
382 2 : masterPub = master.publicKey.toBase64();
383 1 : final json = <String, dynamic>{
384 : 'user_id': userID,
385 1 : 'usage': ['master'],
386 1 : 'keys': <String, dynamic>{
387 1 : 'ed25519:$masterPub': masterPub,
388 : },
389 : };
390 1 : masterKey = MatrixCrossSigningKey.fromJson(json);
391 1 : secretsToStore[EventTypes.CrossSigningMasterKey] = masterSigningKey;
392 : } else {
393 0 : Logs().v('Get stored key...');
394 : masterSigningKey =
395 0 : await newSsssKey?.getStored(EventTypes.CrossSigningMasterKey) ?? '';
396 0 : if (masterSigningKey.isEmpty) {
397 : // no master signing key :(
398 0 : throw BootstrapBadStateException('No master key');
399 : }
400 0 : final master = vod.PkSigning.fromSecretKey(masterSigningKey);
401 0 : masterPub = master.publicKey.toBase64();
402 : }
403 1 : String? sign(Map<String, dynamic> object) {
404 1 : final keyObj = vod.PkSigning.fromSecretKey(masterSigningKey);
405 : return keyObj
406 3 : .sign(String.fromCharCodes(canonicalJson.encode(object)))
407 1 : .toBase64();
408 : }
409 :
410 : if (setupSelfSigningKey) {
411 1 : final selfSigning = vod.PkSigning();
412 1 : final selfSigningPriv = selfSigning.secretKey;
413 2 : final selfSigningPub = selfSigning.publicKey.toBase64();
414 1 : final json = <String, dynamic>{
415 : 'user_id': userID,
416 1 : 'usage': ['self_signing'],
417 1 : 'keys': <String, dynamic>{
418 1 : 'ed25519:$selfSigningPub': selfSigningPub,
419 : },
420 : };
421 1 : final signature = sign(json);
422 2 : json['signatures'] = <String, dynamic>{
423 1 : userID: <String, dynamic>{
424 1 : 'ed25519:$masterPub': signature,
425 : },
426 : };
427 1 : selfSigningKey = MatrixCrossSigningKey.fromJson(json);
428 1 : secretsToStore[EventTypes.CrossSigningSelfSigning] = selfSigningPriv;
429 : }
430 : if (setupUserSigningKey) {
431 1 : final userSigning = vod.PkSigning();
432 1 : final userSigningPriv = userSigning.secretKey;
433 2 : final userSigningPub = userSigning.publicKey.toBase64();
434 1 : final json = <String, dynamic>{
435 : 'user_id': userID,
436 1 : 'usage': ['user_signing'],
437 1 : 'keys': <String, dynamic>{
438 1 : 'ed25519:$userSigningPub': userSigningPub,
439 : },
440 : };
441 1 : final signature = sign(json);
442 2 : json['signatures'] = <String, dynamic>{
443 1 : userID: <String, dynamic>{
444 1 : 'ed25519:$masterPub': signature,
445 : },
446 : };
447 1 : userSigningKey = MatrixCrossSigningKey.fromJson(json);
448 1 : secretsToStore[EventTypes.CrossSigningUserSigning] = userSigningPriv;
449 : }
450 : // upload the keys!
451 1 : state = BootstrapState.loading;
452 2 : Logs().v('Upload device signing keys.');
453 2 : await client.uiaRequestBackground(
454 3 : (AuthenticationData? auth) => client.uploadCrossSigningKeys(
455 : masterKey: masterKey,
456 : selfSigningKey: selfSigningKey,
457 : userSigningKey: userSigningKey,
458 : auth: auth,
459 : ),
460 : );
461 2 : Logs().v('Device signing keys have been uploaded.');
462 : // aaaand set the SSSS secrets
463 : if (masterKey != null) {
464 1 : while (!(masterKey.publicKey != null &&
465 8 : client.userDeviceKeys[client.userID]?.masterKey?.ed25519Key ==
466 1 : masterKey.publicKey)) {
467 0 : Logs().v('Waiting for master to be created');
468 0 : await client.oneShotSync();
469 : }
470 : }
471 1 : if (newSsssKey != null) {
472 1 : final storeFutures = <Future<void>>[];
473 2 : for (final entry in secretsToStore.entries) {
474 5 : storeFutures.add(newSsssKey!.store(entry.key, entry.value));
475 : }
476 2 : Logs().v('Store new SSSS key entries...');
477 1 : await Future.wait(storeFutures);
478 : }
479 :
480 1 : final keysToSign = <SignableKey>[];
481 : if (masterKey != null) {
482 8 : if (client.userDeviceKeys[client.userID]?.masterKey?.ed25519Key !=
483 1 : masterKey.publicKey) {
484 0 : throw BootstrapBadStateException(
485 : 'ERROR: New master key does not match up!',
486 : );
487 : }
488 2 : Logs().v('Set own master key to verified...');
489 6 : await client.userDeviceKeys[client.userID]!.masterKey!
490 1 : .setVerified(true, false);
491 7 : keysToSign.add(client.userDeviceKeys[client.userID]!.masterKey!);
492 : }
493 : if (selfSigningKey != null) {
494 1 : keysToSign.add(
495 9 : client.userDeviceKeys[client.userID]!.deviceKeys[client.deviceID]!,
496 : );
497 : }
498 2 : Logs().v('Sign ourself...');
499 3 : await encryption.crossSigning.sign(keysToSign);
500 : } catch (e, s) {
501 0 : Logs().e('[Bootstrapping] Error setting up cross signing', e, s);
502 0 : state = BootstrapState.error;
503 : return;
504 : }
505 :
506 3 : await client.dehydratedDeviceSetup(newSsssKey!);
507 1 : checkOnlineKeyBackup();
508 : }
509 :
510 1 : void checkOnlineKeyBackup() {
511 : // check if we have online key backup set up
512 3 : if (encryption.keyManager.enabled) {
513 1 : state = BootstrapState.askWipeOnlineKeyBackup;
514 : return;
515 : }
516 1 : state = BootstrapState.askSetupOnlineKeyBackup;
517 : }
518 :
519 1 : void wipeOnlineKeyBackup(bool wipe) {
520 2 : if (state != BootstrapState.askWipeOnlineKeyBackup) {
521 0 : throw BootstrapBadStateException();
522 : }
523 : if (wipe) {
524 1 : state = BootstrapState.askSetupOnlineKeyBackup;
525 : } else {
526 1 : state = BootstrapState.done;
527 : }
528 : }
529 :
530 1 : Future<void> askSetupOnlineKeyBackup(bool setup) async {
531 2 : if (state != BootstrapState.askSetupOnlineKeyBackup) {
532 0 : throw BootstrapBadStateException();
533 : }
534 : if (!setup) {
535 1 : state = BootstrapState.done;
536 : return;
537 : }
538 : try {
539 1 : final keyObj = vod.PkDecryption();
540 : String pubKey;
541 : Uint8List privKey;
542 :
543 1 : pubKey = keyObj.publicKey;
544 1 : privKey = keyObj.privateKey;
545 :
546 2 : Logs().v('Create the new backup version...');
547 2 : await client.postRoomKeysVersion(
548 : BackupAlgorithm.mMegolmBackupV1Curve25519AesSha2,
549 1 : <String, dynamic>{
550 : 'public_key': pubKey,
551 : },
552 : );
553 2 : Logs().v('Store the secret...');
554 3 : await newSsssKey?.store(megolmKey, base64.encode(privKey));
555 :
556 2 : Logs().v(
557 : 'And finally set all megolm keys as needing to be uploaded again...',
558 : );
559 3 : await client.database.markInboundGroupSessionsAsNeedingUpload();
560 2 : Logs().v('And uploading keys...');
561 4 : await client.encryption?.keyManager.uploadInboundGroupSessions();
562 : } catch (e, s) {
563 0 : Logs().e('[Bootstrapping] Error setting up online key backup', e, s);
564 0 : state = BootstrapState.error;
565 0 : encryption.client.onEncryptionError.add(
566 0 : SdkError(exception: e, stackTrace: s),
567 : );
568 : return;
569 : }
570 1 : state = BootstrapState.done;
571 : }
572 :
573 1 : set state(BootstrapState newState) {
574 3 : Logs().v('BootstrapState: $newState');
575 2 : if (state != BootstrapState.error) {
576 1 : _state = newState;
577 : }
578 :
579 2 : onUpdate?.call(this);
580 : }
581 : }
582 :
583 : class BootstrapBadStateException implements Exception {
584 : String cause;
585 0 : BootstrapBadStateException([this.cause = 'Bad state']);
586 :
587 0 : @override
588 0 : String toString() => 'BootstrapBadStateException: $cause';
589 : }
|