Set up key backup using non-deprecated APIs (#12005)
parent
1ce569bfc2
commit
df11b90fd6
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -238,3 +238,7 @@ export const expect = baseExpect.extend({
|
|||
return { pass: true, message: () => "", name: "toMatchScreenshot" };
|
||||
},
|
||||
});
|
||||
|
||||
test.use({
|
||||
permissions: ["clipboard-read"],
|
||||
});
|
||||
|
|
|
@ -50,6 +50,19 @@ export class ElementAppPage {
|
|||
return this.settings.closeDialog();
|
||||
}
|
||||
|
||||
public async getClipboard(): Promise<string> {
|
||||
return await this.page.evaluate(() => navigator.clipboard.readText());
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an open dialog by its title
|
||||
*/
|
||||
public async getDialogByTitle(title: string, timeout = 5000): Promise<Locator> {
|
||||
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
|
||||
|
|
|
@ -319,8 +319,13 @@ export async function promptForBackupPassphrase(): Promise<Uint8Array> {
|
|||
* @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<void> => {}, forceReset = false): Promise<void> {
|
||||
export async function accessSecretStorage(
|
||||
func = async (): Promise<void> => {},
|
||||
forceReset = false,
|
||||
setupNewKeyBackup = true,
|
||||
): Promise<void> {
|
||||
secretStorageBeingAccessed = true;
|
||||
try {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
|
@ -352,7 +357,12 @@ export async function accessSecretStorage(func = async (): Promise<void> => {},
|
|||
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<void> => {
|
||||
const { finished } = Modal.createDialog(InteractiveAuthDialog, {
|
||||
title: _t("encryption|bootstrap_title"),
|
||||
|
@ -365,8 +375,9 @@ export async function accessSecretStorage(func = async (): Promise<void> => {},
|
|||
}
|
||||
},
|
||||
});
|
||||
await cli.bootstrapSecretStorage({
|
||||
await crypto.bootstrapSecretStorage({
|
||||
getKeyBackupPassphrase: promptForBackupPassphrase,
|
||||
setupNewKeyBackup,
|
||||
});
|
||||
|
||||
const keyId = Object.keys(secretStorageKeys)[0];
|
||||
|
|
|
@ -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<IProps, I
|
|||
this.setState({
|
||||
error: undefined,
|
||||
});
|
||||
let info: IKeyBackupInfo | undefined;
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
try {
|
||||
await accessSecretStorage(async (): Promise<void> => {
|
||||
// `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<void> => {
|
||||
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<IProps, I
|
|||
// 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.
|
||||
if (info?.version) {
|
||||
cli.deleteKeyBackupVersion(info.version);
|
||||
}
|
||||
this.setState({
|
||||
error: true,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
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 { mocked } from "jest-mock";
|
||||
import { CryptoApi } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { accessSecretStorage } from "../src/SecurityManager";
|
||||
import { filterConsole, stubClient } from "./test-utils";
|
||||
|
||||
describe("SecurityManager", () => {
|
||||
describe("accessSecretStorage", () => {
|
||||
filterConsole("Not setting dehydration key: no SSSS key found");
|
||||
|
||||
it("runs the function passed in", async () => {
|
||||
// Given a client
|
||||
const crypto = {
|
||||
bootstrapCrossSigning: () => {},
|
||||
bootstrapSecretStorage: () => {},
|
||||
} as unknown as CryptoApi;
|
||||
const client = stubClient();
|
||||
mocked(client.hasSecretStorageKey).mockResolvedValue(true);
|
||||
mocked(client.getCrypto).mockReturnValue(crypto);
|
||||
|
||||
// When I run accessSecretStorage
|
||||
const func = jest.fn();
|
||||
await accessSecretStorage(func);
|
||||
|
||||
// Then we call the passed-in function
|
||||
expect(func).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe("expecting errors", () => {
|
||||
filterConsole("End-to-end encryption is disabled - unable to access secret storage");
|
||||
|
||||
it("throws if crypto is unavailable", async () => {
|
||||
// Given a client with no crypto
|
||||
const client = stubClient();
|
||||
mocked(client.hasSecretStorageKey).mockResolvedValue(true);
|
||||
mocked(client.getCrypto).mockReturnValue(undefined);
|
||||
|
||||
// When I run accessSecretStorage
|
||||
// Then we throw an error
|
||||
await expect(async () => {
|
||||
await accessSecretStorage(jest.fn());
|
||||
}).rejects.toThrow("End-to-end encryption is disabled - unable to access secret storage");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -19,11 +19,13 @@ 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 { createTestClient, filterConsole } from "../../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
|
||||
jest.mock("../../../../../src/SecurityManager", () => ({
|
||||
accessSecretStorage: jest.fn().mockResolvedValue(undefined),
|
||||
accessSecretStorage: async (func = async () => Promise<void>) => {
|
||||
await func();
|
||||
},
|
||||
}));
|
||||
|
||||
describe("CreateKeyBackupDialog", () => {
|
||||
|
@ -39,16 +41,33 @@ describe("CreateKeyBackupDialog", () => {
|
|||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should display the error message when backup creation failed", async () => {
|
||||
const matrixClient = createTestClient();
|
||||
mocked(matrixClient.scheduleAllGroupSessionsForBackup).mockRejectedValue("my error");
|
||||
MatrixClientPeg.safeGet = MatrixClientPeg.get = () => matrixClient;
|
||||
describe("expecting failure", () => {
|
||||
filterConsole("Error creating key backup");
|
||||
|
||||
const { asFragment } = render(<CreateKeyBackupDialog onFinished={jest.fn()} />);
|
||||
it("should display an error message when backup creation failed", async () => {
|
||||
const matrixClient = createTestClient();
|
||||
mocked(matrixClient.getCrypto()!.resetKeyBackup).mockImplementation(() => {
|
||||
throw new Error("failed");
|
||||
});
|
||||
MatrixClientPeg.safeGet = MatrixClientPeg.get = () => matrixClient;
|
||||
|
||||
// Check if the error message is displayed
|
||||
await waitFor(() => expect(screen.getByText("Unable to create key backup")).toBeDefined());
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
const { asFragment } = render(<CreateKeyBackupDialog onFinished={jest.fn()} />);
|
||||
|
||||
// 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();
|
||||
mocked(matrixClient.getCrypto).mockReturnValue(undefined);
|
||||
MatrixClientPeg.safeGet = MatrixClientPeg.get = () => matrixClient;
|
||||
|
||||
render(<CreateKeyBackupDialog onFinished={jest.fn()} />);
|
||||
|
||||
// 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 () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CreateKeyBackupDialog should display the error message when backup creation failed 1`] = `
|
||||
exports[`CreateKeyBackupDialog expecting failure should display an error message when backup creation failed 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
|
|
|
@ -132,6 +132,7 @@ export function createTestClient(): MatrixClient {
|
|||
getUserDeviceInfo: jest.fn(),
|
||||
getUserVerificationStatus: jest.fn(),
|
||||
getDeviceVerificationStatus: jest.fn(),
|
||||
resetKeyBackup: jest.fn(),
|
||||
}),
|
||||
|
||||
getPushActionsForEvent: jest.fn(),
|
||||
|
|
Loading…
Reference in New Issue