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;
|
||||
|
|
|
@ -103,7 +103,6 @@
|
|||
"report_content": "Report Content",
|
||||
"resend": "Resend",
|
||||
"reset": "Reset",
|
||||
"restore": "Restore",
|
||||
"resume": "Resume",
|
||||
"retry": "Retry",
|
||||
"review": "Review",
|
||||
|
@ -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."
|
||||
|
|
|
@ -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(),
|
||||
|
@ -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);
|
||||
|
|
|
@ -10,42 +10,23 @@ import { render, RenderResult, screen } from "jest-matrix-react";
|
|||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
import { mocked, MockedObject } from "jest-mock";
|
||||
import { Crypto, MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
|
||||
import { defer, IDeferred, sleep } from "matrix-js-sdk/src/utils";
|
||||
import { BackupTrustInfo, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import {
|
||||
filterConsole,
|
||||
flushPromises,
|
||||
getMockClientWithEventEmitter,
|
||||
mockClientMethodsCrypto,
|
||||
mockClientMethodsServer,
|
||||
} from "../../../../../test-utils";
|
||||
import { filterConsole, stubClient } from "../../../../../test-utils";
|
||||
import CreateSecretStorageDialog from "../../../../../../src/async-components/views/dialogs/security/CreateSecretStorageDialog";
|
||||
import Modal from "../../../../../../src/Modal";
|
||||
import RestoreKeyBackupDialog from "../../../../../../src/components/views/dialogs/security/RestoreKeyBackupDialog";
|
||||
|
||||
describe("CreateSecretStorageDialog", () => {
|
||||
let mockClient: MockedObject<MatrixClient>;
|
||||
let mockCrypto: MockedObject<Crypto.CryptoApi>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsServer(),
|
||||
...mockClientMethodsCrypto(),
|
||||
uploadDeviceSigningKeys: jest.fn().mockImplementation(async () => {
|
||||
await sleep(0); // CreateSecretStorageDialog doesn't expect this to resolve immediately
|
||||
throw new MatrixError({ flows: [] });
|
||||
}),
|
||||
});
|
||||
|
||||
mockCrypto = mocked(mockClient.getCrypto()!);
|
||||
Object.assign(mockCrypto, {
|
||||
isKeyBackupTrusted: jest.fn(),
|
||||
isDehydrationSupported: jest.fn(() => false),
|
||||
bootstrapCrossSigning: jest.fn(),
|
||||
bootstrapSecretStorage: jest.fn(),
|
||||
mockClient = mocked(stubClient());
|
||||
mockClient.uploadDeviceSigningKeys.mockImplementation(async () => {
|
||||
await sleep(0); // CreateSecretStorageDialog doesn't expect this to resolve immediately
|
||||
throw new MatrixError({ flows: [] });
|
||||
});
|
||||
// Mock the clipboard API
|
||||
document.execCommand = jest.fn().mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -59,11 +40,37 @@ describe("CreateSecretStorageDialog", () => {
|
|||
return render(<CreateSecretStorageDialog onFinished={onFinished} {...props} />);
|
||||
}
|
||||
|
||||
it("shows a loading spinner initially", async () => {
|
||||
const { container } = renderComponent();
|
||||
expect(screen.getByTestId("spinner")).toBeDefined();
|
||||
expect(container).toMatchSnapshot();
|
||||
await flushPromises();
|
||||
it("handles the happy path", async () => {
|
||||
const result = renderComponent();
|
||||
await result.findByText(
|
||||
"Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.",
|
||||
);
|
||||
expect(result.container).toMatchSnapshot();
|
||||
await userEvent.click(result.getByRole("button", { name: "Continue" }));
|
||||
|
||||
await screen.findByText("Save your Security Key");
|
||||
expect(result.container).toMatchSnapshot();
|
||||
// Copy the key to enable the continue button
|
||||
await userEvent.click(screen.getByRole("button", { name: "Copy" }));
|
||||
expect(result.queryByText("Copied!")).not.toBeNull();
|
||||
await userEvent.click(screen.getByRole("button", { name: "Continue" }));
|
||||
|
||||
await screen.findByText("Your keys are now being backed up from this device.");
|
||||
});
|
||||
|
||||
it("when there is an error when bootstraping the secret storage, it shows an error", async () => {
|
||||
jest.spyOn(mockClient.getCrypto()!, "bootstrapSecretStorage").mockRejectedValue(new Error("error"));
|
||||
|
||||
renderComponent();
|
||||
await screen.findByText(
|
||||
"Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.",
|
||||
);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Continue" }));
|
||||
await screen.findByText("Save your Security Key");
|
||||
await userEvent.click(screen.getByRole("button", { name: "Copy" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Continue" }));
|
||||
|
||||
await screen.findByText("Unable to set up secret storage");
|
||||
});
|
||||
|
||||
describe("when there is an error fetching the backup version", () => {
|
||||
|
@ -75,139 +82,19 @@ describe("CreateSecretStorageDialog", () => {
|
|||
});
|
||||
|
||||
const result = renderComponent();
|
||||
// We go though the dialog until we have to get the key backup
|
||||
await userEvent.click(result.getByRole("button", { name: "Continue" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Copy" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Continue" }));
|
||||
|
||||
// XXX the error message is... misleading.
|
||||
await result.findByText("Unable to query secret storage status");
|
||||
expect(result.container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows 'Generate a Security Key' text if no key backup is present", async () => {
|
||||
const result = renderComponent();
|
||||
await flushPromises();
|
||||
expect(result.container).toMatchSnapshot();
|
||||
result.getByText("Generate a Security Key");
|
||||
});
|
||||
|
||||
describe("when canUploadKeysWithPasswordOnly", () => {
|
||||
// spy on Modal.createDialog
|
||||
let modalSpy: jest.SpyInstance;
|
||||
|
||||
// deferred which should be resolved to indicate that the created dialog has completed
|
||||
let restoreDialogFinishedDefer: IDeferred<[done?: boolean]>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo);
|
||||
mockClient.uploadDeviceSigningKeys.mockImplementation(async () => {
|
||||
await sleep(0);
|
||||
throw new MatrixError({
|
||||
flows: [{ stages: ["m.login.password"] }],
|
||||
});
|
||||
});
|
||||
|
||||
restoreDialogFinishedDefer = defer<[done?: boolean]>();
|
||||
modalSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({
|
||||
finished: restoreDialogFinishedDefer.promise,
|
||||
close: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
it("prompts for a password and then shows RestoreKeyBackupDialog", async () => {
|
||||
const result = renderComponent();
|
||||
await result.findByText(/Enter your account password to confirm the upgrade/);
|
||||
await screen.findByText("Unable to query secret storage status");
|
||||
expect(result.container).toMatchSnapshot();
|
||||
|
||||
await userEvent.type(result.getByPlaceholderText("Password"), "my pass");
|
||||
result.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
expect(modalSpy).toHaveBeenCalledWith(
|
||||
RestoreKeyBackupDialog,
|
||||
{
|
||||
showSummary: false,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
restoreDialogFinishedDefer.resolve([]);
|
||||
});
|
||||
|
||||
it("calls bootstrapSecretStorage once keys are restored if the backup is now trusted", async () => {
|
||||
const result = renderComponent();
|
||||
await result.findByText(/Enter your account password to confirm the upgrade/);
|
||||
expect(result.container).toMatchSnapshot();
|
||||
|
||||
await userEvent.type(result.getByPlaceholderText("Password"), "my pass");
|
||||
result.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
expect(modalSpy).toHaveBeenCalled();
|
||||
|
||||
// While we restore the key backup, its signature becomes accepted
|
||||
mockCrypto.isKeyBackupTrusted.mockResolvedValue({ trusted: true } as BackupTrustInfo);
|
||||
|
||||
restoreDialogFinishedDefer.resolve([]);
|
||||
await flushPromises();
|
||||
|
||||
// XXX no idea why this is a sensible thing to do. I just work here.
|
||||
expect(mockCrypto.bootstrapCrossSigning).toHaveBeenCalled();
|
||||
expect(mockCrypto.bootstrapSecretStorage).toHaveBeenCalled();
|
||||
|
||||
await result.findByText("Your keys are now being backed up from this device.");
|
||||
});
|
||||
|
||||
describe("when there is an error fetching the backup version after RestoreKeyBackupDialog", () => {
|
||||
filterConsole("Error fetching backup data from server");
|
||||
|
||||
it("handles the error sensibly", async () => {
|
||||
const result = renderComponent();
|
||||
await result.findByText(/Enter your account password to confirm the upgrade/);
|
||||
expect(result.container).toMatchSnapshot();
|
||||
|
||||
await userEvent.type(result.getByPlaceholderText("Password"), "my pass");
|
||||
result.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
expect(modalSpy).toHaveBeenCalled();
|
||||
|
||||
mockClient.getKeyBackupVersion.mockImplementation(async () => {
|
||||
throw new Error("bleh bleh");
|
||||
});
|
||||
restoreDialogFinishedDefer.resolve([]);
|
||||
await result.findByText("Unable to query secret storage status");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when backup is present but not trusted", () => {
|
||||
beforeEach(() => {
|
||||
mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo);
|
||||
});
|
||||
|
||||
it("shows migrate text, then 'RestoreKeyBackupDialog' if 'Restore' is clicked", async () => {
|
||||
const result = renderComponent();
|
||||
await result.findByText("Restore your key backup to upgrade your encryption");
|
||||
expect(result.container).toMatchSnapshot();
|
||||
|
||||
// before we click "Restore", set up a spy on createDialog
|
||||
const restoreDialogFinishedDefer = defer<[done?: boolean]>();
|
||||
const modalSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({
|
||||
finished: restoreDialogFinishedDefer.promise,
|
||||
close: jest.fn(),
|
||||
});
|
||||
|
||||
result.getByRole("button", { name: "Restore" }).click();
|
||||
|
||||
expect(modalSpy).toHaveBeenCalledWith(
|
||||
RestoreKeyBackupDialog,
|
||||
{
|
||||
showSummary: false,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
// simulate RestoreKeyBackupDialog completing, to run that code path
|
||||
restoreDialogFinishedDefer.resolve([]);
|
||||
// Now we can get the backup and we retry
|
||||
mockClient.getKeyBackupVersion.mockRestore();
|
||||
await userEvent.click(screen.getByRole("button", { name: "Retry" }));
|
||||
await screen.findByText("Your keys are now being backed up from this device.");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CreateSecretStorageDialog shows 'Generate a Security Key' text if no key backup is present 1`] = `
|
||||
exports[`CreateSecretStorageDialog handles the happy path 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
|
@ -128,7 +128,7 @@ exports[`CreateSecretStorageDialog shows 'Generate a Security Key' text if no ke
|
|||
</div>
|
||||
`;
|
||||
|
||||
exports[`CreateSecretStorageDialog shows a loading spinner initially 1`] = `
|
||||
exports[`CreateSecretStorageDialog handles the happy path 2`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
|
@ -143,19 +143,68 @@ exports[`CreateSecretStorageDialog shows a loading spinner initially 1`] = `
|
|||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
/>
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title mx_CreateSecretStorageDialog_titleWithIcon mx_CreateSecretStorageDialog_secureBackupTitle"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Save your Security Key
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<p>
|
||||
Store your Security Key somewhere safe, like a password manager or a safe, as it's used to safeguard your encrypted data.
|
||||
</p>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
class="mx_CreateSecretStorageDialog_primaryContainer mx_CreateSecretStorageDialog_recoveryKeyPrimarycontainer"
|
||||
>
|
||||
<div
|
||||
aria-label="Loading…"
|
||||
class="mx_Spinner_icon"
|
||||
data-testid="spinner"
|
||||
role="progressbar"
|
||||
style="width: 32px; height: 32px;"
|
||||
/>
|
||||
class="mx_CreateSecretStorageDialog_recoveryKeyContainer"
|
||||
>
|
||||
<div
|
||||
class="mx_CreateSecretStorageDialog_recoveryKey"
|
||||
>
|
||||
<code />
|
||||
</div>
|
||||
<div
|
||||
class="mx_CreateSecretStorageDialog_recoveryKeyButtons"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_Dialog_primary mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Download
|
||||
</div>
|
||||
<span>
|
||||
or
|
||||
</span>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Copy
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -168,331 +217,6 @@ exports[`CreateSecretStorageDialog shows a loading spinner initially 1`] = `
|
|||
</div>
|
||||
`;
|
||||
|
||||
exports[`CreateSecretStorageDialog when backup is present but not trusted shows migrate text, then 'RestoreKeyBackupDialog' if 'Restore' is clicked 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_CreateSecretStorageDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Upgrade your encryption
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<form>
|
||||
<p>
|
||||
Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.
|
||||
</p>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
Restore your key backup to upgrade your encryption
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
class="danger"
|
||||
type="button"
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
type="button"
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly calls bootstrapSecretStorage once keys are restored if the backup is now trusted 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_CreateSecretStorageDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Upgrade your encryption
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<form>
|
||||
<p>
|
||||
Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.
|
||||
</p>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
Enter your account password to confirm the upgrade:
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_Field mx_Field_input"
|
||||
>
|
||||
<input
|
||||
id="mx_CreateSecretStorageDialog_password"
|
||||
label="Password"
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_CreateSecretStorageDialog_password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
class="danger"
|
||||
type="button"
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly prompts for a password and then shows RestoreKeyBackupDialog 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_CreateSecretStorageDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Upgrade your encryption
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<form>
|
||||
<p>
|
||||
Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.
|
||||
</p>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
Enter your account password to confirm the upgrade:
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_Field mx_Field_input"
|
||||
>
|
||||
<input
|
||||
id="mx_CreateSecretStorageDialog_password"
|
||||
label="Password"
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_CreateSecretStorageDialog_password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
class="danger"
|
||||
type="button"
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly when there is an error fetching the backup version after RestoreKeyBackupDialog handles the error sensibly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_CreateSecretStorageDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Upgrade your encryption
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<form>
|
||||
<p>
|
||||
Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.
|
||||
</p>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
Enter your account password to confirm the upgrade:
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_Field mx_Field_input"
|
||||
>
|
||||
<input
|
||||
id="mx_CreateSecretStorageDialog_password"
|
||||
label="Password"
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_CreateSecretStorageDialog_password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
class="danger"
|
||||
type="button"
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CreateSecretStorageDialog when there is an error fetching the backup version shows an error 1`] = `
|
||||
<div>
|
||||
<div
|
||||
|
|
Loading…
Reference in a new issue