Skip to content

Commit

Permalink
Remove "Upgrade your encryption" flow in CreateSecretStorageDialog (#…
Browse files Browse the repository at this point in the history
…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 <[email protected]>

---------

Co-authored-by: Richard van der Hoff <[email protected]>
  • Loading branch information
florianduros and richvdh authored Oct 30, 2024
1 parent c23c9df commit 386b782
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 663 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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,
Expand All @@ -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",
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -160,60 +141,15 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
accountPassword,
};

cli.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatusChange);

this.getInitialPhase();
}

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();
if (keyFromCustomisations) this.initExtension(keyFromCustomisations);
}

/**
* 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> {
Expand All @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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");
Expand All @@ -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);
Expand All @@ -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);
}
};
Expand All @@ -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 => {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}>
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 0 additions & 6 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@
"report_content": "Report Content",
"resend": "Resend",
"reset": "Reset",
"restore": "Restore",
"resume": "Resume",
"retry": "Retry",
"review": "Review",
Expand Down Expand Up @@ -2587,18 +2586,13 @@
"pass_phrase_match_failed": "That doesn't match.",
"pass_phrase_match_success": "That matches!",
"phrase_strong_enough": "Great! This Security Phrase looks strong enough.",
"requires_key_restore": "Restore your key backup to upgrade your encryption",
"requires_password_confirmation": "Enter your account password to confirm the upgrade:",
"requires_server_authentication": "You'll need to authenticate with the server to confirm the upgrade.",
"secret_storage_query_failure": "Unable to query secret storage status",
"security_key_safety_reminder": "Store your Security Key somewhere safe, like a password manager or a safe, as it's used to safeguard your encrypted data.",
"session_upgrade_description": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.",
"set_phrase_again": "Go back to set it again.",
"settings_reminder": "You can also set up Secure Backup & manage your keys in Settings.",
"title_confirm_phrase": "Confirm Security Phrase",
"title_save_key": "Save your Security Key",
"title_set_phrase": "Set a Security Phrase",
"title_upgrade_encryption": "Upgrade your encryption",
"unable_to_setup": "Unable to set up secret storage",
"use_different_passphrase": "Use a different passphrase?",
"use_phrase_only_you_know": "Use a secret phrase only you know, and optionally save a Security Key to use for backup."
Expand Down
5 changes: 5 additions & 0 deletions test/test-utils/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ export function createTestClient(): MatrixClient {
prepareToEncrypt: jest.fn(),
bootstrapCrossSigning: jest.fn(),
getActiveSessionBackupVersion: jest.fn().mockResolvedValue(null),
isKeyBackupTrusted: jest.fn().mockResolvedValue({}),
createRecoveryKeyFromPassphrase: jest.fn().mockResolvedValue({}),
bootstrapSecretStorage: jest.fn(),
isDehydrationSupported: jest.fn().mockResolvedValue(false),
}),

getPushActionsForEvent: jest.fn(),
Expand Down Expand Up @@ -270,6 +274,7 @@ export function createTestClient(): MatrixClient {
getOrCreateFilter: jest.fn(),
sendStickerMessage: jest.fn(),
getLocalAliases: jest.fn().mockReturnValue([]),
uploadDeviceSigningKeys: jest.fn(),
} as unknown as MatrixClient;

client.reEmitter = new ReEmitter(client);
Expand Down
Loading

0 comments on commit 386b782

Please sign in to comment.