diff --git a/playwright/e2e/crypto/backups.spec.ts b/playwright/e2e/crypto/backups.spec.ts new file mode 100644 index 000000000000..847784bff221 --- /dev/null +++ b/playwright/e2e/crypto/backups.spec.ts @@ -0,0 +1,57 @@ +/* +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 { test, expect } from "../../element-web-test"; + +test.describe("Backups", () => { + test.use({ + displayName: "Hanako", + }); + + test("Create, delete and recreate a keys backup", async ({ page, user, app }, workerInfo) => { + // skipIfLegacyCrypto + test.skip( + workerInfo.project.name === "Legacy Crypto", + "This test only works with Rust crypto. Deleting the backup seems to fail with legacy crypto.", + ); + + // Create a backup + const tab = await app.settings.openUserSettings("Security & Privacy"); + await expect(tab.getByRole("heading", { name: "Secure Backup" })).toBeVisible(); + await tab.getByRole("button", { name: "Set up", exact: true }).click(); + const dialog = await app.getDialogByTitle("Set up Secure Backup", 60000); + await dialog.getByRole("button", { name: "Continue", exact: true }).click(); + await expect(dialog.getByRole("heading", { name: "Save your Security Key" })).toBeVisible(); + await dialog.getByRole("button", { name: "Copy", exact: true }).click(); + const securityKey = await app.getClipboard(); + await dialog.getByRole("button", { name: "Continue", exact: true }).click(); + await expect(dialog.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible(); + await dialog.getByRole("button", { name: "Done", exact: true }).click(); + + // Delete it + await app.settings.openUserSettings("Security & Privacy"); + await expect(tab.getByRole("heading", { name: "Secure Backup" })).toBeVisible(); + await tab.getByRole("button", { name: "Delete Backup", exact: true }).click(); + await dialog.getByTestId("dialog-primary-button").click(); // Click "Delete Backup" + + // Create another + await tab.getByRole("button", { name: "Set up", exact: true }).click(); + dialog.getByLabel("Security Key").fill(securityKey); + await dialog.getByRole("button", { name: "Continue", exact: true }).click(); + await expect(dialog.getByRole("heading", { name: "Success!" })).toBeVisible(); + await dialog.getByRole("button", { name: "OK", exact: true }).click(); + }); +}); diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index a551ec593d16..e2e1729a4bc4 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -238,3 +238,7 @@ export const expect = baseExpect.extend({ return { pass: true, message: () => "", name: "toMatchScreenshot" }; }, }); + +test.use({ + permissions: ["clipboard-read"], +}); diff --git a/playwright/pages/ElementAppPage.ts b/playwright/pages/ElementAppPage.ts index 0d8a5213fccb..8d5b43f1d82f 100644 --- a/playwright/pages/ElementAppPage.ts +++ b/playwright/pages/ElementAppPage.ts @@ -50,6 +50,19 @@ export class ElementAppPage { return this.settings.closeDialog(); } + public async getClipboard(): Promise { + return await this.page.evaluate(() => navigator.clipboard.readText()); + } + + /** + * Find an open dialog by its title + */ + public async getDialogByTitle(title: string, timeout = 5000): Promise { + const dialog = this.page.locator(".mx_Dialog"); + await dialog.getByRole("heading", { name: title }).waitFor({ timeout }); + return dialog; + } + /** * Opens the given room by name. The room must be visible in the * room list, but the room list may be folded horizontally, and the diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 10848e7afb9a..056be7a2a922 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -319,8 +319,13 @@ export async function promptForBackupPassphrase(): Promise { * @param {Function} [func] An operation to perform once secret storage has been * bootstrapped. Optional. * @param {bool} [forceReset] Reset secret storage even if it's already set up + * @param {bool} [setupNewKeyBackup] Reset secret storage even if it's already set up */ -export async function accessSecretStorage(func = async (): Promise => {}, forceReset = false): Promise { +export async function accessSecretStorage( + func = async (): Promise => {}, + forceReset = false, + setupNewKeyBackup = true, +): Promise { secretStorageBeingAccessed = true; try { const cli = MatrixClientPeg.safeGet(); @@ -352,7 +357,12 @@ export async function accessSecretStorage(func = async (): Promise => {}, throw new Error("Secret storage creation canceled"); } } else { - await cli.bootstrapCrossSigning({ + const crypto = cli.getCrypto(); + if (!crypto) { + throw new Error("End-to-end encryption is disabled - unable to access secret storage."); + } + + await crypto.bootstrapCrossSigning({ authUploadDeviceSigningKeys: async (makeRequest): Promise => { const { finished } = Modal.createDialog(InteractiveAuthDialog, { title: _t("encryption|bootstrap_title"), @@ -365,8 +375,9 @@ export async function accessSecretStorage(func = async (): Promise => {}, } }, }); - await cli.bootstrapSecretStorage({ + await crypto.bootstrapSecretStorage({ getKeyBackupPassphrase: promptForBackupPassphrase, + setupNewKeyBackup, }); const keyId = Object.keys(secretStorageKeys)[0]; diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx index 453a9dc84c41..5a7e317dd288 100644 --- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx @@ -17,7 +17,6 @@ limitations under the License. import React from "react"; import { logger } from "matrix-js-sdk/src/logger"; -import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; import { MatrixClientPeg } from "../../../../MatrixClientPeg"; import { _t } from "../../../../languageHandler"; @@ -75,24 +74,25 @@ export default class CreateKeyBackupDialog extends React.PureComponent => { - // `accessSecretStorage` will have bootstrapped secret storage if necessary, so we can now - // set up key backup. - // - // XXX: `bootstrapSecretStorage` also sets up key backup as a side effect, so there is a 90% chance - // this is actually redundant. - // - // The only time it would *not* be redundant would be if, for some reason, we had working 4S but no - // working key backup. (For example, if the user clicked "Delete Backup".) - info = await cli.prepareKeyBackupVersion(null /* random key */, { - secureSecretStorage: true, - }); - info = await cli.createKeyBackupVersion(info); - }); - await cli.scheduleAllGroupSessionsForBackup(); + // We don't want accessSecretStorage to create a backup for us - we + // will create one ourselves in the closure we pass in by calling + // resetKeyBackup. + const setupNewKeyBackup = false; + const forceReset = false; + + await accessSecretStorage( + async (): Promise => { + const crypto = cli.getCrypto(); + if (!crypto) { + throw new Error("End-to-end encryption is disabled - unable to create backup."); + } + await crypto.resetKeyBackup(); + }, + forceReset, + setupNewKeyBackup, + ); this.setState({ phase: Phase.Done, }); @@ -102,9 +102,6 @@ export default class CreateKeyBackupDialog extends React.PureComponent