Remove "Upgrade your encryption" flow in CreateSecretStorageDialog
(#28290)
* Remove "Upgrade your encryption" flow * Rename and remove tests * Remove `BackupTrustInfo` * Get keybackup when bootstraping the secret storage. * Update src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx 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:
parent
c23c9dfacb
commit
386b782f2a
5 changed files with 146 additions and 694 deletions
|
@ -11,7 +11,7 @@ import React, { createRef } from "react";
|
|||
import FileSaver from "file-saver";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { AuthDict, CrossSigningKeys, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix";
|
||||
import { CryptoEvent, BackupTrustInfo, GeneratedSecretStorageKey, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||
import classNames from "classnames";
|
||||
import CheckmarkIcon from "@vector-im/compound-design-tokens/assets/web/icons/check";
|
||||
|
||||
|
@ -25,7 +25,6 @@ import StyledRadioButton from "../../../../components/views/elements/StyledRadio
|
|||
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
|
||||
import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
||||
import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
|
||||
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
|
||||
import {
|
||||
getSecureBackupSetupMethods,
|
||||
isSecureBackupRequired,
|
||||
|
@ -45,7 +44,6 @@ enum Phase {
|
|||
Loading = "loading",
|
||||
LoadError = "load_error",
|
||||
ChooseKeyPassphrase = "choose_key_passphrase",
|
||||
Migrate = "migrate",
|
||||
Passphrase = "passphrase",
|
||||
PassphraseConfirm = "passphrase_confirm",
|
||||
ShowKey = "show_key",
|
||||
|
@ -72,24 +70,6 @@ interface IState {
|
|||
downloaded: boolean;
|
||||
setPassphrase: boolean;
|
||||
|
||||
/** Information on the current key backup version, as returned by the server.
|
||||
*
|
||||
* `null` could mean any of:
|
||||
* * we haven't yet requested the data from the server.
|
||||
* * we were unable to reach the server.
|
||||
* * the server returned key backup version data we didn't understand or was malformed.
|
||||
* * there is actually no backup on the server.
|
||||
*/
|
||||
backupInfo: KeyBackupInfo | null;
|
||||
|
||||
/**
|
||||
* Information on whether the backup in `backupInfo` is correctly signed, and whether we have the right key to
|
||||
* decrypt it.
|
||||
*
|
||||
* `undefined` if `backupInfo` is null, or if crypto is not enabled in the client.
|
||||
*/
|
||||
backupTrustInfo: BackupTrustInfo | undefined;
|
||||
|
||||
// does the server offer a UI auth flow with just m.login.password
|
||||
// for /keys/device_signing/upload?
|
||||
canUploadKeysWithPasswordOnly: boolean | null;
|
||||
|
@ -141,16 +121,17 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
this.queryKeyUploadAuth();
|
||||
}
|
||||
|
||||
const keyFromCustomisations = ModuleRunner.instance.extensions.cryptoSetup.createSecretStorageKey();
|
||||
const phase = keyFromCustomisations ? Phase.Loading : Phase.ChooseKeyPassphrase;
|
||||
|
||||
this.state = {
|
||||
phase: Phase.Loading,
|
||||
phase,
|
||||
passPhrase: "",
|
||||
passPhraseValid: false,
|
||||
passPhraseConfirm: "",
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
setPassphrase: false,
|
||||
backupInfo: null,
|
||||
backupTrustInfo: undefined,
|
||||
// does the server offer a UI auth flow with just m.login.password
|
||||
// for /keys/device_signing/upload?
|
||||
accountPasswordCorrect: null,
|
||||
|
@ -160,60 +141,15 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
accountPassword,
|
||||
};
|
||||
|
||||
cli.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatusChange);
|
||||
|
||||
this.getInitialPhase();
|
||||
if (keyFromCustomisations) this.initExtension(keyFromCustomisations);
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
MatrixClientPeg.get()?.removeListener(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatusChange);
|
||||
}
|
||||
|
||||
private getInitialPhase(): void {
|
||||
const keyFromCustomisations = ModuleRunner.instance.extensions.cryptoSetup.createSecretStorageKey();
|
||||
if (keyFromCustomisations) {
|
||||
logger.log("CryptoSetupExtension: Created key via extension, jumping to bootstrap step");
|
||||
this.recoveryKey = {
|
||||
privateKey: keyFromCustomisations,
|
||||
};
|
||||
this.bootstrapSecretStorage();
|
||||
return;
|
||||
}
|
||||
|
||||
this.fetchBackupInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to get information on the current backup from the server, and update the state.
|
||||
*
|
||||
* Updates {@link IState.backupInfo} and {@link IState.backupTrustInfo}, and picks an appropriate phase for
|
||||
* {@link IState.phase}.
|
||||
*
|
||||
* @returns If the backup data was retrieved successfully, the trust info for the backup. Otherwise, undefined.
|
||||
*/
|
||||
private async fetchBackupInfo(): Promise<BackupTrustInfo | undefined> {
|
||||
try {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const backupInfo = await cli.getKeyBackupVersion();
|
||||
const backupTrustInfo =
|
||||
// we may not have started crypto yet, in which case we definitely don't trust the backup
|
||||
backupInfo ? await cli.getCrypto()?.isKeyBackupTrusted(backupInfo) : undefined;
|
||||
|
||||
const { forceReset } = this.props;
|
||||
const phase = backupInfo && !forceReset ? Phase.Migrate : Phase.ChooseKeyPassphrase;
|
||||
|
||||
this.setState({
|
||||
phase,
|
||||
backupInfo,
|
||||
backupTrustInfo,
|
||||
});
|
||||
|
||||
return backupTrustInfo;
|
||||
} catch (e) {
|
||||
console.error("Error fetching backup data from server", e);
|
||||
this.setState({ phase: Phase.LoadError });
|
||||
return undefined;
|
||||
}
|
||||
private initExtension(keyFromCustomisations: Uint8Array): void {
|
||||
logger.log("CryptoSetupExtension: Created key via extension, jumping to bootstrap step");
|
||||
this.recoveryKey = {
|
||||
privateKey: keyFromCustomisations,
|
||||
};
|
||||
this.bootstrapSecretStorage();
|
||||
}
|
||||
|
||||
private async queryKeyUploadAuth(): Promise<void> {
|
||||
|
@ -237,10 +173,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
}
|
||||
}
|
||||
|
||||
private onKeyBackupStatusChange = (): void => {
|
||||
if (this.state.phase === Phase.Migrate) this.fetchBackupInfo();
|
||||
};
|
||||
|
||||
private onKeyPassphraseChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
passPhraseKeySelected: e.target.value,
|
||||
|
@ -265,15 +197,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
}
|
||||
};
|
||||
|
||||
private onMigrateFormSubmit = (e: React.FormEvent): void => {
|
||||
e.preventDefault();
|
||||
if (this.state.backupTrustInfo?.trusted) {
|
||||
this.bootstrapSecretStorage();
|
||||
} else {
|
||||
this.restoreBackup();
|
||||
}
|
||||
};
|
||||
|
||||
private onCopyClick = (): void => {
|
||||
const successful = copyNode(this.recoveryKeyNode.current);
|
||||
if (successful) {
|
||||
|
@ -340,16 +263,28 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
};
|
||||
|
||||
private bootstrapSecretStorage = async (): Promise<void> => {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const crypto = cli.getCrypto()!;
|
||||
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
|
||||
if (!forceReset) {
|
||||
try {
|
||||
this.setState({ phase: Phase.Loading });
|
||||
backupInfo = await cli.getKeyBackupVersion();
|
||||
} catch (e) {
|
||||
logger.error("Error fetching backup data from server", e);
|
||||
this.setState({ phase: Phase.LoadError });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
phase: Phase.Storing,
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const crypto = cli.getCrypto()!;
|
||||
|
||||
const { forceReset } = this.props;
|
||||
|
||||
try {
|
||||
if (forceReset) {
|
||||
logger.log("Forcing secret storage reset");
|
||||
|
@ -371,8 +306,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
});
|
||||
await crypto.bootstrapSecretStorage({
|
||||
createSecretStorageKey: async () => this.recoveryKey!,
|
||||
keyBackupInfo: this.state.backupInfo!,
|
||||
setupNewKeyBackup: !this.state.backupInfo,
|
||||
setupNewKeyBackup: !backupInfo,
|
||||
});
|
||||
}
|
||||
await initialiseDehydration(true);
|
||||
|
@ -381,20 +315,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
phase: Phase.Stored,
|
||||
});
|
||||
} catch (e) {
|
||||
if (
|
||||
this.state.canUploadKeysWithPasswordOnly &&
|
||||
e instanceof MatrixError &&
|
||||
e.httpStatus === 401 &&
|
||||
e.data.flows
|
||||
) {
|
||||
this.setState({
|
||||
accountPassword: "",
|
||||
accountPasswordCorrect: false,
|
||||
phase: Phase.Migrate,
|
||||
});
|
||||
} else {
|
||||
this.setState({ error: true });
|
||||
}
|
||||
this.setState({ error: true });
|
||||
logger.error("Error bootstrapping secret storage", e);
|
||||
}
|
||||
};
|
||||
|
@ -403,27 +324,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
private restoreBackup = async (): Promise<void> => {
|
||||
const { finished } = Modal.createDialog(
|
||||
RestoreKeyBackupDialog,
|
||||
{
|
||||
showSummary: false,
|
||||
},
|
||||
undefined,
|
||||
/* priority = */ false,
|
||||
/* static = */ false,
|
||||
);
|
||||
|
||||
await finished;
|
||||
const backupTrustInfo = await this.fetchBackupInfo();
|
||||
if (backupTrustInfo?.trusted && this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
|
||||
this.bootstrapSecretStorage();
|
||||
}
|
||||
};
|
||||
|
||||
private onLoadRetryClick = (): void => {
|
||||
this.setState({ phase: Phase.Loading });
|
||||
this.fetchBackupInfo();
|
||||
this.bootstrapSecretStorage();
|
||||
};
|
||||
|
||||
private onShowKeyContinueClick = (): void => {
|
||||
|
@ -495,12 +397,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
});
|
||||
};
|
||||
|
||||
private onAccountPasswordChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
accountPassword: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
private renderOptionKey(): JSX.Element {
|
||||
return (
|
||||
<StyledRadioButton
|
||||
|
@ -565,55 +461,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
);
|
||||
}
|
||||
|
||||
private renderPhaseMigrate(): JSX.Element {
|
||||
let authPrompt;
|
||||
let nextCaption = _t("action|next");
|
||||
if (this.state.canUploadKeysWithPasswordOnly) {
|
||||
authPrompt = (
|
||||
<div>
|
||||
<div>{_t("settings|key_backup|setup_secure_backup|requires_password_confirmation")}</div>
|
||||
<div>
|
||||
<Field
|
||||
id="mx_CreateSecretStorageDialog_password"
|
||||
type="password"
|
||||
label={_t("common|password")}
|
||||
value={this.state.accountPassword}
|
||||
onChange={this.onAccountPasswordChange}
|
||||
forceValidity={this.state.accountPasswordCorrect === false ? false : undefined}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (!this.state.backupTrustInfo?.trusted) {
|
||||
authPrompt = (
|
||||
<div>
|
||||
<div>{_t("settings|key_backup|setup_secure_backup|requires_key_restore")}</div>
|
||||
</div>
|
||||
);
|
||||
nextCaption = _t("action|restore");
|
||||
} else {
|
||||
authPrompt = <p>{_t("settings|key_backup|setup_secure_backup|requires_server_authentication")}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={this.onMigrateFormSubmit}>
|
||||
<p>{_t("settings|key_backup|setup_secure_backup|session_upgrade_description")}</p>
|
||||
<div>{authPrompt}</div>
|
||||
<DialogButtons
|
||||
primaryButton={nextCaption}
|
||||
onPrimaryButtonClick={this.onMigrateFormSubmit}
|
||||
hasCancel={false}
|
||||
primaryDisabled={!!this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
|
||||
>
|
||||
<button type="button" className="danger" onClick={this.onCancelClick}>
|
||||
{_t("action|skip")}
|
||||
</button>
|
||||
</DialogButtons>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
private renderPhasePassPhrase(): JSX.Element {
|
||||
return (
|
||||
<form onSubmit={this.onPassPhraseNextClick}>
|
||||
|
@ -829,8 +676,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
switch (phase) {
|
||||
case Phase.ChooseKeyPassphrase:
|
||||
return _t("encryption|set_up_toast_title");
|
||||
case Phase.Migrate:
|
||||
return _t("settings|key_backup|setup_secure_backup|title_upgrade_encryption");
|
||||
case Phase.Passphrase:
|
||||
return _t("settings|key_backup|setup_secure_backup|title_set_phrase");
|
||||
case Phase.PassphraseConfirm:
|
||||
|
@ -889,9 +734,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
case Phase.ChooseKeyPassphrase:
|
||||
content = this.renderPhaseChooseKeyPassphrase();
|
||||
break;
|
||||
case Phase.Migrate:
|
||||
content = this.renderPhaseMigrate();
|
||||
break;
|
||||
case Phase.Passphrase:
|
||||
content = this.renderPhasePassPhrase();
|
||||
break;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue