UNPKG

matrix-react-sdk

Version:
308 lines (267 loc) 12.8 kB
/* Copyright 2024 New Vector Ltd. Copyright 2020-2024 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ import EventEmitter from "events"; import { KeyBackupInfo, VerificationPhase, VerificationRequest, VerificationRequestEvent, } from "matrix-js-sdk/src/crypto-api"; import { logger } from "matrix-js-sdk/src/logger"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { Device, SecretStorage } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../MatrixClientPeg"; import { AccessCancelledError, accessSecretStorage } from "../SecurityManager"; import Modal from "../Modal"; import InteractiveAuthDialog from "../components/views/dialogs/InteractiveAuthDialog"; import { _t } from "../languageHandler"; import { SdkContextClass } from "../contexts/SDKContext"; import { asyncSome } from "../utils/arrays"; import { initialiseDehydration } from "../utils/device/dehydration"; export enum Phase { Loading = 0, Intro = 1, Busy = 2, Done = 3, // final done stage, but still showing UX ConfirmSkip = 4, Finished = 5, // UX can be closed ConfirmReset = 6, } export class SetupEncryptionStore extends EventEmitter { private started?: boolean; public phase?: Phase; public verificationRequest: VerificationRequest | null = null; public backupInfo: KeyBackupInfo | null = null; // ID of the key that the secrets we want are encrypted with public keyId: string | null = null; // Descriptor of the key that the secrets we want are encrypted with public keyInfo: SecretStorage.SecretStorageKeyDescription | null = null; public hasDevicesToVerifyAgainst?: boolean; public static sharedInstance(): SetupEncryptionStore { if (!window.mxSetupEncryptionStore) window.mxSetupEncryptionStore = new SetupEncryptionStore(); return window.mxSetupEncryptionStore; } public start(): void { if (this.started) { return; } this.started = true; this.phase = Phase.Loading; const cli = MatrixClientPeg.safeGet(); cli.on(CryptoEvent.VerificationRequestReceived, this.onVerificationRequest); cli.on(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged); const requestsInProgress = cli.getCrypto()!.getVerificationRequestsToDeviceInProgress(cli.getUserId()!); if (requestsInProgress.length) { // If there are multiple, we take the most recent. Equally if the user sends another request from // another device after this screen has been shown, we'll switch to the new one, so this // generally doesn't support multiple requests. this.setActiveVerificationRequest(requestsInProgress[requestsInProgress.length - 1]); } this.fetchKeyInfo(); } public stop(): void { if (!this.started) { return; } this.started = false; this.verificationRequest?.off(VerificationRequestEvent.Change, this.onVerificationRequestChange); const cli = MatrixClientPeg.get(); if (!!cli) { cli.removeListener(CryptoEvent.VerificationRequestReceived, this.onVerificationRequest); cli.removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged); } } public async fetchKeyInfo(): Promise<void> { if (!this.started) return; // bail if we were stopped const cli = MatrixClientPeg.safeGet(); const keys = await cli.secretStorage.isStored("m.cross_signing.master"); if (keys === null || Object.keys(keys).length === 0) { this.keyId = null; this.keyInfo = null; } else { // If the secret is stored under more than one key, we just pick an arbitrary one this.keyId = Object.keys(keys)[0]; this.keyInfo = keys[this.keyId]; } // do we have any other verified devices which are E2EE which we can verify against? const dehydratedDevice = await cli.getDehydratedDevice(); const ownUserId = cli.getUserId()!; const crypto = cli.getCrypto()!; const userDevices: Iterable<Device> = (await crypto.getUserDeviceInfo([ownUserId])).get(ownUserId)?.values() ?? []; this.hasDevicesToVerifyAgainst = await asyncSome(userDevices, async (device) => { // Ignore dehydrated devices. `dehydratedDevice` is set by the // implementation of MSC2697, whereas MSC3814 proposes that devices // should set a `dehydrated` flag in the device key. We ignore // both types of dehydrated devices. if (dehydratedDevice && device.deviceId == dehydratedDevice?.device_id) return false; if (device.dehydrated) return false; // ignore devices without an identity key if (!device.getIdentityKey()) return false; const verificationStatus = await crypto.getDeviceVerificationStatus(ownUserId, device.deviceId); return !!verificationStatus?.signedByOwner; }); this.phase = Phase.Intro; this.emit("update"); } public async usePassPhrase(): Promise<void> { logger.debug("SetupEncryptionStore.usePassphrase"); this.phase = Phase.Busy; this.emit("update"); try { const cli = MatrixClientPeg.safeGet(); const backupInfo = await cli.getKeyBackupVersion(); this.backupInfo = backupInfo; this.emit("update"); await new Promise((resolve: (value?: unknown) => void, reject: (reason?: any) => void) => { accessSecretStorage(async (): Promise<void> => { // `accessSecretStorage` will call `boostrapCrossSigning` and `bootstrapSecretStorage`, so that // should be enough to ensure that our device is correctly cross-signed. // // The remaining tasks (device dehydration and restoring key backup) may take some time due to // processing many to-device messages in the case of device dehydration, or having many keys to // restore in the case of key backups, so we allow the dialog to advance before this. // // However, we need to keep the 4S key cached, so we stay inside `accessSecretStorage`. logger.debug( "SetupEncryptionStore.usePassphrase: cross-signing and secret storage set up; checking " + "dehydration and backup in the background", ); resolve(); await initialiseDehydration(); if (backupInfo) { await cli.restoreKeyBackupWithSecretStorage(backupInfo); } }).catch(reject); }); if (await cli.getCrypto()?.getCrossSigningKeyId()) { logger.debug("SetupEncryptionStore.usePassphrase: done"); this.phase = Phase.Done; this.emit("update"); } } catch (e) { if (e instanceof AccessCancelledError) { logger.debug("SetupEncryptionStore.usePassphrase: user cancelled access to secret storage"); } else { logger.log("SetupEncryptionStore.usePassphrase: error", e); } this.phase = Phase.Intro; this.emit("update"); } } private onUserTrustStatusChanged = async (userId: string): Promise<void> => { if (userId !== MatrixClientPeg.safeGet().getSafeUserId()) return; const publicKeysTrusted = await MatrixClientPeg.safeGet().getCrypto()?.getCrossSigningKeyId(); if (publicKeysTrusted) { this.phase = Phase.Done; this.emit("update"); } }; public onVerificationRequest = (request: VerificationRequest): void => { this.setActiveVerificationRequest(request); }; public onVerificationRequestChange = async (): Promise<void> => { if (this.verificationRequest?.phase === VerificationPhase.Cancelled) { this.verificationRequest.off(VerificationRequestEvent.Change, this.onVerificationRequestChange); this.verificationRequest = null; this.emit("update"); } else if (this.verificationRequest?.phase === VerificationPhase.Done) { this.verificationRequest.off(VerificationRequestEvent.Change, this.onVerificationRequestChange); this.verificationRequest = null; // At this point, the verification has finished, we just need to wait for // cross signing to be ready to use, so wait for the user trust status to // change (or change to DONE if it's already ready). const publicKeysTrusted = await MatrixClientPeg.safeGet().getCrypto()?.getCrossSigningKeyId(); this.phase = publicKeysTrusted ? Phase.Done : Phase.Busy; this.emit("update"); } }; public skip(): void { this.phase = Phase.ConfirmSkip; this.emit("update"); } public skipConfirm(): void { this.phase = Phase.Finished; this.emit("update"); } public returnAfterSkip(): void { this.phase = Phase.Intro; this.emit("update"); } public reset(): void { this.phase = Phase.ConfirmReset; this.emit("update"); } public async resetConfirm(): Promise<void> { try { // If we've gotten here, the user presumably lost their // secret storage key if they had one. Start by resetting // secret storage and setting up a new recovery key, then // create new cross-signing keys once that succeeds. await accessSecretStorage(async (): Promise<void> => { const cli = MatrixClientPeg.safeGet(); await cli.getCrypto()?.bootstrapCrossSigning({ authUploadDeviceSigningKeys: async (makeRequest): Promise<void> => { const cachedPassword = SdkContextClass.instance.accountPasswordStore.getPassword(); if (cachedPassword) { await makeRequest({ type: "m.login.password", identifier: { type: "m.id.user", user: cli.getSafeUserId(), }, user: cli.getSafeUserId(), password: cachedPassword, }); return; } const { finished } = Modal.createDialog(InteractiveAuthDialog, { title: _t("encryption|bootstrap_title"), matrixClient: cli, makeRequest, }); const [confirmed] = await finished; if (!confirmed) { throw new Error("Cross-signing key upload auth canceled"); } }, setupNewCrossSigning: true, }); await initialiseDehydration(true); this.phase = Phase.Finished; }, true); } catch (e) { logger.error("Error resetting cross-signing", e); this.phase = Phase.Intro; } this.emit("update"); } public returnAfterReset(): void { this.phase = Phase.Intro; this.emit("update"); } public done(): void { this.phase = Phase.Finished; this.emit("update"); // async - ask other clients for keys, if necessary MatrixClientPeg.safeGet().crypto?.cancelAndResendAllOutgoingKeyRequests(); } private async setActiveVerificationRequest(request: VerificationRequest): Promise<void> { if (!this.started) return; // bail if we were stopped if (request.otherUserId !== MatrixClientPeg.safeGet().getUserId()) return; if (this.verificationRequest) { this.verificationRequest.off(VerificationRequestEvent.Change, this.onVerificationRequestChange); } this.verificationRequest = request; await request.accept(); request.on(VerificationRequestEvent.Change, this.onVerificationRequestChange); this.emit("update"); } public lostKeys(): boolean { return !this.hasDevicesToVerifyAgainst && !this.keyInfo; } }