Avoid using accessSecretStorage to create 4S (#30244)

* remove resetCrossSigning flag, which is no longer in use

* drop unnecessary check for cross-signing

The only place where verifyUser is called already checks that cross-signing is
set up.  (The function name is also incorrect, since it checks for the
cross-signing key, and not for 4S.)

* avoid calling accessSecretStorage to set up cross-signing or 4S

Send the user to the Encryption settings tab instead

* only create secret storage when specifically asked to

* deprecate using accessSecretStorage to create new 4S

* also remove the obsolete snapshot

* add tests

* Tweak comment

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
Hubert Chathi
2025-07-03 13:34:05 +00:00
committed by GitHub
co-authored by Richard van der Hoff
parent 66d7c6a100
commit 9095ebdb1b
17 changed files with 87 additions and 651 deletions
@@ -1,186 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2018, 2019 New Vector Ltd
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import { _t } from "../../../../languageHandler";
import { accessSecretStorage, withSecretStorageKeyCache } from "../../../../SecurityManager";
import Spinner from "../../../../components/views/elements/Spinner";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
enum Phase {
BackingUp = "backing_up",
Done = "done",
}
interface IProps {
onFinished(done?: boolean): void;
}
interface IState {
phase: Phase;
passPhrase: string;
passPhraseValid: boolean;
passPhraseConfirm: string;
copied: boolean;
downloaded: boolean;
error?: boolean;
}
/**
* Walks the user through the process of setting up e2e key backups to a new backup, and storing the decryption key in
* SSSS.
*
* Uses {@link accessSecretStorage}, which means that if 4S is not already configured, it will be bootstrapped (which
* involves displaying an {@link CreateSecretStorageDialog} so the user can enter a passphrase and/or download the 4S
* key).
*/
export default class CreateKeyBackupDialog extends React.PureComponent<IProps, IState> {
public constructor(props: IProps) {
super(props);
this.state = {
phase: Phase.BackingUp,
passPhrase: "",
passPhraseValid: false,
passPhraseConfirm: "",
copied: false,
downloaded: false,
};
}
public componentDidMount(): void {
this.createBackup();
}
private createBackup = async (): Promise<void> => {
this.setState({
error: undefined,
});
const cli = MatrixClientPeg.safeGet();
try {
// Check if 4S already set up
const secretStorageAlreadySetup = await cli.secretStorage.hasKey();
if (!secretStorageAlreadySetup) {
// bootstrap secret storage; that will also create a backup version
await accessSecretStorage(async (): Promise<void> => {
// do nothing, all is now set up correctly
});
} else {
await withSecretStorageKeyCache(async () => {
const crypto = cli.getCrypto();
if (!crypto) {
throw new Error("End-to-end encryption is disabled - unable to create backup.");
}
// Before we reset the backup, let's make sure we can access secret storage, to
// reduce the chance of us getting into a broken state where we have an outdated
// secret in secret storage.
// `SecretStorage.get` will ask the user to enter their passphrase/key if necessary;
// it will then be cached for the actual backup reset operation.
await cli.secretStorage.get("m.megolm_backup.v1");
// We now know we can store the new backup key in secret storage, so it is safe to
// go ahead with the reset.
await crypto.resetKeyBackup();
});
}
this.setState({
phase: Phase.Done,
});
} catch (e) {
logger.error("Error creating key backup", e);
// TODO: If creating a version succeeds, but backup fails, should we
// delete the version, disable backup, or do nothing? If we just
// disable without deleting, we'll enable on next app reload since
// it is trusted.
this.setState({
error: true,
});
}
};
private onCancel = (): void => {
this.props.onFinished(false);
};
private onDone = (): void => {
this.props.onFinished(true);
};
private renderBusyPhase(): JSX.Element {
return (
<div>
<Spinner />
</div>
);
}
private renderPhaseDone(): JSX.Element {
return (
<div>
<p>{_t("settings|key_backup|backup_in_progress")}</p>
<DialogButtons primaryButton={_t("action|ok")} onPrimaryButtonClick={this.onDone} hasCancel={false} />
</div>
);
}
private titleForPhase(phase: Phase): string {
switch (phase) {
case Phase.BackingUp:
return _t("settings|key_backup|backup_starting");
case Phase.Done:
return _t("settings|key_backup|backup_success");
default:
return _t("settings|key_backup|create_title");
}
}
public render(): React.ReactNode {
let content;
if (this.state.error) {
content = (
<div>
<p>{_t("settings|key_backup|cannot_create_backup")}</p>
<DialogButtons
primaryButton={_t("action|retry")}
onPrimaryButtonClick={this.createBackup}
hasCancel={true}
onCancel={this.onCancel}
/>
</div>
);
} else {
switch (this.state.phase) {
case Phase.BackingUp:
content = this.renderBusyPhase();
break;
case Phase.Done:
content = this.renderPhaseDone();
break;
}
}
return (
<BaseDialog
className="mx_CreateKeyBackupDialog"
onFinished={this.props.onFinished}
title={this.titleForPhase(this.state.phase)}
hasCancel={[Phase.Done].includes(this.state.phase)}
>
<div>{content}</div>
</BaseDialog>
);
}
}
@@ -56,7 +56,6 @@ const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow,
interface IProps {
forceReset?: boolean;
resetCrossSigning?: boolean;
onFinished(ok?: boolean): void;
}
@@ -80,11 +79,12 @@ interface IState {
* If the user already has a key backup, follows a "migration" flow (aka "Upgrade your encryption") which
* prompts the user to enter their backup decryption password (a Curve25519 private key, possibly derived
* from a passphrase), and uses that as the (AES) 4S encryption key.
*
* @deprecated send the user to EncryptionUserSettingsTab instead
*/
export default class CreateSecretStorageDialog extends React.PureComponent<IProps, IState> {
public static defaultProps: Partial<IProps> = {
forceReset: false,
resetCrossSigning: false,
};
private recoveryKey?: GeneratedSecretStorageKey;
private recoveryKeyNode = createRef<HTMLElement>();
@@ -211,7 +211,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
private bootstrapSecretStorage = async (): Promise<void> => {
const cli = MatrixClientPeg.safeGet();
const crypto = cli.getCrypto()!;
const { forceReset, resetCrossSigning } = this.props;
const { forceReset } = this.props;
let backupInfo;
// First, unless we know we want to do a reset, we see if there is an existing key backup
@@ -246,13 +246,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
createSecretStorageKey: async () => this.recoveryKey!,
setupNewSecretStorage: true,
});
if (resetCrossSigning) {
logger.log("Resetting cross signing");
await crypto.bootstrapCrossSigning({
authUploadDeviceSigningKeys: this.doBootstrapUIAuth,
setupNewCrossSigning: true,
});
}
logger.log("Resetting key backup");
await crypto.resetKeyBackup();
} else {
@@ -7,14 +7,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { lazy } from "react";
import React from "react";
import dis from "../../../../dispatcher/dispatcher";
import { _t } from "../../../../languageHandler";
import Modal from "../../../../Modal";
import { Action } from "../../../../dispatcher/actions";
import { UserTab } from "../../../../components/views/dialogs/UserTab";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
import { type OpenToTabPayload } from "../../../../dispatcher/payloads/OpenToTabPayload";
interface IProps {
onFinished(): void;
@@ -28,13 +29,12 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent<IPr
private onSetupClick = (): void => {
this.props.onFinished();
Modal.createDialog(
lazy(() => import("./CreateKeyBackupDialog")),
undefined,
undefined,
/* priority = */ false,
/* static = */ true,
);
// Open the user settings dialog to the encryption tab and start the flow to reset encryption
const payload: OpenToTabPayload = {
action: Action.ViewUserSettings,
initialTabId: UserTab.Encryption,
};
dis.dispatch(payload);
};
public render(): React.ReactNode {