diff --git a/playwright/e2e/crypto/device-verification.spec.ts b/playwright/e2e/crypto/device-verification.spec.ts index 83a81c260c..05e010cdf2 100644 --- a/playwright/e2e/crypto/device-verification.spec.ts +++ b/playwright/e2e/crypto/device-verification.spec.ts @@ -15,6 +15,7 @@ import { awaitVerifier, checkDeviceIsConnectedKeyBackup, checkDeviceIsCrossSigned, + createBot, doTwoWaySasVerification, logIntoElement, waitForVerificationRequest, @@ -28,29 +29,9 @@ test.describe("Device verification", () => { let expectedBackupVersion: string; test.beforeEach(async ({ page, homeserver, credentials }) => { - // Visit the login page of the app, to load the matrix sdk - await page.goto("/#/login"); - - // wait for the page to load - await page.waitForSelector(".mx_AuthPage", { timeout: 30000 }); - - // Create a new device for alice - aliceBotClient = new Bot(page, homeserver, { - bootstrapCrossSigning: true, - bootstrapSecretStorage: true, - }); - aliceBotClient.setCredentials(credentials); - - // Backup is prepared in the background. Poll until it is ready. - const botClientHandle = await aliceBotClient.prepareClient(); - await expect - .poll(async () => { - expectedBackupVersion = await botClientHandle.evaluate((cli) => - cli.getCrypto()!.getActiveSessionBackupVersion(), - ); - return expectedBackupVersion; - }) - .not.toBe(null); + const res = await createBot(page, homeserver, credentials); + aliceBotClient = res.aliceBotClient; + expectedBackupVersion = res.expectedBackupVersion; }); // Click the "Verify with another device" button, and have the bot client auto-accept it. diff --git a/playwright/e2e/crypto/utils.ts b/playwright/e2e/crypto/utils.ts index 94b1933977..1b271f7b3c 100644 --- a/playwright/e2e/crypto/utils.ts +++ b/playwright/e2e/crypto/utils.ts @@ -12,6 +12,7 @@ import type { ICreateRoomOpts, MatrixClient } from "matrix-js-sdk/src/matrix"; import type { CryptoEvent, EmojiMapping, + GeneratedSecretStorageKey, ShowSasCallbacks, VerificationRequest, Verifier, @@ -22,6 +23,47 @@ import { Client } from "../../pages/client"; import { ElementAppPage } from "../../pages/ElementAppPage"; import { Bot } from "../../pages/bot"; +/** + * Create a bot and wait for it to be ready to use. + * @param page - the page to use + * @param homeserver - the homeserver to use + * @param credentials - the credentials to use for the bot client + * @param ignoreSecretStorage - whether to ignore secret storage setup + */ +export async function createBot( + page: Page, + homeserver: HomeserverInstance, + credentials: Credentials, +): Promise<{ aliceBotClient: Bot; recoveryKey: GeneratedSecretStorageKey; expectedBackupVersion: string }> { + // Visit the login page of the app, to load the matrix sdk + await page.goto("/#/login"); + + // wait for the page to load + await page.waitForSelector(".mx_AuthPage", { timeout: 30000 }); + + // Create a new device for alice + const aliceBotClient = new Bot(page, homeserver, { + bootstrapCrossSigning: true, + bootstrapSecretStorage: true, + }); + aliceBotClient.setCredentials(credentials); + // Backup is prepared in the background. Poll until it is ready. + const botClientHandle = await aliceBotClient.prepareClient(); + let expectedBackupVersion: string; + await expect + .poll(async () => { + expectedBackupVersion = await botClientHandle.evaluate((cli) => + cli.getCrypto()!.getActiveSessionBackupVersion(), + ); + return expectedBackupVersion; + }) + .not.toBe(null); + + const recoveryKey = await aliceBotClient.getRecoveryKey(); + + return { aliceBotClient, recoveryKey, expectedBackupVersion }; +} + /** * wait for the given client to receive an incoming verification request, and automatically accept it * diff --git a/playwright/e2e/settings/encryption-user-tab/index.ts b/playwright/e2e/settings/encryption-user-tab/index.ts new file mode 100644 index 0000000000..967905eabb --- /dev/null +++ b/playwright/e2e/settings/encryption-user-tab/index.ts @@ -0,0 +1,118 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only + * Please see LICENSE files in the repository root for full details. + */ + +import { Page } from "@playwright/test"; +import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api"; + +import { ElementAppPage } from "../../../pages/ElementAppPage"; +import { test as base, expect } from "../../../element-web-test"; +export { expect }; + +/** + * Set up for the encryption tab test + */ +export const test = base.extend<{ + util: Helpers; +}>({ + util: async ({ page, app, bot }, use) => { + await use(new Helpers(page, app)); + }, +}); + +class Helpers { + constructor( + private page: Page, + private app: ElementAppPage, + ) {} + + /** + * Open the encryption tab + */ + openEncryptionTab() { + return this.app.settings.openUserSettings("Encryption"); + } + + /** + * Go through the device verification flow using the recovery key. + */ + async verifyDevice(recoveryKey: GeneratedSecretStorageKey) { + // Select the security phrase + await this.page.getByRole("button", { name: "Verify with Security Key or Phrase" }).click(); + await this.enterRecoveryKey(recoveryKey); + await this.page.getByRole("button", { name: "Done" }).click(); + } + + /** + * Fill the recovery key in the dialog + * @param recoveryKey + */ + async enterRecoveryKey(recoveryKey: GeneratedSecretStorageKey) { + // Select to use recovery key + await this.page.getByRole("button", { name: "use your Security Key" }).click(); + + // Fill the recovery key + const dialog = this.page.locator(".mx_Dialog"); + await dialog.getByRole("textbox").fill(recoveryKey.encodedPrivateKey); + await dialog.getByRole("button", { name: "Continue" }).click(); + } + + /** + * Get the encryption tab content + */ + getEncryptionTabContent() { + return this.page.getByTestId("encryptionTab"); + } + + /** + * Delete the key backup for the given version + * @param backupVersion + */ + async deleteKeyBackup(backupVersion: string) { + const client = await this.app.client.prepareClient(); + await client.evaluate(async (client, backupVersion) => { + await client.getCrypto()?.deleteKeyBackupVersion(backupVersion); + }, backupVersion); + } + + /** + * Get the security key from the clipboard and fill in the input field + * Then click on the finish button + * @param screenshot + */ + async confirmRecoveryKey(screenshot: `${string}.png`) { + const dialog = this.getEncryptionTabContent(); + await expect(dialog.getByText("Enter your recovery key to confirm")).toBeVisible(); + await expect(dialog).toMatchScreenshot(screenshot); + + const handle = await this.page.evaluateHandle(() => navigator.clipboard.readText()); + const clipboardContent = await handle.jsonValue(); + await dialog.getByRole("textbox").fill(clipboardContent); + await dialog.getByRole("button", { name: "Finish set up" }).click(); + await expect(dialog).toMatchScreenshot("default-recovery.png"); + } + + /** + * Remove the cached secrets from the indexedDB + */ + async deleteCachedSecrets() { + await this.page.evaluate(async () => { + const removeCachedSecrets = new Promise((resolve) => { + const request = window.indexedDB.open("matrix-js-sdk::matrix-sdk-crypto"); + request.onsuccess = async (event: Event & { target: { result: IDBDatabase } }) => { + const db = event.target.result; + const request = db.transaction("core", "readwrite").objectStore("core").delete("private_identity"); + request.onsuccess = () => { + db.close(); + resolve(undefined); + }; + }; + }); + await removeCachedSecrets; + }); + await this.page.reload(); + } +} diff --git a/playwright/e2e/settings/encryption-user-tab/recovery.spec.ts b/playwright/e2e/settings/encryption-user-tab/recovery.spec.ts new file mode 100644 index 0000000000..f46ca1b5e1 --- /dev/null +++ b/playwright/e2e/settings/encryption-user-tab/recovery.spec.ts @@ -0,0 +1,141 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only + * Please see LICENSE files in the repository root for full details. + */ + +// import { test, expect } from "."; +import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api"; + +import { test, expect } from "."; +import { + checkDeviceIsConnectedKeyBackup, + checkDeviceIsCrossSigned, + createBot, + verifySession, +} from "../../crypto/utils"; + +test.describe("Recovery section in Encryption tab", () => { + test.use({ + displayName: "Alice", + }); + + let recoveryKey: GeneratedSecretStorageKey; + let expectedBackupVersion: string; + + test.beforeEach(async ({ page, homeserver, credentials }) => { + const res = await createBot(page, homeserver, credentials); + recoveryKey = res.recoveryKey; + expectedBackupVersion = res.expectedBackupVersion; + }); + + test("should verify the device", { tag: "@screenshot" }, async ({ page, app, util }) => { + const dialog = await util.openEncryptionTab(); + + // The user can only verify the device + const verifyButton = dialog.getByRole("button", { name: "Verify this device" }); + await expect(verifyButton).toBeVisible(); + await expect(util.getEncryptionTabContent()).toMatchScreenshot("verify-device-encryption-tab.png"); + await verifyButton.click(); + + await util.verifyDevice(recoveryKey); + await expect(util.getEncryptionTabContent()).toMatchScreenshot("default-recovery.png"); + + // Check that our device is now cross-signed + await checkDeviceIsCrossSigned(app); + + // Check that the current device is connected to key backup + // The backup decryption key should be in cache also, as we got it directly from the 4S + await app.closeDialog(); + await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true); + }); + + test( + "should change the recovery key", + { tag: "@screenshot" }, + async ({ page, app, homeserver, credentials, util, context }) => { + await verifySession(app, "new passphrase"); + const dialog = await util.openEncryptionTab(); + + // The user can only change the recovery key + const changeButton = dialog.getByRole("button", { name: "Change recovery key" }); + await expect(changeButton).toBeVisible(); + await expect(util.getEncryptionTabContent()).toMatchScreenshot("default-recovery.png"); + await changeButton.click(); + + // Display the new recovery key and click on the copy button + await expect(dialog.getByText("Change recovery key?")).toBeVisible(); + await expect(util.getEncryptionTabContent()).toMatchScreenshot("change-key-1-encryption-tab.png", { + mask: [dialog.getByTestId("recoveryKey")], + }); + await dialog.getByRole("button", { name: "Copy" }).click(); + await dialog.getByRole("button", { name: "Continue" }).click(); + + // Confirm the recovery key + await util.confirmRecoveryKey("change-key-2-encryption-tab.png"); + }, + ); + + test("should setup the recovery key", { tag: "@screenshot" }, async ({ page, app, util }) => { + await verifySession(app, "new passphrase"); + await util.deleteKeyBackup(expectedBackupVersion); + + // The key backup is deleted and the user needs to set up it + const dialog = await util.openEncryptionTab(); + const setupButton = dialog.getByRole("button", { name: "Set up recovery" }); + await expect(setupButton).toBeVisible(); + await expect(util.getEncryptionTabContent()).toMatchScreenshot("set-up-recovery.png"); + await setupButton.click(); + + // Display an informative panel about the recovery key + await expect(dialog.getByRole("heading", { name: "Set up recovery" })).toBeVisible(); + await expect(util.getEncryptionTabContent()).toMatchScreenshot("set-up-key-1-encryption-tab.png"); + await dialog.getByRole("button", { name: "Continue" }).click(); + + // Display the new recovery key and click on the copy button + await expect(dialog.getByText("Save your recovery key somewhere safe")).toBeVisible(); + await expect(util.getEncryptionTabContent()).toMatchScreenshot("set-up-key-2-encryption-tab.png", { + mask: [dialog.getByTestId("recoveryKey")], + }); + await dialog.getByRole("button", { name: "Copy" }).click(); + await dialog.getByRole("button", { name: "Continue" }).click(); + + // Confirm the recovery key + await util.confirmRecoveryKey("set-up-key-3-encryption-tab.png"); + + await app.closeDialog(); + // Check that the current device is connected to key backup and the backup version is the expected one + await checkDeviceIsConnectedKeyBackup(page, "2", true); + }); + + test( + "should enter the recovery key when the secrets are not cached", + { tag: "@screenshot" }, + async ({ page, app, util }) => { + await verifySession(app, "new passphrase"); + // We need to delete the cached secrets + await util.deleteCachedSecrets(); + + await util.openEncryptionTab(); + // We ask the user to enter the recovery key + const dialog = util.getEncryptionTabContent(); + const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" }); + await expect(enterKeyButton).toBeVisible(); + await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png"); + await enterKeyButton.click(); + + // Fill the recovery key + await util.enterRecoveryKey(recoveryKey); + await expect(dialog).toMatchScreenshot("default-recovery.png"); + + // Check that our device is now cross-signed + await checkDeviceIsCrossSigned(app); + + // Check that the current device is connected to key backup + // The backup decryption key should be in cache also, as we got it directly from the 4S + await app.closeDialog(); + await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true); + }, + ); +}); diff --git a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-1-encryption-tab-linux.png b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-1-encryption-tab-linux.png new file mode 100644 index 0000000000..eead4451ce Binary files /dev/null and b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-1-encryption-tab-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-2-encryption-tab-linux.png b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-2-encryption-tab-linux.png new file mode 100644 index 0000000000..1ff8aa8d0a Binary files /dev/null and b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-2-encryption-tab-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/default-recovery-linux.png b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/default-recovery-linux.png new file mode 100644 index 0000000000..971745c412 Binary files /dev/null and b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/default-recovery-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/out-of-sync-recovery-linux.png b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/out-of-sync-recovery-linux.png new file mode 100644 index 0000000000..e6664a5f79 Binary files /dev/null and b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/out-of-sync-recovery-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-1-encryption-tab-linux.png b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-1-encryption-tab-linux.png new file mode 100644 index 0000000000..1a413094ae Binary files /dev/null and b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-1-encryption-tab-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-2-encryption-tab-linux.png b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-2-encryption-tab-linux.png new file mode 100644 index 0000000000..099c0c549e Binary files /dev/null and b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-2-encryption-tab-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-3-encryption-tab-linux.png b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-3-encryption-tab-linux.png new file mode 100644 index 0000000000..6cc32cc431 Binary files /dev/null and b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-3-encryption-tab-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-recovery-linux.png b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-recovery-linux.png new file mode 100644 index 0000000000..78dcd14aea Binary files /dev/null and b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-recovery-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/verify-device-encryption-tab-linux.png b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/verify-device-encryption-tab-linux.png new file mode 100644 index 0000000000..643fe46a1d Binary files /dev/null and b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/verify-device-encryption-tab-linux.png differ