diff --git a/playwright/e2e/crypto/toasts.spec.ts b/playwright/e2e/crypto/toasts.spec.ts new file mode 100644 index 00000000000..7763cc29c21 --- /dev/null +++ b/playwright/e2e/crypto/toasts.spec.ts @@ -0,0 +1,54 @@ +/* + * Copyright 2025 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 { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api"; + +import { test, expect } from "../../element-web-test"; +import { createBot, deleteCachedSecrets, logIntoElement } from "./utils"; + +test.describe("Key storage out of sync toast", () => { + let recoveryKey: GeneratedSecretStorageKey; + + test.beforeEach(async ({ page, homeserver, credentials }) => { + const res = await createBot(page, homeserver, credentials); + recoveryKey = res.recoveryKey; + + await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey); + + await deleteCachedSecrets(page); + + // We won't be prompted for crypto setup unless we have an e2e room, so make one + await page.getByRole("button", { name: "Add room" }).click(); + await page.getByRole("menuitem", { name: "New room" }).click(); + await page.getByRole("textbox", { name: "Name" }).fill("Test room"); + await page.getByRole("button", { name: "Create room" }).click(); + }); + + test("should prompt for recovery key if 'enter recovery key' pressed", { tag: "@screenshot" }, async ({ page }) => { + // Need to wait for 2 to appear since playwright only evaluates 'first()' initially, so the waiting won't work + await expect(page.getByRole("alert")).toHaveCount(2); + await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png"); + + await page.getByRole("button", { name: "Enter recovery key" }).click(); + await page.locator(".mx_Dialog").getByRole("button", { name: "use your Security Key" }).click(); + + await page.getByRole("textbox", { name: "Security key" }).fill(recoveryKey.encodedPrivateKey); + await page.getByRole("button", { name: "Continue" }).click(); + + await expect(page.getByRole("button", { name: "Enter recovery key" })).not.toBeVisible(); + }); + + test("should open settings to reset flow if 'forgot recovery key' pressed", async ({ page, app, credentials }) => { + await expect(page.getByRole("button", { name: "Enter recovery key" })).toBeVisible(); + + await page.getByRole("button", { name: "Forgot recovery key?" }).click(); + + await expect( + page.getByRole("heading", { name: "Forgot your recovery key? You’ll need to reset your identity." }), + ).toBeVisible(); + }); +}); diff --git a/playwright/e2e/crypto/utils.ts b/playwright/e2e/crypto/utils.ts index 6753ae651c3..d4e276094fc 100644 --- a/playwright/e2e/crypto/utils.ts +++ b/playwright/e2e/crypto/utils.ts @@ -214,6 +214,11 @@ export async function logIntoElement(page: Page, credentials: Credentials, secur // if a securityKey was given, verify the new device if (securityKey !== undefined) { await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key" }).click(); + + const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Security Key" }); + if (await useSecurityKey.isVisible()) { + await useSecurityKey.click(); + } // Fill in the security key await page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey); await page.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click(); diff --git a/playwright/snapshots/crypto/toasts.spec.ts/key-storage-out-of-sync-toast-linux.png b/playwright/snapshots/crypto/toasts.spec.ts/key-storage-out-of-sync-toast-linux.png new file mode 100644 index 00000000000..8e335bd2323 Binary files /dev/null and b/playwright/snapshots/crypto/toasts.spec.ts/key-storage-out-of-sync-toast-linux.png differ diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index 75739a7f454..5203c9b0599 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -50,6 +50,7 @@ import { EncryptionUserSettingsTab } from "../settings/tabs/user/EncryptionUserS interface IProps { initialTabId?: UserTab; showMsc4108QrCode?: boolean; + showResetIdentity?: boolean; sdkContext: SdkContextClass; onFinished(): void; } @@ -91,8 +92,9 @@ function titleForTabID(tabId: UserTab): React.ReactNode { export default function UserSettingsDialog(props: IProps): JSX.Element { const voipEnabled = useSettingValue(UIFeature.Voip); const mjolnirEnabled = useSettingValue("feature_mjolnir"); - // store this prop in state as changing tabs back and forth should clear it + // store these props in state as changing tabs back and forth should clear it const [showMsc4108QrCode, setShowMsc4108QrCode] = useState(props.showMsc4108QrCode); + const [showResetIdentity, setShowResetIdentity] = useState(props.showResetIdentity); const getTabs = (): NonEmptyArray> => { const tabs: Tab[] = []; @@ -184,7 +186,12 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { ); tabs.push( - new Tab(UserTab.Encryption, _td("settings|encryption|title"), , ), + new Tab( + UserTab.Encryption, + _td("settings|encryption|title"), + , + , + ), ); if (showLabsFlags() || SettingsStore.getFeatureSettingNames().some((k) => SettingsStore.getBetaInfo(k))) { @@ -219,8 +226,9 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { const [activeTabId, _setActiveTabId] = useActiveTabWithDefault(getTabs(), UserTab.Account, props.initialTabId); const setActiveTabId = (tabId: UserTab): void => { _setActiveTabId(tabId); - // Clear this so switching away from the tab and back to it will not show the QR code again + // Clear these so switching away from the tab and back to it will not show the QR code again setShowMsc4108QrCode(false); + setShowResetIdentity(false); }; const [activeToast, toastRack] = useActiveToast(); diff --git a/src/components/views/settings/encryption/ResetIdentityPanel.tsx b/src/components/views/settings/encryption/ResetIdentityPanel.tsx index c9113e9fe70..b0a6b3fbf27 100644 --- a/src/components/views/settings/encryption/ResetIdentityPanel.tsx +++ b/src/components/views/settings/encryption/ResetIdentityPanel.tsx @@ -25,12 +25,21 @@ interface ResetIdentityPanelProps { * Called when the cancel button is clicked or when we go back in the breadcrumbs. */ onCancelClick: () => void; + + /** + * The variant of the panel to show. We show more warnings in the 'compromised' variant (no use in showing a user this + * warning if they have to reset because they no longer have their key) + * "compromised" is shown when the user chooses 'reset' explicitly in settings, usually because they believe their + * identity has been compromised. + * "forgot" is shown when the user has just forgotten their passphrase. + */ + variant: "compromised" | "forgot"; } /** * The panel for resetting the identity of the current user. */ -export function ResetIdentityPanel({ onCancelClick, onFinish }: ResetIdentityPanelProps): JSX.Element { +export function ResetIdentityPanel({ onCancelClick, onFinish, variant }: ResetIdentityPanelProps): JSX.Element { const matrixClient = useMatrixClientContext(); return ( @@ -44,7 +53,11 @@ export function ResetIdentityPanel({ onCancelClick, onFinish }: ResetIdentityPan
@@ -59,7 +72,7 @@ export function ResetIdentityPanel({ onCancelClick, onFinish }: ResetIdentityPan {_t("settings|encryption|advanced|breadcrumb_third_description")} - {_t("settings|encryption|advanced|breadcrumb_warning")} + {variant === "compromised" && {_t("settings|encryption|advanced|breadcrumb_warning")}}
+
    +
  1. + + Encryption + +
  2. +
  3. + + Reset encryption + +
  4. +
+ +
+
+
+ + + +
+

+ Forgot your recovery key? You’ll need to reset your identity. +

+
+
+
    +
  • + + Your account details, contacts, preferences, and chat list will be kept +
  • +
  • + + You will lose any message history that’s stored only on the server +
  • +
  • + + You will need to verify all your existing devices and contacts again +
  • +
+
+ +
+ +`; + exports[` should reset the encryption when the continue button is clicked 1`] = `