Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit df11b90

Browse files
authored
Set up key backup using non-deprecated APIs (#12005)
1 parent 1ce569b commit df11b90

File tree

9 files changed

+198
-34
lines changed

9 files changed

+198
-34
lines changed

playwright/e2e/crypto/backups.spec.ts

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { test, expect } from "../../element-web-test";
18+
19+
test.describe("Backups", () => {
20+
test.use({
21+
displayName: "Hanako",
22+
});
23+
24+
test("Create, delete and recreate a keys backup", async ({ page, user, app }, workerInfo) => {
25+
// skipIfLegacyCrypto
26+
test.skip(
27+
workerInfo.project.name === "Legacy Crypto",
28+
"This test only works with Rust crypto. Deleting the backup seems to fail with legacy crypto.",
29+
);
30+
31+
// Create a backup
32+
const tab = await app.settings.openUserSettings("Security & Privacy");
33+
await expect(tab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
34+
await tab.getByRole("button", { name: "Set up", exact: true }).click();
35+
const dialog = await app.getDialogByTitle("Set up Secure Backup", 60000);
36+
await dialog.getByRole("button", { name: "Continue", exact: true }).click();
37+
await expect(dialog.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
38+
await dialog.getByRole("button", { name: "Copy", exact: true }).click();
39+
const securityKey = await app.getClipboard();
40+
await dialog.getByRole("button", { name: "Continue", exact: true }).click();
41+
await expect(dialog.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
42+
await dialog.getByRole("button", { name: "Done", exact: true }).click();
43+
44+
// Delete it
45+
await app.settings.openUserSettings("Security & Privacy");
46+
await expect(tab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
47+
await tab.getByRole("button", { name: "Delete Backup", exact: true }).click();
48+
await dialog.getByTestId("dialog-primary-button").click(); // Click "Delete Backup"
49+
50+
// Create another
51+
await tab.getByRole("button", { name: "Set up", exact: true }).click();
52+
dialog.getByLabel("Security Key").fill(securityKey);
53+
await dialog.getByRole("button", { name: "Continue", exact: true }).click();
54+
await expect(dialog.getByRole("heading", { name: "Success!" })).toBeVisible();
55+
await dialog.getByRole("button", { name: "OK", exact: true }).click();
56+
});
57+
});

playwright/element-web-test.ts

+4
Original file line numberDiff line numberDiff line change
@@ -238,3 +238,7 @@ export const expect = baseExpect.extend({
238238
return { pass: true, message: () => "", name: "toMatchScreenshot" };
239239
},
240240
});
241+
242+
test.use({
243+
permissions: ["clipboard-read"],
244+
});

playwright/pages/ElementAppPage.ts

+13
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,19 @@ export class ElementAppPage {
5050
return this.settings.closeDialog();
5151
}
5252

53+
public async getClipboard(): Promise<string> {
54+
return await this.page.evaluate(() => navigator.clipboard.readText());
55+
}
56+
57+
/**
58+
* Find an open dialog by its title
59+
*/
60+
public async getDialogByTitle(title: string, timeout = 5000): Promise<Locator> {
61+
const dialog = this.page.locator(".mx_Dialog");
62+
await dialog.getByRole("heading", { name: title }).waitFor({ timeout });
63+
return dialog;
64+
}
65+
5366
/**
5467
* Opens the given room by name. The room must be visible in the
5568
* room list, but the room list may be folded horizontally, and the

src/SecurityManager.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -319,8 +319,13 @@ export async function promptForBackupPassphrase(): Promise<Uint8Array> {
319319
* @param {Function} [func] An operation to perform once secret storage has been
320320
* bootstrapped. Optional.
321321
* @param {bool} [forceReset] Reset secret storage even if it's already set up
322+
* @param {bool} [setupNewKeyBackup] Reset secret storage even if it's already set up
322323
*/
323-
export async function accessSecretStorage(func = async (): Promise<void> => {}, forceReset = false): Promise<void> {
324+
export async function accessSecretStorage(
325+
func = async (): Promise<void> => {},
326+
forceReset = false,
327+
setupNewKeyBackup = true,
328+
): Promise<void> {
324329
secretStorageBeingAccessed = true;
325330
try {
326331
const cli = MatrixClientPeg.safeGet();
@@ -352,7 +357,12 @@ export async function accessSecretStorage(func = async (): Promise<void> => {},
352357
throw new Error("Secret storage creation canceled");
353358
}
354359
} else {
355-
await cli.bootstrapCrossSigning({
360+
const crypto = cli.getCrypto();
361+
if (!crypto) {
362+
throw new Error("End-to-end encryption is disabled - unable to access secret storage.");
363+
}
364+
365+
await crypto.bootstrapCrossSigning({
356366
authUploadDeviceSigningKeys: async (makeRequest): Promise<void> => {
357367
const { finished } = Modal.createDialog(InteractiveAuthDialog, {
358368
title: _t("encryption|bootstrap_title"),
@@ -365,8 +375,9 @@ export async function accessSecretStorage(func = async (): Promise<void> => {},
365375
}
366376
},
367377
});
368-
await cli.bootstrapSecretStorage({
378+
await crypto.bootstrapSecretStorage({
369379
getKeyBackupPassphrase: promptForBackupPassphrase,
380+
setupNewKeyBackup,
370381
});
371382

372383
const keyId = Object.keys(secretStorageKeys)[0];

src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx

+17-20
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ limitations under the License.
1717

1818
import React from "react";
1919
import { logger } from "matrix-js-sdk/src/logger";
20-
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
2120

2221
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
2322
import { _t } from "../../../../languageHandler";
@@ -75,24 +74,25 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
7574
this.setState({
7675
error: undefined,
7776
});
78-
let info: IKeyBackupInfo | undefined;
7977
const cli = MatrixClientPeg.safeGet();
8078
try {
81-
await accessSecretStorage(async (): Promise<void> => {
82-
// `accessSecretStorage` will have bootstrapped secret storage if necessary, so we can now
83-
// set up key backup.
84-
//
85-
// XXX: `bootstrapSecretStorage` also sets up key backup as a side effect, so there is a 90% chance
86-
// this is actually redundant.
87-
//
88-
// The only time it would *not* be redundant would be if, for some reason, we had working 4S but no
89-
// working key backup. (For example, if the user clicked "Delete Backup".)
90-
info = await cli.prepareKeyBackupVersion(null /* random key */, {
91-
secureSecretStorage: true,
92-
});
93-
info = await cli.createKeyBackupVersion(info);
94-
});
95-
await cli.scheduleAllGroupSessionsForBackup();
79+
// We don't want accessSecretStorage to create a backup for us - we
80+
// will create one ourselves in the closure we pass in by calling
81+
// resetKeyBackup.
82+
const setupNewKeyBackup = false;
83+
const forceReset = false;
84+
85+
await accessSecretStorage(
86+
async (): Promise<void> => {
87+
const crypto = cli.getCrypto();
88+
if (!crypto) {
89+
throw new Error("End-to-end encryption is disabled - unable to create backup.");
90+
}
91+
await crypto.resetKeyBackup();
92+
},
93+
forceReset,
94+
setupNewKeyBackup,
95+
);
9696
this.setState({
9797
phase: Phase.Done,
9898
});
@@ -102,9 +102,6 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
102102
// delete the version, disable backup, or do nothing? If we just
103103
// disable without deleting, we'll enable on next app reload since
104104
// it is trusted.
105-
if (info?.version) {
106-
cli.deleteKeyBackupVersion(info.version);
107-
}
108105
this.setState({
109106
error: true,
110107
});

test/SecurityManager-test.ts

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { mocked } from "jest-mock";
18+
import { CryptoApi } from "matrix-js-sdk/src/crypto-api";
19+
20+
import { accessSecretStorage } from "../src/SecurityManager";
21+
import { filterConsole, stubClient } from "./test-utils";
22+
23+
describe("SecurityManager", () => {
24+
describe("accessSecretStorage", () => {
25+
filterConsole("Not setting dehydration key: no SSSS key found");
26+
27+
it("runs the function passed in", async () => {
28+
// Given a client
29+
const crypto = {
30+
bootstrapCrossSigning: () => {},
31+
bootstrapSecretStorage: () => {},
32+
} as unknown as CryptoApi;
33+
const client = stubClient();
34+
mocked(client.hasSecretStorageKey).mockResolvedValue(true);
35+
mocked(client.getCrypto).mockReturnValue(crypto);
36+
37+
// When I run accessSecretStorage
38+
const func = jest.fn();
39+
await accessSecretStorage(func);
40+
41+
// Then we call the passed-in function
42+
expect(func).toHaveBeenCalledTimes(1);
43+
});
44+
45+
describe("expecting errors", () => {
46+
filterConsole("End-to-end encryption is disabled - unable to access secret storage");
47+
48+
it("throws if crypto is unavailable", async () => {
49+
// Given a client with no crypto
50+
const client = stubClient();
51+
mocked(client.hasSecretStorageKey).mockResolvedValue(true);
52+
mocked(client.getCrypto).mockReturnValue(undefined);
53+
54+
// When I run accessSecretStorage
55+
// Then we throw an error
56+
await expect(async () => {
57+
await accessSecretStorage(jest.fn());
58+
}).rejects.toThrow("End-to-end encryption is disabled - unable to access secret storage");
59+
});
60+
});
61+
});
62+
});

test/components/views/dialogs/security/CreateKeyBackupDialog-test.tsx

+29-10
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ import React from "react";
1919
import { mocked } from "jest-mock";
2020

2121
import CreateKeyBackupDialog from "../../../../../src/async-components/views/dialogs/security/CreateKeyBackupDialog";
22-
import { createTestClient } from "../../../../test-utils";
22+
import { createTestClient, filterConsole } from "../../../../test-utils";
2323
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
2424

2525
jest.mock("../../../../../src/SecurityManager", () => ({
26-
accessSecretStorage: jest.fn().mockResolvedValue(undefined),
26+
accessSecretStorage: async (func = async () => Promise<void>) => {
27+
await func();
28+
},
2729
}));
2830

2931
describe("CreateKeyBackupDialog", () => {
@@ -39,16 +41,33 @@ describe("CreateKeyBackupDialog", () => {
3941
expect(asFragment()).toMatchSnapshot();
4042
});
4143

42-
it("should display the error message when backup creation failed", async () => {
43-
const matrixClient = createTestClient();
44-
mocked(matrixClient.scheduleAllGroupSessionsForBackup).mockRejectedValue("my error");
45-
MatrixClientPeg.safeGet = MatrixClientPeg.get = () => matrixClient;
44+
describe("expecting failure", () => {
45+
filterConsole("Error creating key backup");
4646

47-
const { asFragment } = render(<CreateKeyBackupDialog onFinished={jest.fn()} />);
47+
it("should display an error message when backup creation failed", async () => {
48+
const matrixClient = createTestClient();
49+
mocked(matrixClient.getCrypto()!.resetKeyBackup).mockImplementation(() => {
50+
throw new Error("failed");
51+
});
52+
MatrixClientPeg.safeGet = MatrixClientPeg.get = () => matrixClient;
4853

49-
// Check if the error message is displayed
50-
await waitFor(() => expect(screen.getByText("Unable to create key backup")).toBeDefined());
51-
expect(asFragment()).toMatchSnapshot();
54+
const { asFragment } = render(<CreateKeyBackupDialog onFinished={jest.fn()} />);
55+
56+
// Check if the error message is displayed
57+
await waitFor(() => expect(screen.getByText("Unable to create key backup")).toBeDefined());
58+
expect(asFragment()).toMatchSnapshot();
59+
});
60+
61+
it("should display an error message when there is no Crypto available", async () => {
62+
const matrixClient = createTestClient();
63+
mocked(matrixClient.getCrypto).mockReturnValue(undefined);
64+
MatrixClientPeg.safeGet = MatrixClientPeg.get = () => matrixClient;
65+
66+
render(<CreateKeyBackupDialog onFinished={jest.fn()} />);
67+
68+
// Check if the error message is displayed
69+
await waitFor(() => expect(screen.getByText("Unable to create key backup")).toBeDefined());
70+
});
5271
});
5372

5473
it("should display the success dialog when the key backup is finished", async () => {

test/components/views/dialogs/security/__snapshots__/CreateKeyBackupDialog-test.tsx.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`CreateKeyBackupDialog should display the error message when backup creation failed 1`] = `
3+
exports[`CreateKeyBackupDialog expecting failure should display an error message when backup creation failed 1`] = `
44
<DocumentFragment>
55
<div
66
data-focus-guard="true"

test/test-utils/test-utils.ts

+1
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ export function createTestClient(): MatrixClient {
132132
getUserDeviceInfo: jest.fn(),
133133
getUserVerificationStatus: jest.fn(),
134134
getDeviceVerificationStatus: jest.fn(),
135+
resetKeyBackup: jest.fn(),
135136
}),
136137

137138
getPushActionsForEvent: jest.fn(),

0 commit comments

Comments
 (0)