/* * Famedly Matrix SDK * Copyright (C) 2020, 2021 Famedly GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ import 'dart:convert'; import 'dart:typed_data'; import 'package:canonical_json/canonical_json.dart'; import 'package:vodozemac/vodozemac.dart' as vod; import 'package:matrix/encryption/encryption.dart'; import 'package:matrix/encryption/key_manager.dart'; import 'package:matrix/encryption/ssss.dart'; import 'package:matrix/matrix.dart'; enum BootstrapState { /// Is loading. loading, /// Existing SSSS found, should we wipe it? askWipeSsss, /// Ask if an existing SSSS should be userDeviceKeys askUseExistingSsss, /// Ask to unlock all the SSSS keys askUnlockSsss, /// SSSS is in a bad state, continue with potential dataloss? askBadSsss, /// Ask for new SSSS key / passphrase askNewSsss, /// Open an existing SSSS key openExistingSsss, /// Ask if cross signing should be wiped askWipeCrossSigning, /// Ask if cross signing should be set up askSetupCrossSigning, /// Ask if online key backup should be wiped askWipeOnlineKeyBackup, /// Ask if the online key backup should be set up askSetupOnlineKeyBackup, /// An error has been occured. error, /// done done, } /// Bootstrapping SSSS and cross-signing class Bootstrap { final Encryption encryption; Client get client => encryption.client; void Function(Bootstrap)? onUpdate; BootstrapState get state => _state; BootstrapState _state = BootstrapState.loading; Map? oldSsssKeys; OpenSSSS? newSsssKey; Map? secretMap; Bootstrap({required this.encryption, this.onUpdate}) { if (analyzeSecrets().isNotEmpty) { state = BootstrapState.askWipeSsss; } else { state = BootstrapState.askNewSsss; } } // cache the secret analyzing so that we don't drop stuff a different client sets during bootstrapping Map>? _secretsCache; /// returns ssss from accountdata, eg: m.megolm_backup.v1, or your m.cross_signing stuff Map> analyzeSecrets() { final secretsCache = _secretsCache; if (secretsCache != null) { // deep-copy so that we can do modifications final newSecrets = >{}; for (final s in secretsCache.entries) { newSecrets[s.key] = Set.from(s.value); } return newSecrets; } final secrets = >{}; for (final entry in client.accountData.entries) { final type = entry.key; final event = entry.value; final encryptedContent = event.content.tryGetMap('encrypted'); if (encryptedContent == null) { continue; } final validKeys = {}; final invalidKeys = {}; for (final keyEntry in encryptedContent.entries) { final key = keyEntry.key; final value = keyEntry.value; if (value is! Map) { // we don't add the key to invalidKeys as this was not a proper secret anyways! continue; } if (value['iv'] is! String || value['ciphertext'] is! String || value['mac'] is! String) { invalidKeys.add(key); continue; } if (!encryption.ssss.isKeyValid(key)) { invalidKeys.add(key); continue; } validKeys.add(key); } if (validKeys.isEmpty && invalidKeys.isEmpty) { continue; // this didn't contain any keys anyways! } // if there are no valid keys and only invalid keys then the validKeys set will be empty // from that we know that there were errors with this secret and that we won't be able to migrate it secrets[type] = validKeys; } _secretsCache = secrets; return analyzeSecrets(); } Set badSecrets() { final secrets = analyzeSecrets(); secrets.removeWhere((k, v) => v.isNotEmpty); return Set.from(secrets.keys); } String mostUsedKey(Map> secrets) { final usage = {}; for (final keys in secrets.values) { for (final key in keys) { usage.update(key, (i) => i + 1, ifAbsent: () => 1); } } final entriesList = usage.entries.toList(); entriesList.sort((a, b) => a.value.compareTo(b.value)); return entriesList.first.key; } Set allNeededKeys() { final secrets = analyzeSecrets(); secrets.removeWhere( (k, v) => v.isEmpty, ); // we don't care about the failed secrets here final keys = {}; final defaultKeyId = encryption.ssss.defaultKeyId; int removeKey(String key) { final sizeBefore = secrets.length; secrets.removeWhere((k, v) => v.contains(key)); return sizeBefore - secrets.length; } // first we want to try the default key id if (defaultKeyId != null) { if (removeKey(defaultKeyId) > 0) { keys.add(defaultKeyId); } } // now we re-try as long as we have keys for all secrets while (secrets.isNotEmpty) { final key = mostUsedKey(secrets); removeKey(key); keys.add(key); } return keys; } void wipeSsss(bool wipe) { if (state != BootstrapState.askWipeSsss) { throw BootstrapBadStateException('Wrong State'); } if (wipe) { state = BootstrapState.askNewSsss; } else if (encryption.ssss.defaultKeyId != null && encryption.ssss.isKeyValid(encryption.ssss.defaultKeyId!)) { state = BootstrapState.askUseExistingSsss; } else if (badSecrets().isNotEmpty) { state = BootstrapState.askBadSsss; } else { migrateOldSsss(); } } void useExistingSsss(bool use) { if (state != BootstrapState.askUseExistingSsss) { throw BootstrapBadStateException('Wrong State'); } if (use) { try { newSsssKey = encryption.ssss.open(encryption.ssss.defaultKeyId); state = BootstrapState.openExistingSsss; } catch (e, s) { Logs().e('[Bootstrapping] Error open SSSS', e, s); state = BootstrapState.error; return; } } else if (badSecrets().isNotEmpty) { state = BootstrapState.askBadSsss; } else { migrateOldSsss(); } } void ignoreBadSecrets(bool ignore) { if (state != BootstrapState.askBadSsss) { throw BootstrapBadStateException('Wrong State'); } if (ignore) { migrateOldSsss(); } else { // that's it, folks. We can't do anything here state = BootstrapState.error; } } void migrateOldSsss() { final keys = allNeededKeys(); final oldSsssKeys = this.oldSsssKeys = {}; try { for (final key in keys) { oldSsssKeys[key] = encryption.ssss.open(key); } } catch (e, s) { Logs().e('[Bootstrapping] Error construction ssss key', e, s); state = BootstrapState.error; return; } state = BootstrapState.askUnlockSsss; } void unlockedSsss() { if (state != BootstrapState.askUnlockSsss) { throw BootstrapBadStateException('Wrong State'); } state = BootstrapState.askNewSsss; } Future newSsss([String? passphrase]) async { if (state != BootstrapState.askNewSsss) { throw BootstrapBadStateException('Wrong State'); } state = BootstrapState.loading; try { Logs().v('Create key...'); newSsssKey = await encryption.ssss.createKey(passphrase); if (oldSsssKeys != null) { // alright, we have to re-encrypt old secrets with the new key final secrets = analyzeSecrets(); Set removeKey(String key) { final s = secrets.entries .where((e) => e.value.contains(key)) .map((e) => e.key) .toSet(); secrets.removeWhere((k, v) => v.contains(key)); return s; } secretMap = {}; for (final entry in oldSsssKeys!.entries) { final key = entry.value; final keyId = entry.key; if (!key.isUnlocked) { continue; } for (final s in removeKey(keyId)) { Logs().v('Get stored key of type $s...'); secretMap![s] = await key.getStored(s); Logs().v('Store new secret with this key...'); await newSsssKey!.store(s, secretMap![s]!, add: true); } } // alright, we re-encrypted all the secrets. We delete the dead weight only *after* we set our key to the default key } await encryption.ssss.setDefaultKeyId(newSsssKey!.keyId); while (encryption.ssss.defaultKeyId != newSsssKey!.keyId) { Logs().v( 'Waiting accountData to have the correct m.secret_storage.default_key', ); await client.oneShotSync(); } if (oldSsssKeys != null) { for (final entry in secretMap!.entries) { Logs().v('Validate and stripe other keys ${entry.key}...'); await newSsssKey!.validateAndStripOtherKeys(entry.key, entry.value); } Logs().v('And make super sure we have everything cached...'); await newSsssKey!.maybeCacheAll(); } } catch (e, s) { Logs().e('[Bootstrapping] Error trying to migrate old secrets', e, s); state = BootstrapState.error; return; } // alright, we successfully migrated all secrets, if needed checkCrossSigning(); } Future openExistingSsss() async { final newSsssKey = this.newSsssKey; if (state != BootstrapState.openExistingSsss || newSsssKey == null) { throw BootstrapBadStateException(); } if (!newSsssKey.isUnlocked) { throw BootstrapBadStateException('Key not unlocked'); } Logs().v('Maybe cache all...'); await newSsssKey.maybeCacheAll(); checkCrossSigning(); } void checkCrossSigning() { // so, let's see if we have cross signing set up if (encryption.crossSigning.enabled) { // cross signing present, ask for wipe state = BootstrapState.askWipeCrossSigning; return; } // no cross signing present state = BootstrapState.askSetupCrossSigning; } Future wipeCrossSigning(bool wipe) async { if (state != BootstrapState.askWipeCrossSigning) { throw BootstrapBadStateException(); } if (wipe) { state = BootstrapState.askSetupCrossSigning; } else { await client.dehydratedDeviceSetup(newSsssKey!); checkOnlineKeyBackup(); } } Future askSetupCrossSigning({ bool setupMasterKey = false, bool setupSelfSigningKey = false, bool setupUserSigningKey = false, }) async { if (state != BootstrapState.askSetupCrossSigning) { throw BootstrapBadStateException(); } if (!setupMasterKey && !setupSelfSigningKey && !setupUserSigningKey) { await client.dehydratedDeviceSetup(newSsssKey!); checkOnlineKeyBackup(); return; } final userID = client.userID!; try { String masterSigningKey; final secretsToStore = {}; MatrixCrossSigningKey? masterKey; MatrixCrossSigningKey? selfSigningKey; MatrixCrossSigningKey? userSigningKey; String? masterPub; if (setupMasterKey) { final master = vod.PkSigning(); masterSigningKey = master.secretKey; masterPub = master.publicKey.toBase64(); final json = { 'user_id': userID, 'usage': ['master'], 'keys': { 'ed25519:$masterPub': masterPub, }, }; masterKey = MatrixCrossSigningKey.fromJson(json); secretsToStore[EventTypes.CrossSigningMasterKey] = masterSigningKey; } else { Logs().v('Get stored key...'); masterSigningKey = await newSsssKey?.getStored(EventTypes.CrossSigningMasterKey) ?? ''; if (masterSigningKey.isEmpty) { // no master signing key :( throw BootstrapBadStateException('No master key'); } final master = vod.PkSigning.fromSecretKey(masterSigningKey); masterPub = master.publicKey.toBase64(); } String? sign(Map object) { final keyObj = vod.PkSigning.fromSecretKey(masterSigningKey); return keyObj .sign(String.fromCharCodes(canonicalJson.encode(object))) .toBase64(); } if (setupSelfSigningKey) { final selfSigning = vod.PkSigning(); final selfSigningPriv = selfSigning.secretKey; final selfSigningPub = selfSigning.publicKey.toBase64(); final json = { 'user_id': userID, 'usage': ['self_signing'], 'keys': { 'ed25519:$selfSigningPub': selfSigningPub, }, }; final signature = sign(json); json['signatures'] = { userID: { 'ed25519:$masterPub': signature, }, }; selfSigningKey = MatrixCrossSigningKey.fromJson(json); secretsToStore[EventTypes.CrossSigningSelfSigning] = selfSigningPriv; } if (setupUserSigningKey) { final userSigning = vod.PkSigning(); final userSigningPriv = userSigning.secretKey; final userSigningPub = userSigning.publicKey.toBase64(); final json = { 'user_id': userID, 'usage': ['user_signing'], 'keys': { 'ed25519:$userSigningPub': userSigningPub, }, }; final signature = sign(json); json['signatures'] = { userID: { 'ed25519:$masterPub': signature, }, }; userSigningKey = MatrixCrossSigningKey.fromJson(json); secretsToStore[EventTypes.CrossSigningUserSigning] = userSigningPriv; } // upload the keys! state = BootstrapState.loading; Logs().v('Upload device signing keys.'); await client.uiaRequestBackground( (AuthenticationData? auth) => client.uploadCrossSigningKeys( masterKey: masterKey, selfSigningKey: selfSigningKey, userSigningKey: userSigningKey, auth: auth, ), ); Logs().v('Device signing keys have been uploaded.'); // aaaand set the SSSS secrets if (masterKey != null) { while (!(masterKey.publicKey != null && client.userDeviceKeys[client.userID]?.masterKey?.ed25519Key == masterKey.publicKey)) { Logs().v('Waiting for master to be created'); await client.oneShotSync(); } } if (newSsssKey != null) { final storeFutures = >[]; for (final entry in secretsToStore.entries) { storeFutures.add(newSsssKey!.store(entry.key, entry.value)); } Logs().v('Store new SSSS key entries...'); await Future.wait(storeFutures); } final keysToSign = []; if (masterKey != null) { if (client.userDeviceKeys[client.userID]?.masterKey?.ed25519Key != masterKey.publicKey) { throw BootstrapBadStateException( 'ERROR: New master key does not match up!', ); } Logs().v('Set own master key to verified...'); await client.userDeviceKeys[client.userID]!.masterKey! .setVerified(true, false); keysToSign.add(client.userDeviceKeys[client.userID]!.masterKey!); } if (selfSigningKey != null) { keysToSign.add( client.userDeviceKeys[client.userID]!.deviceKeys[client.deviceID]!, ); } Logs().v('Sign ourself...'); await encryption.crossSigning.sign(keysToSign); } catch (e, s) { Logs().e('[Bootstrapping] Error setting up cross signing', e, s); state = BootstrapState.error; return; } await client.dehydratedDeviceSetup(newSsssKey!); checkOnlineKeyBackup(); } void checkOnlineKeyBackup() { // check if we have online key backup set up if (encryption.keyManager.enabled) { state = BootstrapState.askWipeOnlineKeyBackup; return; } state = BootstrapState.askSetupOnlineKeyBackup; } void wipeOnlineKeyBackup(bool wipe) { if (state != BootstrapState.askWipeOnlineKeyBackup) { throw BootstrapBadStateException(); } if (wipe) { state = BootstrapState.askSetupOnlineKeyBackup; } else { state = BootstrapState.done; } } Future askSetupOnlineKeyBackup(bool setup) async { if (state != BootstrapState.askSetupOnlineKeyBackup) { throw BootstrapBadStateException(); } if (!setup) { state = BootstrapState.done; return; } try { final keyObj = vod.PkDecryption(); String pubKey; Uint8List privKey; pubKey = keyObj.publicKey; privKey = keyObj.privateKey; Logs().v('Create the new backup version...'); await client.postRoomKeysVersion( BackupAlgorithm.mMegolmBackupV1Curve25519AesSha2, { 'public_key': pubKey, }, ); Logs().v('Store the secret...'); await newSsssKey?.store(megolmKey, base64.encode(privKey)); Logs().v( 'And finally set all megolm keys as needing to be uploaded again...', ); await client.database.markInboundGroupSessionsAsNeedingUpload(); Logs().v('And uploading keys...'); await client.encryption?.keyManager.uploadInboundGroupSessions(); } catch (e, s) { Logs().e('[Bootstrapping] Error setting up online key backup', e, s); state = BootstrapState.error; encryption.client.onEncryptionError.add( SdkError(exception: e, stackTrace: s), ); return; } state = BootstrapState.done; } set state(BootstrapState newState) { Logs().v('BootstrapState: $newState'); if (state != BootstrapState.error) { _state = newState; } onUpdate?.call(this); } } class BootstrapBadStateException implements Exception { String cause; BootstrapBadStateException([this.cause = 'Bad state']); @override String toString() => 'BootstrapBadStateException: $cause'; }