Fix a fresh login creating a new key backup (#12106)

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: Valere <valeref@matrix.org>
fix repeated requests to enter 4S key during cross-signing reset (#12059)
This commit is contained in:
Michael Telatynski 2024-01-04 13:58:05 +00:00 committed by GitHub
parent 951c0d8483
commit 1a469f41cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 302 additions and 348 deletions

View file

@ -1,57 +0,0 @@
/*
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();
});
});

View file

@ -212,6 +212,43 @@ test.describe("Cryptography", function () {
}); });
} }
test("Can reset cross-signing keys", async ({ page, app, user: aliceCredentials }) => {
const secretStorageKey = await enableKeyBackup(app);
// Fetch the current cross-signing keys
async function fetchMasterKey() {
return await test.step("Fetch master key from server", async () => {
const k = await app.client.evaluate(async (cli) => {
const userId = cli.getUserId();
const keys = await cli.downloadKeysForUsers([userId]);
return Object.values(keys.master_keys[userId].keys)[0];
});
console.log(`fetchMasterKey: ${k}`);
return k;
});
}
const masterKey1 = await fetchMasterKey();
// Find the "reset cross signing" button, and click it
await app.settings.openUserSettings("Security & Privacy");
await page.locator("div.mx_CrossSigningPanel_buttonRow").getByRole("button", { name: "Reset" }).click();
// Confirm
await page.getByRole("button", { name: "Clear cross-signing keys" }).click();
// Enter the 4S key
await page.getByPlaceholder("Security Key").fill(secretStorageKey);
await page.getByRole("button", { name: "Continue" }).click();
await expect(async () => {
const masterKey2 = await fetchMasterKey();
expect(masterKey1).not.toEqual(masterKey2);
}).toPass();
// The dialog should have gone away
await expect(page.locator(".mx_Dialog")).toHaveCount(1);
});
test("creating a DM should work, being e2e-encrypted / user verification", async ({ test("creating a DM should work, being e2e-encrypted / user verification", async ({
page, page,
app, app,

View file

@ -103,12 +103,35 @@ export async function checkDeviceIsCrossSigned(app: ElementAppPage): Promise<voi
} }
/** /**
* Check that the current device is connected to the key backup. * Check that the current device is connected to the expected key backup.
* Also checks that the decryption key is known and cached locally.
*
* @param page - the page to check
* @param expectedBackupVersion - the version of the backup we expect to be connected to.
* @param checkBackupKeyInCache - whether to check that the backup key is cached locally.
*/ */
export async function checkDeviceIsConnectedKeyBackup(page: Page) { export async function checkDeviceIsConnectedKeyBackup(
page: Page,
expectedBackupVersion: string,
checkBackupKeyInCache: boolean,
): Promise<void> {
await page.getByRole("button", { name: "User menu" }).click(); await page.getByRole("button", { name: "User menu" }).click();
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Security & Privacy" }).click(); await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Security & Privacy" }).click();
await expect(page.locator(".mx_Dialog").getByRole("button", { name: "Restore from Backup" })).toBeVisible(); await expect(page.locator(".mx_Dialog").getByRole("button", { name: "Restore from Backup" })).toBeVisible();
// expand the advanced section to see the active version in the reports
await page.locator(".mx_SecureBackupPanel_advanced").locator("..").click();
if (checkBackupKeyInCache) {
const cacheDecryptionKeyStatusElement = page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(2) td");
await expect(cacheDecryptionKeyStatusElement).toHaveText("cached locally, well formed");
}
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText(
expectedBackupVersion + " (Algorithm: m.megolm_backup.v1.curve25519-aes-sha2)",
);
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(expectedBackupVersion);
} }
/** /**

View file

@ -33,6 +33,9 @@ import { Bot } from "../../pages/bot";
test.describe("Device verification", () => { test.describe("Device verification", () => {
let aliceBotClient: Bot; let aliceBotClient: Bot;
/** The backup version that was set up by the bot client. */
let expectedBackupVersion: string;
test.beforeEach(async ({ page, homeserver, credentials }) => { test.beforeEach(async ({ page, homeserver, credentials }) => {
// Visit the login page of the app, to load the matrix sdk // Visit the login page of the app, to load the matrix sdk
await page.goto("/#/login"); await page.goto("/#/login");
@ -49,9 +52,13 @@ test.describe("Device verification", () => {
bootstrapSecretStorage: true, bootstrapSecretStorage: true,
}); });
aliceBotClient.setCredentials(credentials); aliceBotClient.setCredentials(credentials);
await aliceBotClient.prepareClient(); const mxClientHandle = await aliceBotClient.prepareClient();
await page.waitForTimeout(20000); await page.waitForTimeout(20000);
expectedBackupVersion = await mxClientHandle.evaluate(async (mxClient) => {
return await mxClient.getCrypto()!.getActiveSessionBackupVersion();
});
}); });
// Click the "Verify with another device" button, and have the bot client auto-accept it. // Click the "Verify with another device" button, and have the bot client auto-accept it.
@ -87,7 +94,9 @@ test.describe("Device verification", () => {
await checkDeviceIsCrossSigned(app); await checkDeviceIsCrossSigned(app);
// Check that the current device is connected to key backup // Check that the current device is connected to key backup
await checkDeviceIsConnectedKeyBackup(page); // For now we don't check that the backup key is in cache because it's a bit flaky,
// as we need to wait for the secret gossiping to happen and the settings dialog doesn't refresh automatically.
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, false);
}); });
test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => { test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => {
@ -130,7 +139,9 @@ test.describe("Device verification", () => {
await checkDeviceIsCrossSigned(app); await checkDeviceIsCrossSigned(app);
// Check that the current device is connected to key backup // Check that the current device is connected to key backup
await checkDeviceIsConnectedKeyBackup(page); // For now we don't check that the backup key is in cache because it's a bit flaky,
// as we need to wait for the secret gossiping to happen and the settings dialog doesn't refresh automatically.
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, false);
}); });
test("Verify device with Security Phrase during login", async ({ page, app, credentials, homeserver }) => { test("Verify device with Security Phrase during login", async ({ page, app, credentials, homeserver }) => {
@ -150,7 +161,8 @@ test.describe("Device verification", () => {
await checkDeviceIsCrossSigned(app); await checkDeviceIsCrossSigned(app);
// Check that the current device is connected to key backup // Check that the current device is connected to key backup
await checkDeviceIsConnectedKeyBackup(page); // The backup decryption key should be in cache also, as we got it directly from the 4S
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
}); });
test("Verify device with Security Key during login", async ({ page, app, credentials, homeserver }) => { test("Verify device with Security Key during login", async ({ page, app, credentials, homeserver }) => {
@ -172,7 +184,8 @@ test.describe("Device verification", () => {
await checkDeviceIsCrossSigned(app); await checkDeviceIsCrossSigned(app);
// Check that the current device is connected to key backup // Check that the current device is connected to key backup
await checkDeviceIsConnectedKeyBackup(page); // The backup decryption key should be in cache also, as we got it directly from the 4S
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
}); });
test("Handle incoming verification request with SAS", async ({ page, credentials, homeserver, toasts }) => { test("Handle incoming verification request with SAS", async ({ page, credentials, homeserver, toasts }) => {

View file

@ -260,7 +260,3 @@ export const expect = baseExpect.extend({
return { pass: true, message: () => "", name: "toMatchScreenshot" }; return { pass: true, message: () => "", name: "toMatchScreenshot" };
}, },
}); });
test.use({
permissions: ["clipboard-read"],
});

View file

@ -51,19 +51,6 @@ export class ElementAppPage {
return this.settings.closeDialog(); 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 * 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 * room list, but the room list may be folded horizontally, and the

View file

@ -299,6 +299,28 @@ export async function promptForBackupPassphrase(): Promise<Uint8Array> {
return key; return key;
} }
/**
* Carry out an operation that may require multiple accesses to secret storage, caching the key.
*
* Use this helper to wrap an operation that may require multiple accesses to secret storage; the user will be prompted
* to enter the 4S key or passphrase on the first access, and the key will be cached for the rest of the operation.
*
* @param func - The operation to be wrapped.
*/
export async function withSecretStorageKeyCache<T>(func: () => Promise<T>): Promise<T> {
secretStorageBeingAccessed = true;
try {
return await func();
} finally {
// Clear secret storage key cache now that work is complete
secretStorageBeingAccessed = false;
if (!isCachingAllowed()) {
secretStorageKeys = {};
secretStorageKeyInfo = {};
}
}
}
/** /**
* This helper should be used whenever you need to access secret storage. It * This helper should be used whenever you need to access secret storage. It
* ensures that secret storage (and also cross-signing since they each depend on * ensures that secret storage (and also cross-signing since they each depend on
@ -319,14 +341,13 @@ export async function promptForBackupPassphrase(): Promise<Uint8Array> {
* @param {Function} [func] An operation to perform once secret storage has been * @param {Function} [func] An operation to perform once secret storage has been
* bootstrapped. Optional. * bootstrapped. Optional.
* @param {bool} [forceReset] Reset secret storage even if it's already set up * @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( export async function accessSecretStorage(func = async (): Promise<void> => {}, forceReset = false): Promise<void> {
func = async (): Promise<void> => {}, await withSecretStorageKeyCache(() => doAccessSecretStorage(func, forceReset));
forceReset = false, }
setupNewKeyBackup = true,
): Promise<void> { /** Helper for {@link #accessSecretStorage} */
secretStorageBeingAccessed = true; export async function doAccessSecretStorage(func = async (): Promise<void> => {}, forceReset = false): Promise<void> {
try { try {
const cli = MatrixClientPeg.safeGet(); const cli = MatrixClientPeg.safeGet();
if (!(await cli.hasSecretStorageKey()) || forceReset) { if (!(await cli.hasSecretStorageKey()) || forceReset) {
@ -357,12 +378,7 @@ export async function accessSecretStorage(
throw new Error("Secret storage creation canceled"); throw new Error("Secret storage creation canceled");
} }
} else { } else {
const crypto = cli.getCrypto(); await cli.bootstrapCrossSigning({
if (!crypto) {
throw new Error("End-to-end encryption is disabled - unable to access secret storage.");
}
await crypto.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async (makeRequest): Promise<void> => { authUploadDeviceSigningKeys: async (makeRequest): Promise<void> => {
const { finished } = Modal.createDialog(InteractiveAuthDialog, { const { finished } = Modal.createDialog(InteractiveAuthDialog, {
title: _t("encryption|bootstrap_title"), title: _t("encryption|bootstrap_title"),
@ -375,9 +391,8 @@ export async function accessSecretStorage(
} }
}, },
}); });
await crypto.bootstrapSecretStorage({ await cli.bootstrapSecretStorage({
getKeyBackupPassphrase: promptForBackupPassphrase, getKeyBackupPassphrase: promptForBackupPassphrase,
setupNewKeyBackup,
}); });
const keyId = Object.keys(secretStorageKeys)[0]; const keyId = Object.keys(secretStorageKeys)[0];
@ -403,13 +418,6 @@ export async function accessSecretStorage(
logger.error(e); logger.error(e);
// Re-throw so that higher level logic can abort as needed // Re-throw so that higher level logic can abort as needed
throw e; throw e;
} finally {
// Clear secret storage key cache now that work is complete
secretStorageBeingAccessed = false;
if (!isCachingAllowed()) {
secretStorageKeys = {};
secretStorageKeyInfo = {};
}
} }
} }

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from "react"; import React from "react";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import { MatrixClientPeg } from "../../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
@ -74,25 +75,24 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
this.setState({ this.setState({
error: undefined, error: undefined,
}); });
let info: IKeyBackupInfo | undefined;
const cli = MatrixClientPeg.safeGet(); const cli = MatrixClientPeg.safeGet();
try { try {
// We don't want accessSecretStorage to create a backup for us - we await accessSecretStorage(async (): Promise<void> => {
// will create one ourselves in the closure we pass in by calling // `accessSecretStorage` will have bootstrapped secret storage if necessary, so we can now
// resetKeyBackup. // set up key backup.
const setupNewKeyBackup = false; //
const forceReset = false; // XXX: `bootstrapSecretStorage` also sets up key backup as a side effect, so there is a 90% chance
// this is actually redundant.
await accessSecretStorage( //
async (): Promise<void> => { // The only time it would *not* be redundant would be if, for some reason, we had working 4S but no
const crypto = cli.getCrypto(); // working key backup. (For example, if the user clicked "Delete Backup".)
if (!crypto) { info = await cli.prepareKeyBackupVersion(null /* random key */, {
throw new Error("End-to-end encryption is disabled - unable to create backup."); secureSecretStorage: true,
} });
await crypto.resetKeyBackup(); info = await cli.createKeyBackupVersion(info);
}, });
forceReset, await cli.scheduleAllGroupSessionsForBackup();
setupNewKeyBackup,
);
this.setState({ this.setState({
phase: Phase.Done, phase: Phase.Done,
}); });
@ -102,6 +102,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
// delete the version, disable backup, or do nothing? If we just // delete the version, disable backup, or do nothing? If we just
// disable without deleting, we'll enable on next app reload since // disable without deleting, we'll enable on next app reload since
// it is trusted. // it is trusted.
if (info?.version) {
cli.deleteKeyBackupVersion(info.version);
}
this.setState({ this.setState({
error: true, error: true,
}); });

View file

@ -26,7 +26,7 @@ import Spinner from "../elements/Spinner";
import InteractiveAuthDialog from "../dialogs/InteractiveAuthDialog"; import InteractiveAuthDialog from "../dialogs/InteractiveAuthDialog";
import ConfirmDestroyCrossSigningDialog from "../dialogs/security/ConfirmDestroyCrossSigningDialog"; import ConfirmDestroyCrossSigningDialog from "../dialogs/security/ConfirmDestroyCrossSigningDialog";
import SetupEncryptionDialog from "../dialogs/security/SetupEncryptionDialog"; import SetupEncryptionDialog from "../dialogs/security/SetupEncryptionDialog";
import { accessSecretStorage } from "../../../SecurityManager"; import { accessSecretStorage, withSecretStorageKeyCache } from "../../../SecurityManager";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import { SettingsSubsectionText } from "./shared/SettingsSubsection"; import { SettingsSubsectionText } from "./shared/SettingsSubsection";
@ -118,19 +118,14 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
} }
/** /**
* Bootstrapping cross-signing take one of these paths: * Reset the user's cross-signing keys.
* 1. Create cross-signing keys locally and store in secret storage (if it
* already exists on the account).
* 2. Access existing secret storage by requesting passphrase and accessing
* cross-signing keys as needed.
* 3. All keys are loaded and there's nothing to do.
* @param {bool} [forceReset] Bootstrap again even if keys already present
*/ */
private bootstrapCrossSigning = async ({ forceReset = false }): Promise<void> => { private async resetCrossSigning(): Promise<void> {
this.setState({ error: false }); this.setState({ error: false });
try { try {
const cli = MatrixClientPeg.safeGet(); const cli = MatrixClientPeg.safeGet();
await cli.bootstrapCrossSigning({ await withSecretStorageKeyCache(async () => {
await cli.getCrypto()!.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async (makeRequest): Promise<void> => { authUploadDeviceSigningKeys: async (makeRequest): Promise<void> => {
const { finished } = Modal.createDialog(InteractiveAuthDialog, { const { finished } = Modal.createDialog(InteractiveAuthDialog, {
title: _t("encryption|bootstrap_title"), title: _t("encryption|bootstrap_title"),
@ -142,7 +137,8 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
throw new Error("Cross-signing key upload auth canceled"); throw new Error("Cross-signing key upload auth canceled");
} }
}, },
setupNewCrossSigning: forceReset, setupNewCrossSigning: true,
});
}); });
} catch (e) { } catch (e) {
this.setState({ error: true }); this.setState({ error: true });
@ -150,13 +146,18 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
} }
if (this.unmounted) return; if (this.unmounted) return;
this.getUpdatedStatus(); this.getUpdatedStatus();
}; }
private resetCrossSigning = (): void => { /**
* Callback for when the user clicks the "reset cross signing" button.
*
* Shows a confirmation dialog, and then does the reset if confirmed.
*/
private onResetCrossSigningClick = (): void => {
Modal.createDialog(ConfirmDestroyCrossSigningDialog, { Modal.createDialog(ConfirmDestroyCrossSigningDialog, {
onFinished: (act) => { onFinished: async (act) => {
if (!act) return; if (!act) return;
this.bootstrapCrossSigning({ forceReset: true }); this.resetCrossSigning();
}, },
}); });
}; };
@ -243,7 +244,7 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
if (keysExistAnywhere) { if (keysExistAnywhere) {
actions.push( actions.push(
<AccessibleButton key="reset" kind="danger" onClick={this.resetCrossSigning}> <AccessibleButton key="reset" kind="danger" onClick={this.onResetCrossSigningClick}>
{_t("action|reset")} {_t("action|reset")}
</AccessibleButton>, </AccessibleButton>,
); );
@ -260,6 +261,7 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
<details> <details>
<summary className="mx_CrossSigningPanel_advanced">{_t("common|advanced")}</summary> <summary className="mx_CrossSigningPanel_advanced">{_t("common|advanced")}</summary>
<table className="mx_CrossSigningPanel_statusList"> <table className="mx_CrossSigningPanel_statusList">
<tbody>
<tr> <tr>
<th scope="row">{_t("settings|security|cross_signing_public_keys")}</th> <th scope="row">{_t("settings|security|cross_signing_public_keys")}</th>
<td> <td>
@ -308,6 +310,7 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
: _t("settings|security|cross_signing_not_found")} : _t("settings|security|cross_signing_not_found")}
</td> </td>
</tr> </tr>
</tbody>
</table> </table>
</details> </details>
{errorSection} {errorSection}

View file

@ -1,62 +0,0 @@
/*
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");
});
});
});
});

View file

@ -19,13 +19,11 @@ import React from "react";
import { mocked } from "jest-mock"; import { mocked } from "jest-mock";
import CreateKeyBackupDialog from "../../../../../src/async-components/views/dialogs/security/CreateKeyBackupDialog"; import CreateKeyBackupDialog from "../../../../../src/async-components/views/dialogs/security/CreateKeyBackupDialog";
import { createTestClient, filterConsole } from "../../../../test-utils"; import { createTestClient } from "../../../../test-utils";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
jest.mock("../../../../../src/SecurityManager", () => ({ jest.mock("../../../../../src/SecurityManager", () => ({
accessSecretStorage: async (func = async () => Promise<void>) => { accessSecretStorage: jest.fn().mockResolvedValue(undefined),
await func();
},
})); }));
describe("CreateKeyBackupDialog", () => { describe("CreateKeyBackupDialog", () => {
@ -41,14 +39,9 @@ describe("CreateKeyBackupDialog", () => {
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
describe("expecting failure", () => { it("should display the error message when backup creation failed", async () => {
filterConsole("Error creating key backup");
it("should display an error message when backup creation failed", async () => {
const matrixClient = createTestClient(); const matrixClient = createTestClient();
mocked(matrixClient.getCrypto()!.resetKeyBackup).mockImplementation(() => { mocked(matrixClient.scheduleAllGroupSessionsForBackup).mockRejectedValue("my error");
throw new Error("failed");
});
MatrixClientPeg.safeGet = MatrixClientPeg.get = () => matrixClient; MatrixClientPeg.safeGet = MatrixClientPeg.get = () => matrixClient;
const { asFragment } = render(<CreateKeyBackupDialog onFinished={jest.fn()} />); const { asFragment } = render(<CreateKeyBackupDialog onFinished={jest.fn()} />);
@ -58,18 +51,6 @@ describe("CreateKeyBackupDialog", () => {
expect(asFragment()).toMatchSnapshot(); 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 () => { it("should display the success dialog when the key backup is finished", async () => {
const onFinished = jest.fn(); const onFinished = jest.fn();
const { asFragment } = render(<CreateKeyBackupDialog onFinished={onFinished} />); const { asFragment } = render(<CreateKeyBackupDialog onFinished={onFinished} />);

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CreateKeyBackupDialog expecting failure should display an error message when backup creation failed 1`] = ` exports[`CreateKeyBackupDialog should display the error message when backup creation failed 1`] = `
<DocumentFragment> <DocumentFragment>
<div <div
data-focus-guard="true" data-focus-guard="true"

View file

@ -26,6 +26,8 @@ import {
mockClientMethodsCrypto, mockClientMethodsCrypto,
mockClientMethodsUser, mockClientMethodsUser,
} from "../../../test-utils"; } from "../../../test-utils";
import Modal from "../../../../src/Modal";
import ConfirmDestroyCrossSigningDialog from "../../../../src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog";
describe("<CrossSigningPanel />", () => { describe("<CrossSigningPanel />", () => {
const userId = "@alice:server.org"; const userId = "@alice:server.org";
@ -43,6 +45,10 @@ describe("<CrossSigningPanel />", () => {
mockClient.isCrossSigningReady.mockResolvedValue(false); mockClient.isCrossSigningReady.mockResolvedValue(false);
}); });
afterEach(() => {
jest.restoreAllMocks();
});
it("should render a spinner while loading", () => { it("should render a spinner while loading", () => {
getComponent(); getComponent();
@ -85,6 +91,21 @@ describe("<CrossSigningPanel />", () => {
expect(screen.getByTestId("summarised-status").innerHTML).toEqual("✅ Cross-signing is ready for use."); expect(screen.getByTestId("summarised-status").innerHTML).toEqual("✅ Cross-signing is ready for use.");
expect(screen.getByText("Cross-signing private keys:").parentElement!).toMatchSnapshot(); expect(screen.getByText("Cross-signing private keys:").parentElement!).toMatchSnapshot();
}); });
it("should allow reset of cross-signing", async () => {
mockClient.getCrypto()!.bootstrapCrossSigning = jest.fn().mockResolvedValue(undefined);
getComponent();
await flushPromises();
const modalSpy = jest.spyOn(Modal, "createDialog");
screen.getByRole("button", { name: "Reset" }).click();
expect(modalSpy).toHaveBeenCalledWith(ConfirmDestroyCrossSigningDialog, expect.any(Object));
modalSpy.mock.lastCall![1]!.onFinished(true);
expect(mockClient.getCrypto()!.bootstrapCrossSigning).toHaveBeenCalledWith(
expect.objectContaining({ setupNewCrossSigning: true }),
);
});
}); });
describe("when cross signing is not ready", () => { describe("when cross signing is not ready", () => {

View file

@ -175,6 +175,7 @@ exports[`<SecurityUserSettingsTab /> renders security section 1`] = `
<table <table
class="mx_CrossSigningPanel_statusList" class="mx_CrossSigningPanel_statusList"
> >
<tbody>
<tr> <tr>
<th <th
scope="row" scope="row"
@ -235,6 +236,7 @@ exports[`<SecurityUserSettingsTab /> renders security section 1`] = `
not found not found
</td> </td>
</tr> </tr>
</tbody>
</table> </table>
</details> </details>
</div> </div>

View file

@ -132,7 +132,6 @@ export function createTestClient(): MatrixClient {
getUserDeviceInfo: jest.fn(), getUserDeviceInfo: jest.fn(),
getUserVerificationStatus: jest.fn(), getUserVerificationStatus: jest.fn(),
getDeviceVerificationStatus: jest.fn(), getDeviceVerificationStatus: jest.fn(),
resetKeyBackup: jest.fn(),
}), }),
getPushActionsForEvent: jest.fn(), getPushActionsForEvent: jest.fn(),