From 9418dc60f4022b10092fe4075be2522b2f078321 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 22 Sep 2023 08:38:31 +0000 Subject: [PATCH 1/8] Update browser-actions/setup-chrome digest to 905ab04 (#11615) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cypress.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cypress.yaml b/.github/workflows/cypress.yaml index b1d0e4d4ff..a86ae7efe0 100644 --- a/.github/workflows/cypress.yaml +++ b/.github/workflows/cypress.yaml @@ -121,7 +121,7 @@ jobs: # Run 4 instances in Parallel runner: [1, 2, 3, 4] steps: - - uses: browser-actions/setup-chrome@c485fa3bab6be59dce18dbc18ef6ab7cbc8ff5f1 + - uses: browser-actions/setup-chrome@905ab04587e418717afec9c975bb144050718dc1 - run: echo "BROWSER_PATH=$(which chrome)" >> $GITHUB_ENV # There's a 'download artifact' action, but it hasn't been updated for the workflow_run action From 29280607df4b7117ae9c08046fe9b672743cb572 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 22 Sep 2023 11:52:10 +0100 Subject: [PATCH 2/8] Revert "Update browser-actions/setup-chrome digest to 905ab04" This reverts commit ea38f6366e0181a73567c9e935eb9809adf21a6d which is from PR https://github.com/matrix-org/matrix-react-sdk/pull/11615 --- .github/workflows/cypress.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cypress.yaml b/.github/workflows/cypress.yaml index a86ae7efe0..b1d0e4d4ff 100644 --- a/.github/workflows/cypress.yaml +++ b/.github/workflows/cypress.yaml @@ -121,7 +121,7 @@ jobs: # Run 4 instances in Parallel runner: [1, 2, 3, 4] steps: - - uses: browser-actions/setup-chrome@905ab04587e418717afec9c975bb144050718dc1 + - uses: browser-actions/setup-chrome@c485fa3bab6be59dce18dbc18ef6ab7cbc8ff5f1 - run: echo "BROWSER_PATH=$(which chrome)" >> $GITHUB_ENV # There's a 'download artifact' action, but it hasn't been updated for the workflow_run action From 11f258e62ea2b1880612cae2da1e77eb9926455f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 22 Sep 2023 12:57:11 +0200 Subject: [PATCH 3/8] `SecureBackupPanel`: stop using deprecated APIs, and other fixes (#11644) * SecureBackupPanel: replace `isKeyBackupTrusted` `MatrixClient.isKeyBackupTrusted` -> `CryptoApi.isKeyBackupTrusted` * SecureBackupPanel: replace `getKeyBackupEnabled` `MatrixClient.getKeyBackupEnabled` -> `CryptoApi.getActiveSessionBackupVersion` * SecureBackupPanel: replace `deleteKeyBackupVersion` `MatrixClient.deleteKeyBackupVersion` -> `CryptoApi.deleteKeyBackupVersion` * Do not show session count if we have no info We shouldn't say "zero sessions to back up" if we don't know. * SecureBackupPanel: distinguish between server and active backup --- .../views/settings/SecureBackupPanel.tsx | 77 ++++++++++++++----- src/i18n/strings/en_EN.json | 5 +- .../views/settings/SecureBackupPanel-test.tsx | 25 +++--- .../SecureBackupPanel-test.tsx.snap | 13 +++- 4 files changed, 81 insertions(+), 39 deletions(-) diff --git a/src/components/views/settings/SecureBackupPanel.tsx b/src/components/views/settings/SecureBackupPanel.tsx index 18c2ee6e90..a0416d01a5 100644 --- a/src/components/views/settings/SecureBackupPanel.tsx +++ b/src/components/views/settings/SecureBackupPanel.tsx @@ -16,10 +16,9 @@ limitations under the License. */ import React, { ReactNode } from "react"; -import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; -import { TrustInfo } from "matrix-js-sdk/src/crypto/backup"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { logger } from "matrix-js-sdk/src/logger"; +import { BackupTrustInfo, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; import type CreateKeyBackupDialog from "../../../async-components/views/dialogs/security/CreateKeyBackupDialog"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; @@ -41,9 +40,34 @@ interface IState { backupKeyWellFormed: boolean | null; secretStorageKeyInAccount: boolean | null; secretStorageReady: boolean | null; - backupInfo: IKeyBackupInfo | null; - backupSigStatus: TrustInfo | null; - sessionsRemaining: number; + + /** 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; + + /** + * If key backup is currently enabled, the backup version we are backing up to. + */ + activeBackupVersion: string | null; + + /** + * Number of sessions remaining to be backed up. `null` if we have no information on this. + */ + sessionsRemaining: number | null; } export default class SecureBackupPanel extends React.PureComponent<{}, IState> { @@ -61,8 +85,9 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { secretStorageKeyInAccount: null, secretStorageReady: null, backupInfo: null, - backupSigStatus: null, - sessionsRemaining: 0, + backupTrustInfo: undefined, + activeBackupVersion: null, + sessionsRemaining: null, }; } @@ -101,14 +126,19 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { this.setState({ loading: true }); this.getUpdatedDiagnostics(); try { - const backupInfo = await MatrixClientPeg.safeGet().getKeyBackupVersion(); - const backupSigStatus = backupInfo ? await MatrixClientPeg.safeGet().isKeyBackupTrusted(backupInfo) : null; + const cli = MatrixClientPeg.safeGet(); + const backupInfo = await cli.getKeyBackupVersion(); + const backupTrustInfo = backupInfo ? await cli.getCrypto()?.isKeyBackupTrusted(backupInfo) : undefined; + + const activeBackupVersion = (await cli.getCrypto()?.getActiveSessionBackupVersion()) ?? null; + if (this.unmounted) return; this.setState({ loading: false, error: false, backupInfo, - backupSigStatus, + backupTrustInfo, + activeBackupVersion, }); } catch (e) { logger.log("Unable to fetch key backup status", e); @@ -117,7 +147,8 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { loading: false, error: true, backupInfo: null, - backupSigStatus: null, + backupTrustInfo: undefined, + activeBackupVersion: null, }); } } @@ -173,8 +204,10 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { onFinished: (proceed) => { if (!proceed) return; this.setState({ loading: true }); + const versionToDelete = this.state.backupInfo!.version!; MatrixClientPeg.safeGet() - .deleteKeyBackupVersion(this.state.backupInfo!.version!) + .getCrypto() + ?.deleteKeyBackupVersion(versionToDelete) .then(() => { this.loadBackupStatus(); }); @@ -209,7 +242,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { secretStorageKeyInAccount, secretStorageReady, backupInfo, - backupSigStatus, + backupTrustInfo, sessionsRemaining, } = this.state; @@ -228,7 +261,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { } else if (backupInfo) { let restoreButtonCaption = _t("Restore from Backup"); - if (MatrixClientPeg.safeGet().getKeyBackupEnabled()) { + if (this.state.activeBackupVersion !== null) { statusDescription = ( ✅ {_t("This session is backing up your keys.")} ); @@ -253,7 +286,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { } let uploadStatus: ReactNode; - if (!MatrixClientPeg.safeGet().getKeyBackupEnabled()) { + if (sessionsRemaining === null) { // No upload status to show when backup disabled. uploadStatus = ""; } else if (sessionsRemaining > 0) { @@ -271,19 +304,21 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { } let trustedLocally: string | undefined; - if (backupSigStatus?.trusted_locally) { - trustedLocally = _t("This backup is trusted because it has been restored on this session"); + if (backupTrustInfo?.matchesDecryptionKey) { + trustedLocally = _t("This backup can be restored on this session"); } extraDetailsTableRows = ( <> - {_t("Backup version:")} - {backupInfo.version} + {_t("Latest backup version on server:")} + + {backupInfo.version} ({_t("Algorithm:")} {backupInfo.algorithm}) + - {_t("Algorithm:")} - {backupInfo.algorithm} + {_t("Active backup version:")} + {this.state.activeBackupVersion === null ? _t("None") : this.state.activeBackupVersion} ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 92adb2fce5..556d62f087 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2138,9 +2138,10 @@ "Connect this session to Key Backup": "Connect this session to Key Backup", "Backing up %(sessionsRemaining)s keys…": "Backing up %(sessionsRemaining)s keys…", "All keys backed up": "All keys backed up", - "This backup is trusted because it has been restored on this session": "This backup is trusted because it has been restored on this session", - "Backup version:": "Backup version:", + "This backup can be restored on this session": "This backup can be restored on this session", + "Latest backup version on server:": "Latest backup version on server:", "Algorithm:": "Algorithm:", + "Active backup version:": "Active backup version:", "Your keys are not being backed up from this session.": "Your keys are not being backed up from this session.", "Back up your keys before signing out to avoid losing them.": "Back up your keys before signing out to avoid losing them.", "Set up": "Set up", diff --git a/test/components/views/settings/SecureBackupPanel-test.tsx b/test/components/views/settings/SecureBackupPanel-test.tsx index d5b28981f2..cadd0353aa 100644 --- a/test/components/views/settings/SecureBackupPanel-test.tsx +++ b/test/components/views/settings/SecureBackupPanel-test.tsx @@ -36,11 +36,8 @@ describe("", () => { const client = getMockClientWithEventEmitter({ ...mockClientMethodsUser(userId), ...mockClientMethodsCrypto(), - getKeyBackupEnabled: jest.fn(), getKeyBackupVersion: jest.fn().mockReturnValue("1"), - isKeyBackupTrusted: jest.fn().mockResolvedValue(true), getClientWellKnown: jest.fn(), - deleteKeyBackupVersion: jest.fn(), }); const getComponent = () => render(); @@ -53,15 +50,17 @@ describe("", () => { public_key: "1234", }, }); - client.isKeyBackupTrusted.mockResolvedValue({ - usable: false, - sigs: [], + Object.assign(client.getCrypto()!, { + isKeyBackupTrusted: jest.fn().mockResolvedValue({ + trusted: false, + matchesDecryptionKey: false, + }), + getActiveSessionBackupVersion: jest.fn().mockResolvedValue(null), + deleteKeyBackupVersion: jest.fn().mockResolvedValue(undefined), }); mocked(client.secretStorage.hasKey).mockClear().mockResolvedValue(false); - client.deleteKeyBackupVersion.mockClear().mockResolvedValue(); client.getKeyBackupVersion.mockClear(); - client.isKeyBackupTrusted.mockClear(); mocked(accessSecretStorage).mockClear().mockResolvedValue(); }); @@ -100,7 +99,7 @@ describe("", () => { }); it("displays when session is connected to key backup", async () => { - client.getKeyBackupEnabled.mockReturnValue(true); + mocked(client.getCrypto()!).getActiveSessionBackupVersion.mockResolvedValue("1"); getComponent(); // flush checkKeyBackup promise await flushPromises(); @@ -125,7 +124,7 @@ describe("", () => { fireEvent.click(within(dialog).getByText("Cancel")); - expect(client.deleteKeyBackupVersion).not.toHaveBeenCalled(); + expect(client.getCrypto()!.deleteKeyBackupVersion).not.toHaveBeenCalled(); }); it("deletes backup after confirmation", async () => { @@ -154,7 +153,7 @@ describe("", () => { fireEvent.click(within(dialog).getByTestId("dialog-primary-button")); - expect(client.deleteKeyBackupVersion).toHaveBeenCalledWith("1"); + expect(client.getCrypto()!.deleteKeyBackupVersion).toHaveBeenCalledWith("1"); // delete request await flushPromises(); @@ -169,7 +168,7 @@ describe("", () => { await flushPromises(); client.getKeyBackupVersion.mockClear(); - client.isKeyBackupTrusted.mockClear(); + mocked(client.getCrypto()!).isKeyBackupTrusted.mockClear(); fireEvent.click(screen.getByText("Reset")); @@ -179,6 +178,6 @@ describe("", () => { // backup status refreshed expect(client.getKeyBackupVersion).toHaveBeenCalled(); - expect(client.isKeyBackupTrusted).toHaveBeenCalled(); + expect(client.getCrypto()!.isKeyBackupTrusted).toHaveBeenCalled(); }); }); diff --git a/test/components/views/settings/__snapshots__/SecureBackupPanel-test.tsx.snap b/test/components/views/settings/__snapshots__/SecureBackupPanel-test.tsx.snap index dc00cd38e4..47a6c83f79 100644 --- a/test/components/views/settings/__snapshots__/SecureBackupPanel-test.tsx.snap +++ b/test/components/views/settings/__snapshots__/SecureBackupPanel-test.tsx.snap @@ -140,20 +140,27 @@ exports[` suggests connecting session to key backup when ba - Backup version: + Latest backup version on server: 1 + ( + Algorithm: + + + test + + ) - Algorithm: + Active backup version: - test + None From 6fd46f3bc88567e416344823b3a1212ab551e879 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 22 Sep 2023 13:03:05 +0200 Subject: [PATCH 4/8] `CreateSecretStorageDialog`: stop using deprecated APIs (#11635) * Add some tests for `CreateSecretStorageDialog` * CreateSecretStorageDialog: stop using deprecated APIs --- .../security/CreateSecretStorageDialog.tsx | 78 ++- .../CreateSecretStorageDialog-test.tsx | 224 +++++++ .../CreateSecretStorageDialog-test.tsx.snap | 551 ++++++++++++++++++ test/test-utils/client.ts | 3 +- test/test-utils/test-utils.ts | 2 +- 5 files changed, 830 insertions(+), 28 deletions(-) create mode 100644 test/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx create mode 100644 test/components/views/dialogs/security/__snapshots__/CreateSecretStorageDialog-test.tsx.snap diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 1be7fd0cab..b954854c81 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -1,6 +1,6 @@ /* Copyright 2018, 2019 New Vector Ltd -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020, 2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,12 +18,11 @@ limitations under the License. import React, { createRef } from "react"; import FileSaver from "file-saver"; import { logger } from "matrix-js-sdk/src/logger"; -import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; -import { TrustInfo } from "matrix-js-sdk/src/crypto/backup"; -import { CrossSigningKeys, IAuthDict, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix"; +import { AuthDict, CrossSigningKeys, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix"; import { IRecoveryKey } from "matrix-js-sdk/src/crypto/api"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import classNames from "classnames"; +import { BackupTrustInfo, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; import { MatrixClientPeg } from "../../../../MatrixClientPeg"; import { _t, _td } from "../../../../languageHandler"; @@ -81,8 +80,25 @@ interface IState { copied: boolean; downloaded: boolean; setPassphrase: boolean; - backupInfo: IKeyBackupInfo | null; - backupSigStatus: TrustInfo | null; + + /** 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; @@ -144,7 +160,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { + /** + * 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 { try { const cli = MatrixClientPeg.safeGet(); const backupInfo = await cli.getKeyBackupVersion(); - const backupSigStatus = + const backupTrustInfo = // we may not have started crypto yet, in which case we definitely don't trust the backup - backupInfo && cli.isCryptoEnabled() ? await cli.isKeyBackupTrusted(backupInfo) : null; + backupInfo ? await cli.getCrypto()?.isKeyBackupTrusted(backupInfo) : undefined; const { forceReset } = this.props; const phase = backupInfo && !forceReset ? Phase.Migrate : Phase.ChooseKeyPassphrase; @@ -191,16 +215,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent => { if (this.state.passPhraseKeySelected === SecureBackupSetupMethod.Key) { - this.recoveryKey = await MatrixClientPeg.safeGet().createRecoveryKeyFromPassphrase(); + this.recoveryKey = await MatrixClientPeg.safeGet().getCrypto()!.createRecoveryKeyFromPassphrase(); this.setState({ copied: false, downloaded: false, @@ -255,7 +277,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { e.preventDefault(); - if (this.state.backupSigStatus?.usable) { + if (this.state.backupTrustInfo?.trusted) { this.bootstrapSecretStorage(); } else { this.restoreBackup(); @@ -284,7 +306,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent Promise>, + makeRequest: (authData: AuthDict) => Promise>, ): Promise => { if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) { await makeRequest({ @@ -337,13 +359,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent this.recoveryKey!, setupNewKeyBackup: true, setupNewSecretStorage: true, @@ -356,10 +379,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent this.recoveryKey!, keyBackupInfo: this.state.backupInfo!, setupNewKeyBackup: !this.state.backupInfo, @@ -420,8 +443,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent{_t("Enter your account password to confirm the upgrade:")}
); - } else if (!this.state.backupSigStatus?.usable) { + } else if (!this.state.backupTrustInfo?.trusted) { authPrompt = (
{_t("Restore your key backup to upgrade your encryption")}
diff --git a/test/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx b/test/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx new file mode 100644 index 0000000000..fac786b3c2 --- /dev/null +++ b/test/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx @@ -0,0 +1,224 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { render, RenderResult, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import React from "react"; +import { mocked, MockedObject } from "jest-mock"; +import { CryptoApi, 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 { + filterConsole, + flushPromises, + getMockClientWithEventEmitter, + mockClientMethodsCrypto, + mockClientMethodsServer, +} 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; + 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(), + bootstrapCrossSigning: jest.fn(), + bootstrapSecretStorage: jest.fn(), + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + function renderComponent( + props: Partial> = {}, + ): RenderResult { + const onFinished = jest.fn(); + return render(); + } + + it("shows a loading spinner initially", async () => { + const { container } = renderComponent(); + expect(screen.getByTestId("spinner")).toBeDefined(); + expect(container).toMatchSnapshot(); + 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 () => { + 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/); + expect(result.container).toMatchSnapshot(); + + await userEvent.type(result.getByPlaceholderText("Password"), "my pass"); + result.getByRole("button", { name: "Next" }).click(); + + expect(modalSpy).toHaveBeenCalledWith( + RestoreKeyBackupDialog, + { + keyCallback: expect.any(Function), + showSummary: false, + }, + undefined, + false, + false, + ); + + restoreDialogFinishedDefer.resolve([]); + }); + + it("calls bootstrapSecretStorage once keys are restored if the backup is now trusted", async () => { + mockClient.isCryptoEnabled.mockReturnValue(true); + + 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, + { + keyCallback: expect.any(Function), + showSummary: false, + }, + undefined, + false, + false, + ); + + // simulate RestoreKeyBackupDialog completing, to run that code path + restoreDialogFinishedDefer.resolve([]); + }); + }); +}); diff --git a/test/components/views/dialogs/security/__snapshots__/CreateSecretStorageDialog-test.tsx.snap b/test/components/views/dialogs/security/__snapshots__/CreateSecretStorageDialog-test.tsx.snap new file mode 100644 index 0000000000..2b3d041284 --- /dev/null +++ b/test/components/views/dialogs/security/__snapshots__/CreateSecretStorageDialog-test.tsx.snap @@ -0,0 +1,551 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CreateSecretStorageDialog shows 'Generate a Security Key' text if no key backup is present 1`] = ` +
+
+