From 2461c03b0b832851ce82e9ca16ecdebe722bd68d Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 24 Oct 2024 17:25:28 +0200 Subject: [PATCH 1/5] Remove "Upgrade your encryption" flow --- .../security/CreateSecretStorageDialog.tsx | 121 +------ src/i18n/strings/en_EN.json | 6 - test/test-utils/test-utils.ts | 5 + .../CreateSecretStorageDialog-test.tsx | 203 +++-------- .../CreateSecretStorageDialog-test.tsx.snap | 328 ++++++------------ 5 files changed, 173 insertions(+), 490 deletions(-) diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 2278fb38060..e593a459b75 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -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 { BackupTrustInfo, GeneratedSecretStorageKey, KeyBackupInfo } 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", @@ -160,15 +158,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { - if (this.state.phase === Phase.Migrate) this.fetchBackupInfo(); - }; - private onKeyPassphraseChange = (e: React.ChangeEvent): void => { this.setState({ passPhraseKeySelected: e.target.value, @@ -265,15 +250,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { - e.preventDefault(); - if (this.state.backupTrustInfo?.trusted) { - this.bootstrapSecretStorage(); - } else { - this.restoreBackup(); - } - }; - private onCopyClick = (): void => { const successful = copyNode(this.recoveryKeyNode.current); if (successful) { @@ -381,20 +357,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent => { - 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(); @@ -495,12 +440,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent): void => { - this.setState({ - accountPassword: e.target.value, - }); - }; - private renderOptionKey(): JSX.Element { return ( -
{_t("settings|key_backup|setup_secure_backup|requires_password_confirmation")}
-
- -
- - ); - } else if (!this.state.backupTrustInfo?.trusted) { - authPrompt = ( -
-
{_t("settings|key_backup|setup_secure_backup|requires_key_restore")}
-
- ); - nextCaption = _t("action|restore"); - } else { - authPrompt =

{_t("settings|key_backup|setup_secure_backup|requires_server_authentication")}

; - } - - return ( -
-

{_t("settings|key_backup|setup_secure_backup|session_upgrade_description")}

-
{authPrompt}
- - - -
- ); - } - private renderPhasePassPhrase(): JSX.Element { return (
@@ -829,8 +719,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { let mockClient: MockedObject; - let mockCrypto: MockedObject; 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(() => { @@ -66,114 +49,58 @@ describe("CreateSecretStorageDialog", () => { await flushPromises(); }); - describe("when there is an error fetching the backup version", () => { - filterConsole("Error fetching backup data from server"); - - it("shows an error", async () => { - mockClient.getKeyBackupVersion.mockImplementation(async () => { - throw new Error("bleh bleh"); - }); - - const result = renderComponent(); - // 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 () => { + it("show the standard path", async () => { const result = renderComponent(); - await flushPromises(); + 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(); - result.getByText("Generate a Security Key"); + // 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."); }); - describe("when canUploadKeysWithPasswordOnly", () => { - // spy on Modal.createDialog - let modalSpy: jest.SpyInstance; + 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")); - // deferred which should be resolved to indicate that the created dialog has completed - let restoreDialogFinishedDefer: IDeferred<[done?: boolean]>; + 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" })); - beforeEach(() => { - mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo); - mockClient.uploadDeviceSigningKeys.mockImplementation(async () => { - await sleep(0); - throw new MatrixError({ - flows: [{ stages: ["m.login.password"] }], - }); - }); + await screen.findByText("Unable to set up secret storage"); + }); - restoreDialogFinishedDefer = defer<[done?: boolean]>(); - modalSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({ - finished: restoreDialogFinishedDefer.promise, - close: jest.fn(), - }); - }); + it("when there is an error fetching the backup version handles the error sensibly", async () => { + mockClient.getKeyBackupVersion.mockRejectedValue(new Error("error")); + renderComponent(); - it("prompts for a password and then shows RestoreKeyBackupDialog", async () => { - const result = renderComponent(); - await result.findByText(/Enter your account password to confirm the upgrade/); - expect(result.container).toMatchSnapshot(); + await waitFor(() => expect(screen.queryByText("Unable to query secret storage status")).not.toBeNull()); + }); - await userEvent.type(result.getByPlaceholderText("Password"), "my pass"); - result.getByRole("button", { name: "Next" }).click(); - - expect(modalSpy).toHaveBeenCalledWith( - RestoreKeyBackupDialog, - { - showSummary: false, - }, - undefined, - false, - false, - ); + describe("when there is an error fetching the backup version", () => { + filterConsole("Error fetching backup data from server"); - restoreDialogFinishedDefer.resolve([]); - }); + it("shows an error", async () => { + mockClient.getKeyBackupVersion.mockImplementation(async () => { + throw new Error("bleh bleh"); + }); - 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/); + // XXX the error message is... misleading. + 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).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"); - }); }); }); @@ -182,32 +109,12 @@ describe("CreateSecretStorageDialog", () => { mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo); }); - it("shows migrate text, then 'RestoreKeyBackupDialog' if 'Restore' is clicked", async () => { + it("shows key passphrase change", 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, + await screen.findByText( + "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.", ); - - // simulate RestoreKeyBackupDialog completing, to run that code path - restoreDialogFinishedDefer.resolve([]); + expect(result.container).toMatchSnapshot(); }); }); }); diff --git a/test/unit-tests/components/views/dialogs/security/__snapshots__/CreateSecretStorageDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/security/__snapshots__/CreateSecretStorageDialog-test.tsx.snap index 3ba1018ae11..5ea6a4ffa12 100644 --- a/test/unit-tests/components/views/dialogs/security/__snapshots__/CreateSecretStorageDialog-test.tsx.snap +++ b/test/unit-tests/components/views/dialogs/security/__snapshots__/CreateSecretStorageDialog-test.tsx.snap @@ -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 show the standard path 1`] = `
`; -exports[`CreateSecretStorageDialog shows a loading spinner initially 1`] = ` -
-
-