diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 247dd7edab..b68da87167 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -177,7 +177,6 @@ @import "./views/dialogs/_WidgetCapabilitiesPromptDialog.pcss"; @import "./views/dialogs/security/_AccessSecretStorageDialog.pcss"; @import "./views/dialogs/security/_CreateCrossSigningDialog.pcss"; -@import "./views/dialogs/security/_CreateKeyBackupDialog.pcss"; @import "./views/dialogs/security/_CreateSecretStorageDialog.pcss"; @import "./views/dialogs/security/_KeyBackupFailedDialog.pcss"; @import "./views/dialogs/security/_RestoreKeyBackupDialog.pcss"; diff --git a/res/css/views/dialogs/security/_CreateKeyBackupDialog.pcss b/res/css/views/dialogs/security/_CreateKeyBackupDialog.pcss deleted file mode 100644 index 9bd8539881..0000000000 --- a/res/css/views/dialogs/security/_CreateKeyBackupDialog.pcss +++ /dev/null @@ -1,73 +0,0 @@ -/* -Copyright 2018-2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -.mx_CreateKeyBackupDialog .mx_Dialog_title { - /* TODO: Consider setting this for all dialog titles. */ - margin-bottom: 1em; -} - -.mx_CreateKeyBackupDialog_primaryContainer { - /* FIXME: plinth colour in new theme(s). background-color: $accent; */ - padding: 20px; -} - -.mx_CreateKeyBackupDialog_primaryContainer::after { - content: ""; - clear: both; - display: block; -} - -.mx_CreateKeyBackupDialog_passPhraseContainer { - display: flex; - align-items: flex-start; -} - -.mx_CreateKeyBackupDialog_passPhraseInput { - flex: none; - width: 250px; - border: 1px solid $accent; - border-radius: 5px; - padding: 10px; - margin-bottom: 1em; -} - -.mx_CreateKeyBackupDialog_passPhraseMatch { - margin-left: 20px; -} - -.mx_CreateKeyBackupDialog_recoveryKeyHeader { - margin-bottom: 1em; -} - -.mx_CreateKeyBackupDialog_recoveryKeyContainer { - display: flex; -} - -.mx_CreateKeyBackupDialog_recoveryKey { - width: 262px; - padding: 20px; - color: $info-plinth-fg-color; - background-color: $info-plinth-bg-color; - margin-right: 12px; -} - -.mx_CreateKeyBackupDialog_recoveryKeyButtons { - flex: 1; - display: flex; - align-items: center; -} - -.mx_CreateKeyBackupDialog_recoveryKeyButtons button { - flex: 1; - white-space: nowrap; -} - -.mx_CreateKeyBackupDialog { - details .mx_AccessibleButton { - margin: 1em 0; /* emulate paragraph spacing because we can't put this button in a paragraph due to HTML rules */ - } -} diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index bfd0eb237d..ecf895fd79 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -176,10 +176,11 @@ export async function withSecretStorageKeyCache(func: () => Promise): Prom } export interface AccessSecretStorageOpts { - /** Reset secret storage even if it's already set up. */ + /** + * Reset secret storage even if it's already set up. + * @deprecated send the user to the Encryption settings tab to reset secret storage + */ forceReset?: boolean; - /** Create new cross-signing keys. Only applicable if `forceReset` is `true`. */ - resetCrossSigning?: boolean; } /** @@ -189,8 +190,8 @@ export interface AccessSecretStorageOpts { * provided function. * * Bootstrapping secret storage may take one of these paths: - * 1. Create secret storage from a passphrase and store cross-signing keys - * in secret storage. + * 1. (Only if `opts.forceReset` is set) create secret storage from a passphrase + * and store cross-signing keys in secret storage. * 2. Access existing secret storage by requesting passphrase and accessing * cross-signing keys as needed. * 3. All keys are loaded and there's nothing to do. @@ -199,6 +200,8 @@ export interface AccessSecretStorageOpts { * to ensure the user is prompted only once for their secret storage * passphrase. The cache is then cleared once the provided function completes. * + * Throws an error if secret storage is not set up (and `opts.forceReset` is not set) + * * @param {Function} [func] An operation to perform once secret storage has been * bootstrapped. Optional. * @param [opts] The options to use when accessing secret storage. @@ -219,16 +222,8 @@ async function doAccessSecretStorage(func: () => Promise, opts: AccessSecr throw new Error("End-to-end encryption is disabled - unable to access secret storage."); } - let createNew = false; if (opts.forceReset) { logger.debug("accessSecretStorage: resetting 4S"); - createNew = true; - } else if (!(await cli.secretStorage.hasKey())) { - logger.debug("accessSecretStorage: no 4S key configured, creating a new one"); - createNew = true; - } - - if (createNew) { // This dialog calls bootstrap itself after guiding the user through // passphrase creation. const { finished } = Modal.createDialog( @@ -251,6 +246,9 @@ async function doAccessSecretStorage(func: () => Promise, opts: AccessSecr if (!confirmed) { throw new Error("Secret storage creation canceled"); } + } else if (!(await cli.secretStorage.hasKey())) { + logger.debug("accessSecretStorage: no 4S key configured"); + throw new Error("Secret storage has not been created yet."); } else { logger.debug("accessSecretStorage: bootstrapCrossSigning"); await crypto.bootstrapCrossSigning({ diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx deleted file mode 100644 index 0bc6fea219..0000000000 --- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx +++ /dev/null @@ -1,186 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. -Copyright 2018, 2019 New Vector Ltd - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import React, { type JSX } from "react"; -import { logger } from "matrix-js-sdk/src/logger"; - -import { MatrixClientPeg } from "../../../../MatrixClientPeg"; -import { _t } from "../../../../languageHandler"; -import { accessSecretStorage, withSecretStorageKeyCache } from "../../../../SecurityManager"; -import Spinner from "../../../../components/views/elements/Spinner"; -import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; -import DialogButtons from "../../../../components/views/elements/DialogButtons"; - -enum Phase { - BackingUp = "backing_up", - Done = "done", -} - -interface IProps { - onFinished(done?: boolean): void; -} - -interface IState { - phase: Phase; - passPhrase: string; - passPhraseValid: boolean; - passPhraseConfirm: string; - copied: boolean; - downloaded: boolean; - error?: boolean; -} - -/** - * Walks the user through the process of setting up e2e key backups to a new backup, and storing the decryption key in - * SSSS. - * - * Uses {@link accessSecretStorage}, which means that if 4S is not already configured, it will be bootstrapped (which - * involves displaying an {@link CreateSecretStorageDialog} so the user can enter a passphrase and/or download the 4S - * key). - */ -export default class CreateKeyBackupDialog extends React.PureComponent { - public constructor(props: IProps) { - super(props); - - this.state = { - phase: Phase.BackingUp, - passPhrase: "", - passPhraseValid: false, - passPhraseConfirm: "", - copied: false, - downloaded: false, - }; - } - - public componentDidMount(): void { - this.createBackup(); - } - - private createBackup = async (): Promise => { - this.setState({ - error: undefined, - }); - const cli = MatrixClientPeg.safeGet(); - try { - // Check if 4S already set up - const secretStorageAlreadySetup = await cli.secretStorage.hasKey(); - - if (!secretStorageAlreadySetup) { - // bootstrap secret storage; that will also create a backup version - await accessSecretStorage(async (): Promise => { - // do nothing, all is now set up correctly - }); - } else { - await withSecretStorageKeyCache(async () => { - const crypto = cli.getCrypto(); - if (!crypto) { - throw new Error("End-to-end encryption is disabled - unable to create backup."); - } - - // Before we reset the backup, let's make sure we can access secret storage, to - // reduce the chance of us getting into a broken state where we have an outdated - // secret in secret storage. - // `SecretStorage.get` will ask the user to enter their passphrase/key if necessary; - // it will then be cached for the actual backup reset operation. - await cli.secretStorage.get("m.megolm_backup.v1"); - - // We now know we can store the new backup key in secret storage, so it is safe to - // go ahead with the reset. - await crypto.resetKeyBackup(); - }); - } - - this.setState({ - phase: Phase.Done, - }); - } catch (e) { - logger.error("Error creating key backup", e); - // TODO: If creating a version succeeds, but backup fails, should we - // delete the version, disable backup, or do nothing? If we just - // disable without deleting, we'll enable on next app reload since - // it is trusted. - this.setState({ - error: true, - }); - } - }; - - private onCancel = (): void => { - this.props.onFinished(false); - }; - - private onDone = (): void => { - this.props.onFinished(true); - }; - - private renderBusyPhase(): JSX.Element { - return ( -
- -
- ); - } - - private renderPhaseDone(): JSX.Element { - return ( -
-

{_t("settings|key_backup|backup_in_progress")}

- -
- ); - } - - private titleForPhase(phase: Phase): string { - switch (phase) { - case Phase.BackingUp: - return _t("settings|key_backup|backup_starting"); - case Phase.Done: - return _t("settings|key_backup|backup_success"); - default: - return _t("settings|key_backup|create_title"); - } - } - - public render(): React.ReactNode { - let content; - if (this.state.error) { - content = ( -
-

{_t("settings|key_backup|cannot_create_backup")}

- -
- ); - } else { - switch (this.state.phase) { - case Phase.BackingUp: - content = this.renderBusyPhase(); - break; - case Phase.Done: - content = this.renderPhaseDone(); - break; - } - } - - return ( - -
{content}
-
- ); - } -} diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 0d17ddaa39..4ed369fb13 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -56,7 +56,6 @@ const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, interface IProps { forceReset?: boolean; - resetCrossSigning?: boolean; onFinished(ok?: boolean): void; } @@ -80,11 +79,12 @@ interface IState { * If the user already has a key backup, follows a "migration" flow (aka "Upgrade your encryption") which * prompts the user to enter their backup decryption password (a Curve25519 private key, possibly derived * from a passphrase), and uses that as the (AES) 4S encryption key. + * + * @deprecated send the user to EncryptionUserSettingsTab instead */ export default class CreateSecretStorageDialog extends React.PureComponent { public static defaultProps: Partial = { forceReset: false, - resetCrossSigning: false, }; private recoveryKey?: GeneratedSecretStorageKey; private recoveryKeyNode = createRef(); @@ -211,7 +211,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent => { const cli = MatrixClientPeg.safeGet(); const crypto = cli.getCrypto()!; - const { forceReset, resetCrossSigning } = this.props; + 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 @@ -246,13 +246,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent this.recoveryKey!, setupNewSecretStorage: true, }); - if (resetCrossSigning) { - logger.log("Resetting cross signing"); - await crypto.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: this.doBootstrapUIAuth, - setupNewCrossSigning: true, - }); - } logger.log("Resetting key backup"); await crypto.resetKeyBackup(); } else { diff --git a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx index 7e82ff722b..7c3abf37b1 100644 --- a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx +++ b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx @@ -7,14 +7,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { lazy } from "react"; +import React from "react"; import dis from "../../../../dispatcher/dispatcher"; import { _t } from "../../../../languageHandler"; -import Modal from "../../../../Modal"; import { Action } from "../../../../dispatcher/actions"; +import { UserTab } from "../../../../components/views/dialogs/UserTab"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import DialogButtons from "../../../../components/views/elements/DialogButtons"; +import { type OpenToTabPayload } from "../../../../dispatcher/payloads/OpenToTabPayload"; interface IProps { onFinished(): void; @@ -28,13 +29,12 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent { this.props.onFinished(); - Modal.createDialog( - lazy(() => import("./CreateKeyBackupDialog")), - undefined, - undefined, - /* priority = */ false, - /* static = */ true, - ); + // Open the user settings dialog to the encryption tab and start the flow to reset encryption + const payload: OpenToTabPayload = { + action: Action.ViewUserSettings, + initialTabId: UserTab.Encryption, + }; + dis.dispatch(payload); }; public render(): React.ReactNode { diff --git a/src/components/views/dialogs/LogoutDialog.tsx b/src/components/views/dialogs/LogoutDialog.tsx index 6e000ef631..830e5fb589 100644 --- a/src/components/views/dialogs/LogoutDialog.tsx +++ b/src/components/views/dialogs/LogoutDialog.tsx @@ -13,9 +13,11 @@ import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import Modal from "../../../Modal"; import dis from "../../../dispatcher/dispatcher"; +import { type OpenToTabPayload } from "../../../dispatcher/payloads/OpenToTabPayload"; +import { Action } from "../../../dispatcher/actions"; +import { UserTab } from "../../../components/views/dialogs/UserTab"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import RestoreKeyBackupDialog from "./security/RestoreKeyBackupDialog"; import QuestionDialog from "./QuestionDialog"; import BaseDialog from "./BaseDialog"; import Spinner from "../elements/Spinner"; @@ -138,26 +140,12 @@ export default class LogoutDialog extends React.Component { }; private onSetRecoveryMethodClick = (): void => { - if (this.state.backupStatus === BackupStatus.SERVER_BACKUP_BUT_DISABLED) { - // A key backup exists for this account, but the creating device is not - // verified, so restore the backup which will give us the keys from it and - // allow us to trust it (ie. upload keys to it) - Modal.createDialog( - RestoreKeyBackupDialog, - undefined, - undefined, - /* priority = */ false, - /* static = */ true, - ); - } else { - Modal.createDialog( - lazy(() => import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog")), - undefined, - undefined, - /* priority = */ false, - /* static = */ true, - ); - } + // Open the user settings dialog to the encryption tab and start the flow to reset encryption + const payload: OpenToTabPayload = { + action: Action.ViewUserSettings, + initialTabId: UserTab.Encryption, + }; + dis.dispatch(payload); // close dialog this.props.onFinished(true); @@ -190,22 +178,13 @@ export default class LogoutDialog extends React.Component { ); - let setupButtonCaption; - if (this.state.backupStatus === BackupStatus.SERVER_BACKUP_BUT_DISABLED) { - setupButtonCaption = _t("settings|security|key_backup_connect"); - } else { - // if there's an error fetching the backup info, we'll just assume there's - // no backup for the purpose of the button caption - setupButtonCaption = _t("auth|logout_dialog|use_key_backup"); - } - const dialogContent = (
{description}
your config for incorrect or duplicate entries.", "misconfigured_title": "Your %(brand)s is misconfigured", @@ -2712,11 +2711,6 @@ }, "jump_to_bottom_on_send": "Jump to the bottom of the timeline when you send a message", "key_backup": { - "backup_in_progress": "Your keys are being backed up (the first backup could take a few minutes).", - "backup_starting": "Starting backup…", - "backup_success": "Success!", - "cannot_create_backup": "Unable to create key backup", - "create_title": "Create key backup", "setup_secure_backup": { "backup_setup_success_description": "Your keys are now being backed up from this device.", "backup_setup_success_title": "Secure Backup successful", @@ -2877,7 +2871,6 @@ "ignore_users_empty": "You have no ignored users.", "ignore_users_section": "Ignored users", "key_backup_algorithm": "Algorithm:", - "key_backup_connect": "Connect this session to Key Backup", "message_search_disable_warning": "If disabled, messages from encrypted rooms won't appear in search results.", "message_search_disabled": "Securely cache encrypted messages locally for them to appear in search results.", "message_search_enabled": { diff --git a/src/toasts/SetupEncryptionToast.ts b/src/toasts/SetupEncryptionToast.ts index 5a3f1e5a39..f4aef8881f 100644 --- a/src/toasts/SetupEncryptionToast.ts +++ b/src/toasts/SetupEncryptionToast.ts @@ -154,6 +154,7 @@ export const showToast = (kind: Kind): void => { const onPrimaryClick = async (): Promise => { switch (kind) { + case Kind.SET_UP_RECOVERY: case Kind.TURN_ON_KEY_STORAGE: { // Open the user settings dialog to the encryption tab const payload: OpenToTabPayload = { @@ -166,7 +167,6 @@ export const showToast = (kind: Kind): void => { case Kind.VERIFY_THIS_SESSION: Modal.createDialog(SetupEncryptionDialog, {}, undefined, /* priority = */ false, /* static = */ true); break; - case Kind.SET_UP_RECOVERY: case Kind.KEY_STORAGE_OUT_OF_SYNC: case Kind.KEY_STORAGE_OUT_OF_SYNC_STORE: { const modal = Modal.createDialog( @@ -248,7 +248,7 @@ export const showToast = (kind: Kind): void => { * key, to create a new 4S that we can store the secrets in. */ const onAccessSecretStorageFailed = ( - kind: Kind.SET_UP_RECOVERY | Kind.KEY_STORAGE_OUT_OF_SYNC | Kind.KEY_STORAGE_OUT_OF_SYNC_STORE, + kind: Kind.KEY_STORAGE_OUT_OF_SYNC | Kind.KEY_STORAGE_OUT_OF_SYNC_STORE, error: Error, ): void => { if (error instanceof AccessCancelledError) { diff --git a/src/verification.ts b/src/verification.ts index 5dc3ea2979..e3e6fb2293 100644 --- a/src/verification.ts +++ b/src/verification.ts @@ -7,35 +7,24 @@ Please see LICENSE files in the repository root for full details. */ import { type User, type MatrixClient, type RoomMember } from "matrix-js-sdk/src/matrix"; -import { CrossSigningKey, type VerificationRequest } from "matrix-js-sdk/src/crypto-api"; +import { type VerificationRequest } from "matrix-js-sdk/src/crypto-api"; import dis from "./dispatcher/dispatcher"; import { RightPanelPhases } from "./stores/right-panel/RightPanelStorePhases"; -import { accessSecretStorage } from "./SecurityManager"; import RightPanelStore from "./stores/right-panel/RightPanelStore"; import { type IRightPanelCardState } from "./stores/right-panel/RightPanelStoreIPanelState"; import { findDMForUser } from "./utils/dm/findDMForUser"; -async function enable4SIfNeeded(matrixClient: MatrixClient): Promise { - const crypto = matrixClient.getCrypto(); - if (!crypto) return false; - const usk = await crypto.getCrossSigningKeyId(CrossSigningKey.UserSigning); - if (!usk) { - await accessSecretStorage(); - return false; - } - - return true; -} - +/** + * Verify another user. + * + * Note: cross-signing must be set up before calling this function. + */ export async function verifyUser(matrixClient: MatrixClient, user: User): Promise { if (matrixClient.isGuest()) { dis.dispatch({ action: "require_registration" }); return; } - if (!(await enable4SIfNeeded(matrixClient))) { - return; - } const existingRequest = pendingVerificationRequestForUser(matrixClient, user); setRightPanel({ member: user, verificationRequest: existingRequest }); } diff --git a/test/unit-tests/SecurityManager-test.ts b/test/unit-tests/SecurityManager-test.ts index 8c1671b52e..20d93b2c24 100644 --- a/test/unit-tests/SecurityManager-test.ts +++ b/test/unit-tests/SecurityManager-test.ts @@ -67,6 +67,17 @@ describe("SecurityManager", () => { await accessSecretStorage(jest.fn()); }).rejects.toThrow("End-to-end encryption is disabled - unable to access secret storage"); }); + + it("throws if there is no 4S", async () => { + // Given a client with no default 4S key ID + stubClient(); + + // When I run accessSecretStorage + // Then we throw an error + await expect(async () => { + await accessSecretStorage(jest.fn()); + }).rejects.toThrow("Secret storage has not been created yet"); + }); }); it("should show CreateSecretStorageDialog if forceReset=true", async () => { diff --git a/test/unit-tests/async-components/dialogs/security/RecoveryMethodRemovedDialog-test.tsx b/test/unit-tests/async-components/dialogs/security/RecoveryMethodRemovedDialog-test.tsx index ec5e714286..8779748b8e 100644 --- a/test/unit-tests/async-components/dialogs/security/RecoveryMethodRemovedDialog-test.tsx +++ b/test/unit-tests/async-components/dialogs/security/RecoveryMethodRemovedDialog-test.tsx @@ -9,7 +9,9 @@ import React from "react"; import { render, screen, fireEvent, waitFor } from "jest-matrix-react"; import RecoveryMethodRemovedDialog from "../../../../../src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog"; -import Modal from "../../../../../src/Modal.tsx"; +import dispatch from "../../../../../src/dispatcher/dispatcher"; +import { Action } from "../../../../../src/dispatcher/actions"; +import { UserTab } from "../../../../../src/components/views/dialogs/UserTab"; describe("", () => { afterEach(() => { @@ -18,16 +20,15 @@ describe("", () => { it("should open CreateKeyBackupDialog on primary action click", async () => { const onFinished = jest.fn(); - const spy = jest.spyOn(Modal, "createDialog"); - jest.mock("../../../../../src/async-components/views/dialogs/security/CreateKeyBackupDialog", () => ({ - __test: true, - __esModule: true, - default: () => mocked dialog, - })); + jest.spyOn(dispatch, "dispatch"); render(); fireEvent.click(screen.getByRole("button", { name: "Set up Secure Messages" })); - await waitFor(() => expect(spy).toHaveBeenCalledTimes(1)); - expect((spy.mock.lastCall![0] as any)._payload._result).toEqual(expect.objectContaining({ __test: true })); + await waitFor(() => + expect(dispatch.dispatch).toHaveBeenCalledWith({ + action: Action.ViewUserSettings, + initialTabId: UserTab.Encryption, + }), + ); }); }); diff --git a/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx b/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx index cee615380f..4e8a017ed8 100644 --- a/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx @@ -10,10 +10,13 @@ import React from "react"; import { mocked, type MockedObject } from "jest-mock"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { type CryptoApi, type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; -import { fireEvent, render, type RenderResult, screen } from "jest-matrix-react"; +import { fireEvent, render, type RenderResult, screen, waitFor } from "jest-matrix-react"; import { filterConsole, getMockClientWithEventEmitter, mockClientMethodsCrypto } from "../../../../test-utils"; import LogoutDialog from "../../../../../src/components/views/dialogs/LogoutDialog"; +import dispatch from "../../../../../src/dispatcher/dispatcher"; +import { Action } from "../../../../../src/dispatcher/actions"; +import { UserTab } from "../../../../../src/components/views/dialogs/UserTab"; describe("LogoutDialog", () => { let mockClient: MockedObject; @@ -56,17 +59,26 @@ describe("LogoutDialog", () => { await rendered.findByText("You'll lose access to your encrypted messages"); }); - it("Prompts user to connect backup if there is a backup on the server", async () => { + it("Prompts user to go to settings if there is a backup on the server", async () => { mockCrypto.getKeyBackupInfo.mockResolvedValue({} as KeyBackupInfo); const rendered = renderComponent(); - await rendered.findByText("Connect this session to Key Backup"); + await rendered.findByText("Go to Settings"); expect(rendered.container).toMatchSnapshot(); + + jest.spyOn(dispatch, "dispatch"); + fireEvent.click(await screen.findByRole("button", { name: "Go to Settings" })); + await waitFor(() => + expect(dispatch.dispatch).toHaveBeenCalledWith({ + action: Action.ViewUserSettings, + initialTabId: UserTab.Encryption, + }), + ); }); - it("Prompts user to set up backup if there is no backup on the server", async () => { + it("Prompts user to go to settings if there is no backup on the server", async () => { mockCrypto.getKeyBackupInfo.mockResolvedValue(null); const rendered = renderComponent(); - await rendered.findByText("Start using Key Backup"); + await rendered.findByText("Go to Settings"); expect(rendered.container).toMatchSnapshot(); fireEvent.click(await screen.findByRole("button", { name: "Manually export keys" })); @@ -75,12 +87,12 @@ describe("LogoutDialog", () => { describe("when there is an error fetching backups", () => { filterConsole("Unable to fetch key backup status"); - it("prompts user to set up backup", async () => { + it("prompts user to go to settings", async () => { mockCrypto.getKeyBackupInfo.mockImplementation(async () => { throw new Error("beep"); }); const rendered = renderComponent(); - await rendered.findByText("Start using Key Backup"); + await rendered.findByText("Go to Settings"); }); }); }); diff --git a/test/unit-tests/components/views/dialogs/__snapshots__/LogoutDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/__snapshots__/LogoutDialog-test.tsx.snap index c5db254cb2..31d5a09a4f 100644 --- a/test/unit-tests/components/views/dialogs/__snapshots__/LogoutDialog-test.tsx.snap +++ b/test/unit-tests/components/views/dialogs/__snapshots__/LogoutDialog-test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`LogoutDialog Prompts user to connect backup if there is a backup on the server 1`] = ` +exports[`LogoutDialog Prompts user to go to settings if there is a backup on the server 1`] = `
- Connect this session to Key Backup + Go to Settings
@@ -87,7 +87,7 @@ exports[`LogoutDialog Prompts user to connect backup if there is a backup on the
`; -exports[`LogoutDialog Prompts user to set up backup if there is no backup on the server 1`] = ` +exports[`LogoutDialog Prompts user to go to settings if there is no backup on the server 1`] = `
- Start using Key Backup + Go to Settings
diff --git a/test/unit-tests/components/views/dialogs/security/CreateKeyBackupDialog-test.tsx b/test/unit-tests/components/views/dialogs/security/CreateKeyBackupDialog-test.tsx deleted file mode 100644 index b5f965cfa2..0000000000 --- a/test/unit-tests/components/views/dialogs/security/CreateKeyBackupDialog-test.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * Copyright 2023 The Matrix.org Foundation C.I.C. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { render, screen, waitFor } from "jest-matrix-react"; -import React from "react"; -import { mocked } from "jest-mock"; - -import CreateKeyBackupDialog from "../../../../../../src/async-components/views/dialogs/security/CreateKeyBackupDialog"; -import { createTestClient } from "../../../../../test-utils"; -import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg"; - -jest.mock("../../../../../../src/SecurityManager", () => ({ - accessSecretStorage: jest.fn().mockResolvedValue(undefined), - withSecretStorageKeyCache: jest.fn().mockImplementation((fn) => fn()), -})); - -describe("CreateKeyBackupDialog", () => { - beforeEach(() => { - MatrixClientPeg.safeGet = MatrixClientPeg.get = () => createTestClient(); - }); - - it("should display the spinner when creating backup", () => { - const { asFragment } = render(); - - // Check if the spinner is displayed - expect(screen.getByTestId("spinner")).toBeDefined(); - expect(asFragment()).toMatchSnapshot(); - }); - - it("should display an error message when backup creation failed", async () => { - const matrixClient = createTestClient(); - jest.spyOn(matrixClient.secretStorage, "hasKey").mockResolvedValue(true); - mocked(matrixClient.getCrypto()!.resetKeyBackup).mockImplementation(() => { - throw new Error("failed"); - }); - MatrixClientPeg.safeGet = MatrixClientPeg.get = () => matrixClient; - - const { asFragment } = render(); - - // Check if the error message is displayed - await waitFor(() => expect(screen.getByText("Unable to create key backup")).toBeDefined()); - expect(asFragment()).toMatchSnapshot(); - }); - - it("should display an error message when there is no Crypto available", async () => { - const matrixClient = createTestClient(); - jest.spyOn(matrixClient.secretStorage, "hasKey").mockResolvedValue(true); - mocked(matrixClient.getCrypto).mockReturnValue(undefined); - MatrixClientPeg.safeGet = MatrixClientPeg.get = () => matrixClient; - - render(); - - // Check if the error message is displayed - await waitFor(() => expect(screen.getByText("Unable to create key backup")).toBeDefined()); - }); - - it("should display the success dialog when the key backup is finished", async () => { - const onFinished = jest.fn(); - const { asFragment } = render(); - - await waitFor(() => - expect( - screen.getByText("Your keys are being backed up (the first backup could take a few minutes)."), - ).toBeDefined(), - ); - expect(asFragment()).toMatchSnapshot(); - - // Click on the OK button - screen.getByRole("button", { name: "OK" }).click(); - expect(onFinished).toHaveBeenCalledWith(true); - }); -}); diff --git a/test/unit-tests/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx b/test/unit-tests/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx index 000f6efdb4..da68906c63 100644 --- a/test/unit-tests/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx @@ -13,7 +13,7 @@ import { mocked, type MockedObject } from "jest-mock"; import { type MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; import { sleep } from "matrix-js-sdk/src/utils"; -import { filterConsole, flushPromises, stubClient } from "../../../../../test-utils"; +import { filterConsole, stubClient } from "../../../../../test-utils"; import CreateSecretStorageDialog from "../../../../../../src/async-components/views/dialogs/security/CreateSecretStorageDialog"; describe("CreateSecretStorageDialog", () => { @@ -97,39 +97,4 @@ describe("CreateSecretStorageDialog", () => { await screen.findByText("Your keys are now being backed up from this device."); }); }); - - it("resets keys in the right order when resetting secret storage and cross-signing", async () => { - const result = renderComponent({ forceReset: true, resetCrossSigning: true }); - - await result.findByText(/Set up Secure Backup/); - jest.spyOn(mockClient.getCrypto()!, "createRecoveryKeyFromPassphrase").mockResolvedValue({ - privateKey: new Uint8Array(), - encodedPrivateKey: "abcd efgh ijkl", - }); - result.getByRole("button", { name: "Continue" }).click(); - - await result.findByText(/Save your Recovery Key/); - result.getByRole("button", { name: "Copy" }).click(); - - // Resetting should reset secret storage, cross signing, and key - // backup. We make sure that all three are reset, and done in the - // right order. - const resetFunctionCallLog: string[] = []; - jest.spyOn(mockClient.getCrypto()!, "bootstrapSecretStorage").mockImplementation(async () => { - resetFunctionCallLog.push("bootstrapSecretStorage"); - }); - jest.spyOn(mockClient.getCrypto()!, "bootstrapCrossSigning").mockImplementation(async () => { - resetFunctionCallLog.push("bootstrapCrossSigning"); - }); - jest.spyOn(mockClient.getCrypto()!, "resetKeyBackup").mockImplementation(async () => { - resetFunctionCallLog.push("resetKeyBackup"); - }); - - await flushPromises(); - result.getByRole("button", { name: "Continue" }).click(); - - await result.findByText("Your keys are now being backed up from this device."); - - expect(resetFunctionCallLog).toEqual(["bootstrapSecretStorage", "bootstrapCrossSigning", "resetKeyBackup"]); - }); }); diff --git a/test/unit-tests/components/views/dialogs/security/__snapshots__/CreateKeyBackupDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/security/__snapshots__/CreateKeyBackupDialog-test.tsx.snap deleted file mode 100644 index 60a051eec9..0000000000 --- a/test/unit-tests/components/views/dialogs/security/__snapshots__/CreateKeyBackupDialog-test.tsx.snap +++ /dev/null @@ -1,168 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CreateKeyBackupDialog should display an error message when backup creation failed 1`] = ` - -
- -
- -`; - -exports[`CreateKeyBackupDialog should display the spinner when creating backup 1`] = ` - -
-