Stop showing a dialog prompting the user to enter an old recovery key (#29143)
* SecurityManager: improve logging * Only prompt user for default 4S key We don't really support the concept of having multiple 4S keys active, so prompting the user to enter a non-default 4S key without even telling them which one we want is rather silly. * playwright: factor out helper for setting up 4S We seem to already have about 5 copies of this code, so before I add another, let's factor it out. * Playwright test for dehydrated device in reset flow This should be fixed by the previous commit, so let's check it stays that way.
This commit is contained in:
committed by
GitHub
parent
12932e2dc6
commit
099c3073b6
@@ -10,6 +10,7 @@ import { type Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { completeCreateSecretStorageDialog } from "./utils.ts";
|
||||
|
||||
async function expectBackupVersionToBe(page: Page, version: string) {
|
||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText(
|
||||
@@ -35,19 +36,7 @@ test.describe("Backups", () => {
|
||||
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||
|
||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||
|
||||
// It's the first time and secure storage is not set up, so it will create one
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
|
||||
// copy the recovery key to use it later
|
||||
const securityKey = await app.getClipboard();
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
|
||||
const securityKey = await completeCreateSecretStorageDialog(page);
|
||||
|
||||
// Open the settings again
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
@@ -62,6 +51,7 @@ test.describe("Backups", () => {
|
||||
await expectBackupVersionToBe(page, "1");
|
||||
|
||||
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
|
||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
|
||||
// Delete it
|
||||
await currentDialogLocator.getByTestId("dialog-primary-button").click(); // Click "Delete Backup"
|
||||
|
||||
@@ -8,7 +8,14 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect, test } from "../../element-web-test";
|
||||
import { autoJoin, copyAndContinue, createSharedRoomWithUser, enableKeyBackup, verify } from "./utils";
|
||||
import {
|
||||
autoJoin,
|
||||
completeCreateSecretStorageDialog,
|
||||
copyAndContinue,
|
||||
createSharedRoomWithUser,
|
||||
enableKeyBackup,
|
||||
verify,
|
||||
} from "./utils";
|
||||
import { Bot } from "../../pages/bot";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
@@ -111,18 +118,7 @@ test.describe("Cryptography", function () {
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
||||
|
||||
const dialog = page.locator(".mx_Dialog");
|
||||
// Recovery key is selected by default
|
||||
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||
await copyAndContinue(page);
|
||||
|
||||
// If the device is unverified, there should be a "Setting up keys" step; however, it
|
||||
// can be quite quick, and playwright can miss it, so we can't test for it.
|
||||
|
||||
// Either way, we end up at a success dialog:
|
||||
await expect(dialog.getByText("Secure Backup successful")).toBeVisible();
|
||||
await dialog.getByRole("button", { name: "Done" }).click();
|
||||
await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible();
|
||||
await completeCreateSecretStorageDialog(page);
|
||||
|
||||
// Verify that the SSSS keys are in the account data stored in the server
|
||||
await verifyKey(app, "master");
|
||||
|
||||
@@ -11,6 +11,8 @@ import { Locator, type Page } from "@playwright/test";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { viewRoomSummaryByName } from "../right-panel/utils";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { completeCreateSecretStorageDialog, createBot, logIntoElement } from "./utils.ts";
|
||||
import { Client } from "../../pages/client.ts";
|
||||
|
||||
const ROOM_NAME = "Test room";
|
||||
const NAME = "Alice";
|
||||
@@ -44,7 +46,7 @@ test.use({
|
||||
test.describe("Dehydration", () => {
|
||||
test.skip(isDendrite, "does not yet support dehydration v2");
|
||||
|
||||
test("Create dehydrated device", async ({ page, user, app }, workerInfo) => {
|
||||
test("'Set up secure backup' creates dehydrated device", async ({ page, user, app }, workerInfo) => {
|
||||
// Create a backup (which will create SSSS, and dehydrated device)
|
||||
|
||||
const securityTab = await app.settings.openUserSettings("Security & Privacy");
|
||||
@@ -53,17 +55,7 @@ test.describe("Dehydration", () => {
|
||||
await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible();
|
||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||
|
||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||
|
||||
// It's the first time and secure storage is not set up, so it will create one
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
|
||||
await completeCreateSecretStorageDialog(page);
|
||||
|
||||
// Open the settings again
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
@@ -96,4 +88,49 @@ test.describe("Dehydration", () => {
|
||||
await expect(page.locator(".mx_UserInfo_devices").getByText("Offline device enabled")).toBeVisible();
|
||||
await expect(page.locator(".mx_UserInfo_devices").getByText("Dehydrated device")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("Reset recovery key during login re-creates dehydrated device", async ({
|
||||
page,
|
||||
homeserver,
|
||||
app,
|
||||
credentials,
|
||||
}) => {
|
||||
// Set up cross-signing and recovery
|
||||
const { botClient } = await createBot(page, homeserver, credentials);
|
||||
// ... and dehydration
|
||||
await botClient.evaluate(async (client) => await client.getCrypto().startDehydration());
|
||||
|
||||
const initialDehydratedDeviceIds = await getDehydratedDeviceIds(botClient);
|
||||
expect(initialDehydratedDeviceIds.length).toBe(1);
|
||||
|
||||
await botClient.evaluate(async (client) => client.stopClient());
|
||||
|
||||
// Log in our client
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
// Oh no, we forgot our recovery key
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Reset all" }).click();
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Proceed with reset" }).click();
|
||||
|
||||
await completeCreateSecretStorageDialog(page, { accountPassword: credentials.password });
|
||||
|
||||
// There should be a brand new dehydrated device
|
||||
const dehydratedDeviceIds = await getDehydratedDeviceIds(app.client);
|
||||
expect(dehydratedDeviceIds.length).toBe(1);
|
||||
expect(dehydratedDeviceIds[0]).not.toEqual(initialDehydratedDeviceIds[0]);
|
||||
});
|
||||
});
|
||||
|
||||
async function getDehydratedDeviceIds(client: Client): Promise<string[]> {
|
||||
return await client.evaluate(async (client) => {
|
||||
const userId = client.getUserId();
|
||||
const devices = await client.getCrypto().getUserDeviceInfo([userId]);
|
||||
return Array.from(
|
||||
devices
|
||||
.get(userId)
|
||||
.values()
|
||||
.filter((d) => d.dehydrated)
|
||||
.map((d) => d.deviceId),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -288,19 +288,52 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
|
||||
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await app.page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
||||
const dialog = app.page.locator(".mx_Dialog");
|
||||
// Recovery key is selected by default
|
||||
await dialog.getByRole("button", { name: "Continue" }).click({ timeout: 60000 });
|
||||
|
||||
// copy the text ourselves
|
||||
const securityKey = await dialog.locator(".mx_CreateSecretStorageDialog_recoveryKey code").textContent();
|
||||
await copyAndContinue(app.page);
|
||||
return await completeCreateSecretStorageDialog(app.page);
|
||||
}
|
||||
|
||||
await expect(dialog.getByText("Secure Backup successful")).toBeVisible();
|
||||
await dialog.getByRole("button", { name: "Done" }).click();
|
||||
await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible();
|
||||
/**
|
||||
* Go through the "Set up Secure Backup" dialog (aka the `CreateSecretStorageDialog`).
|
||||
*
|
||||
* Assumes the dialog is already open for some reason (see also {@link enableKeyBackup}).
|
||||
*
|
||||
* @param page - The playwright `Page` fixture.
|
||||
* @param opts - Options object
|
||||
* @param opts.accountPassword - The user's account password. If we are also resetting cross-signing, then we will need
|
||||
* to upload the public cross-signing keys, which will cause the app to prompt for the password.
|
||||
*
|
||||
* @returns the new recovery key.
|
||||
*/
|
||||
export async function completeCreateSecretStorageDialog(
|
||||
page: Page,
|
||||
opts?: { accountPassword?: string },
|
||||
): Promise<string> {
|
||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||
|
||||
return securityKey;
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
|
||||
// "Generate a Security Key" is selected by default
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
|
||||
// copy the recovery key to use it later
|
||||
const recoveryKey = await page.evaluate(() => navigator.clipboard.readText());
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
// If the device is unverified, there should be a "Setting up keys" step.
|
||||
// If this is not the first time we are setting up cross-signing, the app will prompt for our password; otherwise
|
||||
// the step is quite quick, and playwright can miss it, so we can't test for it.
|
||||
if (opts && Object.hasOwn(opts, "accountPassword")) {
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Setting up keys" })).toBeVisible();
|
||||
await page.getByPlaceholder("Password").fill(opts!.accountPassword);
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue" }).click();
|
||||
}
|
||||
|
||||
// Either way, we end up at a success dialog:
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByText("Secure Backup successful")).not.toBeVisible();
|
||||
|
||||
return recoveryKey;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user