2020-03-24 15:57:04 +01:00
|
|
|
/*
|
2024-09-09 14:57:16 +01:00
|
|
|
Copyright 2024 New Vector Ltd.
|
|
|
|
|
Copyright 2020-2024 The Matrix.org Foundation C.I.C.
|
2020-03-24 15:57:04 +01:00
|
|
|
|
2024-09-09 14:57:16 +01:00
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
|
|
|
Please see LICENSE files in the repository root for full details.
|
2020-03-24 15:57:04 +01:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import EventEmitter from "events";
|
2024-07-04 16:50:07 +01:00
|
|
|
import {
|
|
|
|
|
KeyBackupInfo,
|
|
|
|
|
VerificationPhase,
|
|
|
|
|
VerificationRequest,
|
|
|
|
|
VerificationRequestEvent,
|
|
|
|
|
} from "matrix-js-sdk/src/crypto-api";
|
2021-10-22 17:23:32 -05:00
|
|
|
import { logger } from "matrix-js-sdk/src/logger";
|
2022-02-22 12:18:08 +00:00
|
|
|
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
2024-03-22 12:28:13 +00:00
|
|
|
import { Device, SecretStorage } from "matrix-js-sdk/src/matrix";
|
2021-06-24 21:22:56 +01:00
|
|
|
|
2020-03-24 15:57:04 +01:00
|
|
|
import { MatrixClientPeg } from "../MatrixClientPeg";
|
2022-02-22 12:18:08 +00:00
|
|
|
import { AccessCancelledError, accessSecretStorage } from "../SecurityManager";
|
2021-09-08 13:55:31 -04:00
|
|
|
import Modal from "../Modal";
|
|
|
|
|
import InteractiveAuthDialog from "../components/views/dialogs/InteractiveAuthDialog";
|
|
|
|
|
import { _t } from "../languageHandler";
|
2023-02-23 08:46:49 +01:00
|
|
|
import { SdkContextClass } from "../contexts/SDKContext";
|
2023-05-03 15:02:58 +01:00
|
|
|
import { asyncSome } from "../utils/arrays";
|
2024-04-15 11:47:15 -04:00
|
|
|
import { initialiseDehydration } from "../utils/device/dehydration";
|
2020-03-24 15:57:04 +01:00
|
|
|
|
2021-06-18 12:44:15 +01:00
|
|
|
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
|
2021-09-08 13:55:31 -04:00
|
|
|
ConfirmReset = 6,
|
2021-06-14 20:58:20 +01:00
|
|
|
}
|
2020-03-24 15:57:04 +01:00
|
|
|
|
|
|
|
|
export class SetupEncryptionStore extends EventEmitter {
|
2023-05-10 08:41:55 +01:00
|
|
|
private started?: boolean;
|
2023-05-16 14:25:43 +01:00
|
|
|
public phase?: Phase;
|
2023-02-15 13:36:22 +00:00
|
|
|
public verificationRequest: VerificationRequest | null = null;
|
2024-07-04 16:50:07 +01:00
|
|
|
public backupInfo: KeyBackupInfo | null = null;
|
2023-02-15 13:36:22 +00:00
|
|
|
// 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
|
2024-03-22 12:28:13 +00:00
|
|
|
public keyInfo: SecretStorage.SecretStorageKeyDescription | null = null;
|
2023-05-10 08:41:55 +01:00
|
|
|
public hasDevicesToVerifyAgainst?: boolean;
|
2021-06-14 20:58:20 +01:00
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
public static sharedInstance(): SetupEncryptionStore {
|
2021-06-14 20:58:20 +01:00
|
|
|
if (!window.mxSetupEncryptionStore) window.mxSetupEncryptionStore = new SetupEncryptionStore();
|
|
|
|
|
return window.mxSetupEncryptionStore;
|
2020-03-24 15:57:04 +01:00
|
|
|
}
|
|
|
|
|
|
2021-06-14 20:58:20 +01:00
|
|
|
public start(): void {
|
|
|
|
|
if (this.started) {
|
2020-03-24 15:57:04 +01:00
|
|
|
return;
|
|
|
|
|
}
|
2021-06-14 20:58:20 +01:00
|
|
|
this.started = true;
|
2021-06-18 12:44:15 +01:00
|
|
|
this.phase = Phase.Loading;
|
2020-06-02 16:32:15 +01:00
|
|
|
|
2023-06-15 15:11:49 +01:00
|
|
|
const cli = MatrixClientPeg.safeGet();
|
2023-06-28 11:11:18 +01:00
|
|
|
cli.on(CryptoEvent.VerificationRequestReceived, this.onVerificationRequest);
|
2022-02-22 12:18:08 +00:00
|
|
|
cli.on(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged);
|
2020-06-19 17:18:48 +01:00
|
|
|
|
2023-06-16 11:27:56 +01:00
|
|
|
const requestsInProgress = cli.getCrypto()!.getVerificationRequestsToDeviceInProgress(cli.getUserId()!);
|
2020-06-15 17:41:22 +01:00
|
|
|
if (requestsInProgress.length) {
|
2020-06-16 14:53:13 +01:00
|
|
|
// If there are multiple, we take the most recent. Equally if the user sends another request from
|
2020-06-15 17:46:22 +01:00
|
|
|
// another device after this screen has been shown, we'll switch to the new one, so this
|
|
|
|
|
// generally doesn't support multiple requests.
|
2021-06-14 20:58:20 +01:00
|
|
|
this.setActiveVerificationRequest(requestsInProgress[requestsInProgress.length - 1]);
|
2020-06-15 17:41:22 +01:00
|
|
|
}
|
2020-06-02 16:32:15 +01:00
|
|
|
|
|
|
|
|
this.fetchKeyInfo();
|
2020-03-24 15:57:04 +01:00
|
|
|
}
|
|
|
|
|
|
2021-06-14 20:58:20 +01:00
|
|
|
public stop(): void {
|
|
|
|
|
if (!this.started) {
|
2020-03-24 15:57:04 +01:00
|
|
|
return;
|
|
|
|
|
}
|
2021-06-14 20:58:20 +01:00
|
|
|
this.started = false;
|
2022-05-26 11:12:49 +01:00
|
|
|
this.verificationRequest?.off(VerificationRequestEvent.Change, this.onVerificationRequestChange);
|
2023-06-15 15:11:49 +01:00
|
|
|
|
|
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
|
if (!!cli) {
|
2023-06-28 11:11:18 +01:00
|
|
|
cli.removeListener(CryptoEvent.VerificationRequestReceived, this.onVerificationRequest);
|
2023-06-15 15:11:49 +01:00
|
|
|
cli.removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged);
|
2020-03-24 15:57:04 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-14 20:58:20 +01:00
|
|
|
public async fetchKeyInfo(): Promise<void> {
|
2022-05-26 11:12:49 +01:00
|
|
|
if (!this.started) return; // bail if we were stopped
|
2023-06-15 15:11:49 +01:00
|
|
|
const cli = MatrixClientPeg.safeGet();
|
2023-06-19 23:20:14 +01:00
|
|
|
const keys = await cli.secretStorage.isStored("m.cross_signing.master");
|
2020-06-19 20:07:20 +01:00
|
|
|
if (keys === null || Object.keys(keys).length === 0) {
|
2020-06-02 16:32:15 +01:00
|
|
|
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];
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-08 13:55:31 -04:00
|
|
|
// do we have any other verified devices which are E2EE which we can verify against?
|
2021-03-08 04:49:59 +00:00
|
|
|
const dehydratedDevice = await cli.getDehydratedDevice();
|
2023-02-15 13:36:22 +00:00
|
|
|
const ownUserId = cli.getUserId()!;
|
2023-06-19 23:20:14 +01:00
|
|
|
const crypto = cli.getCrypto()!;
|
|
|
|
|
const userDevices: Iterable<Device> =
|
|
|
|
|
(await crypto.getUserDeviceInfo([ownUserId])).get(ownUserId)?.values() ?? [];
|
|
|
|
|
this.hasDevicesToVerifyAgainst = await asyncSome(userDevices, async (device) => {
|
2024-04-15 11:47:15 -04:00
|
|
|
// 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.
|
2023-06-19 23:20:14 +01:00
|
|
|
if (dehydratedDevice && device.deviceId == dehydratedDevice?.device_id) return false;
|
2024-04-15 11:47:15 -04:00
|
|
|
if (device.dehydrated) return false;
|
2023-06-19 23:20:14 +01:00
|
|
|
|
|
|
|
|
// ignore devices without an identity key
|
|
|
|
|
if (!device.getIdentityKey()) return false;
|
|
|
|
|
|
|
|
|
|
const verificationStatus = await crypto.getDeviceVerificationStatus(ownUserId, device.deviceId);
|
2023-05-03 15:02:58 +01:00
|
|
|
return !!verificationStatus?.signedByOwner;
|
2023-04-24 14:19:46 +01:00
|
|
|
});
|
2021-03-08 04:49:59 +00:00
|
|
|
|
2021-09-08 13:55:31 -04:00
|
|
|
this.phase = Phase.Intro;
|
2020-06-02 16:32:15 +01:00
|
|
|
this.emit("update");
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-14 20:58:20 +01:00
|
|
|
public async usePassPhrase(): Promise<void> {
|
2024-07-13 13:36:45 +01:00
|
|
|
logger.debug("SetupEncryptionStore.usePassphrase");
|
2021-06-18 12:44:15 +01:00
|
|
|
this.phase = Phase.Busy;
|
2020-03-24 15:57:04 +01:00
|
|
|
this.emit("update");
|
|
|
|
|
try {
|
2023-06-15 15:11:49 +01:00
|
|
|
const cli = MatrixClientPeg.safeGet();
|
2020-03-24 15:57:04 +01:00
|
|
|
const backupInfo = await cli.getKeyBackupVersion();
|
|
|
|
|
this.backupInfo = backupInfo;
|
|
|
|
|
this.emit("update");
|
2024-07-13 13:36:45 +01:00
|
|
|
|
2021-06-14 20:58:20 +01:00
|
|
|
await new Promise((resolve: (value?: unknown) => void, reject: (reason?: any) => void) => {
|
2023-01-12 13:25:14 +00:00
|
|
|
accessSecretStorage(async (): Promise<void> => {
|
2024-07-13 13:36:45 +01:00
|
|
|
// `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",
|
|
|
|
|
);
|
2021-03-26 13:01:30 +00:00
|
|
|
resolve();
|
2024-04-15 11:47:15 -04:00
|
|
|
|
|
|
|
|
await initialiseDehydration();
|
|
|
|
|
|
2021-03-26 13:01:30 +00:00
|
|
|
if (backupInfo) {
|
|
|
|
|
await cli.restoreKeyBackupWithSecretStorage(backupInfo);
|
|
|
|
|
}
|
|
|
|
|
}).catch(reject);
|
2020-03-24 15:57:04 +01:00
|
|
|
});
|
|
|
|
|
|
2023-05-15 19:30:43 +01:00
|
|
|
if (await cli.getCrypto()?.getCrossSigningKeyId()) {
|
2024-07-13 13:36:45 +01:00
|
|
|
logger.debug("SetupEncryptionStore.usePassphrase: done");
|
2021-06-18 12:44:15 +01:00
|
|
|
this.phase = Phase.Done;
|
2020-03-24 15:57:04 +01:00
|
|
|
this.emit("update");
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
2024-07-13 13:36:45 +01:00
|
|
|
if (e instanceof AccessCancelledError) {
|
|
|
|
|
logger.debug("SetupEncryptionStore.usePassphrase: user cancelled access to secret storage");
|
|
|
|
|
} else {
|
|
|
|
|
logger.log("SetupEncryptionStore.usePassphrase: error", e);
|
2020-03-24 15:57:04 +01:00
|
|
|
}
|
2024-07-13 13:36:45 +01:00
|
|
|
|
2021-06-18 12:44:15 +01:00
|
|
|
this.phase = Phase.Intro;
|
2020-03-24 15:57:04 +01:00
|
|
|
this.emit("update");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-15 19:30:43 +01:00
|
|
|
private onUserTrustStatusChanged = async (userId: string): Promise<void> => {
|
2023-06-15 15:11:49 +01:00
|
|
|
if (userId !== MatrixClientPeg.safeGet().getSafeUserId()) return;
|
|
|
|
|
const publicKeysTrusted = await MatrixClientPeg.safeGet().getCrypto()?.getCrossSigningKeyId();
|
2020-09-02 14:13:36 +01:00
|
|
|
if (publicKeysTrusted) {
|
2021-06-18 12:44:15 +01:00
|
|
|
this.phase = Phase.Done;
|
2020-04-30 22:08:00 +01:00
|
|
|
this.emit("update");
|
|
|
|
|
}
|
2021-06-29 13:11:58 +01:00
|
|
|
};
|
2020-04-30 22:08:00 +01:00
|
|
|
|
2021-06-14 20:58:20 +01:00
|
|
|
public onVerificationRequest = (request: VerificationRequest): void => {
|
|
|
|
|
this.setActiveVerificationRequest(request);
|
2021-06-29 13:11:58 +01:00
|
|
|
};
|
2020-03-24 15:57:04 +01:00
|
|
|
|
2023-05-15 19:30:43 +01:00
|
|
|
public onVerificationRequestChange = async (): Promise<void> => {
|
2023-06-06 09:27:53 +01:00
|
|
|
if (this.verificationRequest?.phase === VerificationPhase.Cancelled) {
|
2022-02-22 12:18:08 +00:00
|
|
|
this.verificationRequest.off(VerificationRequestEvent.Change, this.onVerificationRequestChange);
|
2020-03-24 15:57:04 +01:00
|
|
|
this.verificationRequest = null;
|
|
|
|
|
this.emit("update");
|
2023-06-14 15:35:32 +01:00
|
|
|
} else if (this.verificationRequest?.phase === VerificationPhase.Done) {
|
2022-02-22 12:18:08 +00:00
|
|
|
this.verificationRequest.off(VerificationRequestEvent.Change, this.onVerificationRequestChange);
|
2020-04-30 22:08:00 +01:00
|
|
|
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
|
2020-05-01 10:58:00 +01:00
|
|
|
// change (or change to DONE if it's already ready).
|
2023-06-15 15:11:49 +01:00
|
|
|
const publicKeysTrusted = await MatrixClientPeg.safeGet().getCrypto()?.getCrossSigningKeyId();
|
2021-06-18 12:44:15 +01:00
|
|
|
this.phase = publicKeysTrusted ? Phase.Done : Phase.Busy;
|
2020-04-30 22:08:00 +01:00
|
|
|
this.emit("update");
|
2020-03-24 15:57:04 +01:00
|
|
|
}
|
2021-06-29 13:11:58 +01:00
|
|
|
};
|
2020-03-24 15:57:04 +01:00
|
|
|
|
2021-06-14 20:58:20 +01:00
|
|
|
public skip(): void {
|
2021-06-18 12:44:15 +01:00
|
|
|
this.phase = Phase.ConfirmSkip;
|
2020-03-24 15:57:04 +01:00
|
|
|
this.emit("update");
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-14 20:58:20 +01:00
|
|
|
public skipConfirm(): void {
|
2021-06-18 12:44:15 +01:00
|
|
|
this.phase = Phase.Finished;
|
2020-03-24 15:57:04 +01:00
|
|
|
this.emit("update");
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-14 20:58:20 +01:00
|
|
|
public returnAfterSkip(): void {
|
2021-06-18 12:44:15 +01:00
|
|
|
this.phase = Phase.Intro;
|
2020-03-24 15:57:04 +01:00
|
|
|
this.emit("update");
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-08 13:55:31 -04:00
|
|
|
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.
|
2023-01-12 13:25:14 +00:00
|
|
|
await accessSecretStorage(async (): Promise<void> => {
|
2023-06-15 15:11:49 +01:00
|
|
|
const cli = MatrixClientPeg.safeGet();
|
2023-06-19 23:20:14 +01:00
|
|
|
await cli.getCrypto()?.bootstrapCrossSigning({
|
2023-01-12 13:25:14 +00:00
|
|
|
authUploadDeviceSigningKeys: async (makeRequest): Promise<void> => {
|
2023-02-23 08:46:49 +01:00
|
|
|
const cachedPassword = SdkContextClass.instance.accountPasswordStore.getPassword();
|
|
|
|
|
|
|
|
|
|
if (cachedPassword) {
|
|
|
|
|
await makeRequest({
|
|
|
|
|
type: "m.login.password",
|
|
|
|
|
identifier: {
|
|
|
|
|
type: "m.id.user",
|
2023-06-15 15:11:49 +01:00
|
|
|
user: cli.getSafeUserId(),
|
2023-02-23 08:46:49 +01:00
|
|
|
},
|
2023-06-15 15:11:49 +01:00
|
|
|
user: cli.getSafeUserId(),
|
2023-02-23 08:46:49 +01:00
|
|
|
password: cachedPassword,
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-14 17:51:51 +01:00
|
|
|
const { finished } = Modal.createDialog(InteractiveAuthDialog, {
|
2023-09-22 16:39:40 +01:00
|
|
|
title: _t("encryption|bootstrap_title"),
|
2022-06-14 17:51:51 +01:00
|
|
|
matrixClient: cli,
|
|
|
|
|
makeRequest,
|
|
|
|
|
});
|
2021-09-08 13:55:31 -04:00
|
|
|
const [confirmed] = await finished;
|
|
|
|
|
if (!confirmed) {
|
|
|
|
|
throw new Error("Cross-signing key upload auth canceled");
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
setupNewCrossSigning: true,
|
|
|
|
|
});
|
2024-04-15 11:47:15 -04:00
|
|
|
|
|
|
|
|
await initialiseDehydration(true);
|
|
|
|
|
|
2021-09-08 13:55:31 -04:00
|
|
|
this.phase = Phase.Finished;
|
|
|
|
|
}, true);
|
|
|
|
|
} catch (e) {
|
2021-10-15 16:30:53 +02:00
|
|
|
logger.error("Error resetting cross-signing", e);
|
2021-09-08 13:55:31 -04:00
|
|
|
this.phase = Phase.Intro;
|
|
|
|
|
}
|
|
|
|
|
this.emit("update");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public returnAfterReset(): void {
|
|
|
|
|
this.phase = Phase.Intro;
|
|
|
|
|
this.emit("update");
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-14 20:58:20 +01:00
|
|
|
public done(): void {
|
2021-06-18 12:44:15 +01:00
|
|
|
this.phase = Phase.Finished;
|
2020-03-24 15:57:04 +01:00
|
|
|
this.emit("update");
|
2020-04-03 11:49:08 +01:00
|
|
|
// async - ask other clients for keys, if necessary
|
2023-06-15 15:11:49 +01:00
|
|
|
MatrixClientPeg.safeGet().crypto?.cancelAndResendAllOutgoingKeyRequests();
|
2020-03-24 15:57:04 +01:00
|
|
|
}
|
2020-06-15 17:41:22 +01:00
|
|
|
|
2021-06-14 20:58:20 +01:00
|
|
|
private async setActiveVerificationRequest(request: VerificationRequest): Promise<void> {
|
2022-05-26 11:12:49 +01:00
|
|
|
if (!this.started) return; // bail if we were stopped
|
2023-06-15 15:11:49 +01:00
|
|
|
if (request.otherUserId !== MatrixClientPeg.safeGet().getUserId()) return;
|
2020-06-15 17:41:22 +01:00
|
|
|
|
|
|
|
|
if (this.verificationRequest) {
|
2022-02-22 12:18:08 +00:00
|
|
|
this.verificationRequest.off(VerificationRequestEvent.Change, this.onVerificationRequestChange);
|
2020-06-15 17:41:22 +01:00
|
|
|
}
|
|
|
|
|
this.verificationRequest = request;
|
|
|
|
|
await request.accept();
|
2022-02-22 12:18:08 +00:00
|
|
|
request.on(VerificationRequestEvent.Change, this.onVerificationRequestChange);
|
2020-06-15 17:41:22 +01:00
|
|
|
this.emit("update");
|
|
|
|
|
}
|
2021-09-08 13:55:31 -04:00
|
|
|
|
|
|
|
|
public lostKeys(): boolean {
|
|
|
|
|
return !this.hasDevicesToVerifyAgainst && !this.keyInfo;
|
|
|
|
|
}
|
2020-03-24 15:57:04 +01:00
|
|
|
}
|