Remove Secure Backup, Cross-signing and Cryptography sections in Security & Privacy user settings (#29088)

* feat(security tab)!: remove secure backup panel

BREAKING CHANGE: the key storage user interaction are moved into the Encryption tab. The debugging information are moved into the devtools.

* feat(security tab)!: remove cross signing section

BREAKING CHANGE: the cryptographic identity can be reseted in the Encryption tab. The debugging information are moved into the devtools

* feat(security tab)!: remove cryptography section

BREAKING CHANGE: this section can be found in the Advanced section of the encryption tab.

* test(security tab): update snapshot

* chore(security tab): remove unused component and function

* chore(security tab): update i18n

* test(e2e): remove `backups.spec.ts`
This commit is contained in:
Florian Duros
2025-04-08 14:40:06 +02:00
committed by GitHub
parent 803cb36d60
commit ab51ff6b7e
18 changed files with 8 additions and 2160 deletions

View File

@@ -1,133 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, screen } from "jest-matrix-react";
import { type Mocked, mocked } from "jest-mock";
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
import CrossSigningPanel from "../../../../../src/components/views/settings/CrossSigningPanel";
import {
flushPromises,
getMockClientWithEventEmitter,
mockClientMethodsCrypto,
mockClientMethodsUser,
} from "../../../../test-utils";
import Modal from "../../../../../src/Modal";
import ConfirmDestroyCrossSigningDialog from "../../../../../src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog";
describe("<CrossSigningPanel />", () => {
const userId = "@alice:server.org";
let mockClient: Mocked<MatrixClient>;
const getComponent = () => render(<CrossSigningPanel />);
beforeEach(() => {
mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
...mockClientMethodsCrypto(),
doesServerSupportUnstableFeature: jest.fn(),
});
mockClient.doesServerSupportUnstableFeature.mockResolvedValue(true);
});
afterEach(() => {
jest.restoreAllMocks();
});
it("should render a spinner while loading", () => {
getComponent();
expect(screen.getByRole("progressbar")).toBeInTheDocument();
});
it("should render when homeserver does not support cross-signing", async () => {
mockClient.doesServerSupportUnstableFeature.mockResolvedValue(false);
getComponent();
await flushPromises();
expect(screen.getByText("Your homeserver does not support cross-signing.")).toBeInTheDocument();
});
describe("when cross signing is ready", () => {
it("should render when keys are not backed up", async () => {
getComponent();
await flushPromises();
expect(screen.getByTestId("summarised-status").innerHTML).toEqual(
"⚠️ Cross-signing is ready but keys are not backed up.",
);
expect(screen.getByText("Cross-signing private keys:").parentElement!).toMatchSnapshot();
});
it("should render when keys are backed up", async () => {
mocked(mockClient.getCrypto()!.getCrossSigningStatus).mockResolvedValue({
publicKeysOnDevice: true,
privateKeysInSecretStorage: true,
privateKeysCachedLocally: {
masterKey: true,
selfSigningKey: true,
userSigningKey: true,
},
});
getComponent();
await flushPromises();
expect(screen.getByTestId("summarised-status").innerHTML).toEqual("✅ Cross-signing is ready for use.");
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", () => {
beforeEach(() => {
mocked(mockClient.getCrypto()!.isCrossSigningReady).mockResolvedValue(false);
});
it("should render when keys are not backed up", async () => {
getComponent();
await flushPromises();
expect(screen.getByTestId("summarised-status").innerHTML).toEqual("Cross-signing is not set up.");
});
it("should render when keys are backed up", async () => {
mocked(mockClient.getCrypto()!.getCrossSigningStatus).mockResolvedValue({
publicKeysOnDevice: true,
privateKeysInSecretStorage: true,
privateKeysCachedLocally: {
masterKey: true,
selfSigningKey: true,
userSigningKey: true,
},
});
getComponent();
await flushPromises();
expect(screen.getByTestId("summarised-status").innerHTML).toEqual(
"Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.",
);
expect(screen.getByText("Cross-signing private keys:").parentElement!).toMatchSnapshot();
});
});
});

View File

@@ -1,97 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, waitFor, screen, fireEvent } from "jest-matrix-react";
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import * as TestUtils from "../../../../test-utils";
import CryptographyPanel from "../../../../../src/components/views/settings/CryptographyPanel";
import { withClientContextRenderOptions } from "../../../../test-utils";
describe("CryptographyPanel", () => {
it("shows the session ID and key", async () => {
const sessionId = "ABCDEFGHIJ";
const sessionKey = "AbCDeFghIJK7L/m4nOPqRSTUVW4xyzaBCDef6gHIJkl";
const sessionKeyFormatted = "<strong>AbCD eFgh IJK7 L/m4 nOPq RSTU VW4x yzaB CDef 6gHI Jkl</strong>";
TestUtils.stubClient();
const client: MatrixClient = MatrixClientPeg.safeGet();
client.deviceId = sessionId;
mocked(client.getCrypto()!.getOwnDeviceKeys).mockResolvedValue({ ed25519: sessionKey, curve25519: "1234" });
// When we render the CryptographyPanel
const rendered = render(<CryptographyPanel />, withClientContextRenderOptions(client));
// Then it displays info about the user's session
const codes = rendered.container.querySelectorAll("code");
expect(codes.length).toEqual(2);
expect(codes[0].innerHTML).toEqual(sessionId);
// Initially a placeholder
expect(codes[1].innerHTML).toEqual("<strong>...</strong>");
// Then the actual key
await waitFor(() => expect(codes[1].innerHTML).toEqual(sessionKeyFormatted));
});
it("handles errors fetching session key", async () => {
const sessionId = "ABCDEFGHIJ";
TestUtils.stubClient();
const client: MatrixClient = MatrixClientPeg.safeGet();
client.deviceId = sessionId;
mocked(client.getCrypto()!.getOwnDeviceKeys).mockRejectedValue(new Error("bleh"));
// When we render the CryptographyPanel
const rendered = render(<CryptographyPanel />, withClientContextRenderOptions(client));
// Then it displays info about the user's session
const codes = rendered.container.querySelectorAll("code");
// Initially a placeholder
expect(codes[1].innerHTML).toEqual("<strong>...</strong>");
// Then "not supported key
await waitFor(() => expect(codes[1].innerHTML).toEqual("<strong>&lt;not supported&gt;</strong>"));
});
it("should open the export e2e keys dialog on click", async () => {
const sessionId = "ABCDEFGHIJ";
const sessionKey = "AbCDeFghIJK7L/m4nOPqRSTUVW4xyzaBCDef6gHIJkl";
TestUtils.stubClient();
const client: MatrixClient = MatrixClientPeg.safeGet();
client.deviceId = sessionId;
mocked(client.getCrypto()!.getOwnDeviceKeys).mockResolvedValue({ ed25519: sessionKey, curve25519: "1234" });
render(<CryptographyPanel />, withClientContextRenderOptions(client));
fireEvent.click(await screen.findByRole("button", { name: "Export E2E room keys" }));
await expect(screen.findByRole("heading", { name: "Export room keys" })).resolves.toBeInTheDocument();
});
it("should open the import e2e keys dialog on click", async () => {
const sessionId = "ABCDEFGHIJ";
const sessionKey = "AbCDeFghIJK7L/m4nOPqRSTUVW4xyzaBCDef6gHIJkl";
TestUtils.stubClient();
const client: MatrixClient = MatrixClientPeg.safeGet();
client.deviceId = sessionId;
mocked(client.getCrypto()!.getOwnDeviceKeys).mockResolvedValue({ ed25519: sessionKey, curve25519: "1234" });
render(<CryptographyPanel />, withClientContextRenderOptions(client));
fireEvent.click(await screen.findByRole("button", { name: "Import E2E room keys" }));
await expect(screen.findByRole("heading", { name: "Import room keys" })).resolves.toBeInTheDocument();
});
});

View File

@@ -1,171 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, screen, within } from "jest-matrix-react";
import { mocked } from "jest-mock";
import {
flushPromises,
getMockClientWithEventEmitter,
mockClientMethodsCrypto,
mockClientMethodsUser,
} from "../../../../test-utils";
import SecureBackupPanel from "../../../../../src/components/views/settings/SecureBackupPanel";
import { accessSecretStorage } from "../../../../../src/SecurityManager";
jest.mock("../../../../../src/SecurityManager", () => ({
accessSecretStorage: jest.fn(),
}));
describe("<SecureBackupPanel />", () => {
const userId = "@alice:server.org";
const client = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
...mockClientMethodsCrypto(),
getClientWellKnown: jest.fn(),
});
const getComponent = () => render(<SecureBackupPanel />);
beforeEach(() => {
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({
version: "1",
algorithm: "test",
auth_data: {
public_key: "1234",
},
});
Object.assign(client.getCrypto()!, {
isKeyBackupTrusted: jest.fn().mockResolvedValue({
trusted: false,
matchesDecryptionKey: false,
}),
getActiveSessionBackupVersion: jest.fn().mockResolvedValue(null),
deleteKeyBackupVersion: jest.fn().mockResolvedValue(undefined),
});
mocked(client.secretStorage.hasKey).mockClear().mockResolvedValue(false);
mocked(accessSecretStorage).mockClear().mockResolvedValue();
});
it("displays a loader while checking keybackup", async () => {
getComponent();
expect(screen.getByRole("progressbar")).toBeInTheDocument();
await flushPromises();
expect(screen.queryByRole("progressbar")).not.toBeInTheDocument();
});
it("handles error fetching backup", async () => {
// getKeyBackupInfo can fail for various reasons
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockImplementation(async () => {
throw new Error("beep beep");
});
const renderResult = getComponent();
await renderResult.findByText("Unable to load key backup status");
expect(renderResult.container).toMatchSnapshot();
});
it("handles absence of backup", async () => {
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockResolvedValue(null);
getComponent();
// flush getKeyBackupInfo promise
await flushPromises();
expect(screen.getByText("Back up your keys before signing out to avoid losing them.")).toBeInTheDocument();
});
it("suggests connecting session to key backup when backup exists", async () => {
const { container } = getComponent();
// flush checkKeyBackup promise
await flushPromises();
expect(container).toMatchSnapshot();
});
it("displays when session is connected to key backup", async () => {
mocked(client.getCrypto()!).getActiveSessionBackupVersion.mockResolvedValue("1");
getComponent();
// flush checkKeyBackup promise
await flushPromises();
expect(screen.getByText("✅ This session is backing up your keys.")).toBeInTheDocument();
});
it("asks for confirmation before deleting a backup", async () => {
getComponent();
// flush checkKeyBackup promise
await flushPromises();
fireEvent.click(screen.getByText("Delete Backup"));
const dialog = await screen.findByRole("dialog");
expect(
within(dialog).getByText(
"Are you sure? You will lose your encrypted messages if your keys are not backed up properly.",
),
).toBeInTheDocument();
fireEvent.click(within(dialog).getByText("Cancel"));
expect(client.getCrypto()!.deleteKeyBackupVersion).not.toHaveBeenCalled();
});
it("deletes backup after confirmation", async () => {
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo")
.mockResolvedValueOnce({
version: "1",
algorithm: "test",
auth_data: {
public_key: "1234",
},
})
.mockResolvedValue(null);
getComponent();
fireEvent.click(await screen.findByText("Delete Backup"));
const dialog = await screen.findByRole("dialog");
expect(
within(dialog).getByText(
"Are you sure? You will lose your encrypted messages if your keys are not backed up properly.",
),
).toBeInTheDocument();
fireEvent.click(within(dialog).getByTestId("dialog-primary-button"));
expect(client.getCrypto()!.deleteKeyBackupVersion).toHaveBeenCalledWith("1");
// delete request
await flushPromises();
// refresh backup info
await flushPromises();
});
it("resets secret storage", async () => {
mocked(client.secretStorage.hasKey).mockClear().mockResolvedValue(true);
getComponent();
// flush checkKeyBackup promise
await flushPromises();
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockClear();
mocked(client.getCrypto()!).isKeyBackupTrusted.mockClear();
fireEvent.click(screen.getByText("Reset"));
// enter loading state
expect(accessSecretStorage).toHaveBeenCalled();
await flushPromises();
// backup status refreshed
expect(client.getCrypto()!.getKeyBackupInfo).toHaveBeenCalled();
expect(client.getCrypto()!.isKeyBackupTrusted).toHaveBeenCalled();
});
});

View File

@@ -1,40 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<CrossSigningPanel /> when cross signing is not ready should render when keys are backed up 1`] = `
<tr>
<th
scope="row"
>
Cross-signing private keys:
</th>
<td>
in secret storage
</td>
</tr>
`;
exports[`<CrossSigningPanel /> when cross signing is ready should render when keys are backed up 1`] = `
<tr>
<th
scope="row"
>
Cross-signing private keys:
</th>
<td>
in secret storage
</td>
</tr>
`;
exports[`<CrossSigningPanel /> when cross signing is ready should render when keys are not backed up 1`] = `
<tr>
<th
scope="row"
>
Cross-signing private keys:
</th>
<td>
not found in storage
</td>
</tr>
`;

View File

@@ -1,190 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SecureBackupPanel /> handles error fetching backup 1`] = `
<div>
<div
class="mx_SettingsSubsection_text"
>
Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.
</div>
<div
class="mx_SettingsSubsection_text"
>
Unable to load key backup status
</div>
<details>
<summary
class="mx_SecureBackupPanel_advanced"
>
Advanced
</summary>
<table
class="mx_SecureBackupPanel_statusList"
>
<tr>
<th
scope="row"
>
Backup key stored:
</th>
<td>
not stored
</td>
</tr>
<tr>
<th
scope="row"
>
Backup key cached:
</th>
<td>
not found locally
</td>
</tr>
<tr>
<th
scope="row"
>
Secret storage public key:
</th>
<td>
not found
</td>
</tr>
<tr>
<th
scope="row"
>
Secret storage:
</th>
<td>
not ready
</td>
</tr>
</table>
</details>
</div>
`;
exports[`<SecureBackupPanel /> suggests connecting session to key backup when backup exists 1`] = `
<div>
<div
class="mx_SettingsSubsection_text"
>
Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.
</div>
<div
class="mx_SettingsSubsection_text"
>
<span>
This session is
<strong>
not backing up your keys
</strong>
, but you do have an existing backup you can restore from and add to going forward.
</span>
</div>
<div
class="mx_SettingsSubsection_text"
>
Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.
</div>
<details>
<summary
class="mx_SecureBackupPanel_advanced"
>
Advanced
</summary>
<table
class="mx_SecureBackupPanel_statusList"
>
<tr>
<th
scope="row"
>
Backup key stored:
</th>
<td>
not stored
</td>
</tr>
<tr>
<th
scope="row"
>
Backup key cached:
</th>
<td>
not found locally
</td>
</tr>
<tr>
<th
scope="row"
>
Secret storage public key:
</th>
<td>
not found
</td>
</tr>
<tr>
<th
scope="row"
>
Secret storage:
</th>
<td>
not ready
</td>
</tr>
<tr>
<th
scope="row"
>
Latest backup version on server:
</th>
<td>
1
(
Algorithm:
<code>
test
</code>
)
</td>
</tr>
<tr>
<th
scope="row"
>
Active backup version:
</th>
<td>
None
</td>
</tr>
</table>
<div />
</details>
<div
class="mx_SecureBackupPanel_buttonRow"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
role="button"
tabindex="0"
>
Connect this session to Key Backup
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_outline"
role="button"
tabindex="0"
>
Delete Backup
</div>
</div>
</div>
`;

View File

@@ -120,90 +120,6 @@ exports[`<SecurityUserSettingsTab /> renders security section 1`] = `
<div
class="mx_SettingsSection_subSections"
>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Secure Backup
</h3>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_SettingsSubsection_text"
>
Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.
</div>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
<details>
<summary
class="mx_SecureBackupPanel_advanced"
>
Advanced
</summary>
<table
class="mx_SecureBackupPanel_statusList"
>
<tr>
<th
scope="row"
>
Backup key stored:
</th>
<td>
not stored
</td>
</tr>
<tr>
<th
scope="row"
>
Backup key cached:
</th>
<td>
not found locally
</td>
</tr>
<tr>
<th
scope="row"
>
Secret storage public key:
</th>
<td>
not found
</td>
</tr>
<tr>
<th
scope="row"
>
Secret storage:
</th>
<td>
not ready
</td>
</tr>
</table>
</details>
</div>
</div>
<div
class="mx_SettingsSubsection"
>
@@ -240,203 +156,6 @@ exports[`<SecurityUserSettingsTab /> renders security section 1`] = `
</div>
</div>
</div>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Cross-signing
</h3>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
<details>
<summary
class="mx_CrossSigningPanel_advanced"
>
Advanced
</summary>
<table
class="mx_CrossSigningPanel_statusList"
>
<tbody>
<tr>
<th
scope="row"
>
Cross-signing public keys:
</th>
<td>
not found
</td>
</tr>
<tr>
<th
scope="row"
>
Cross-signing private keys:
</th>
<td>
not found in storage
</td>
</tr>
<tr>
<th
scope="row"
>
Master private key:
</th>
<td>
not found locally
</td>
</tr>
<tr>
<th
scope="row"
>
Self signing private key:
</th>
<td>
not found locally
</td>
</tr>
<tr>
<th
scope="row"
>
User signing private key:
</th>
<td>
not found locally
</td>
</tr>
<tr>
<th
scope="row"
>
Homeserver feature support:
</th>
<td>
not found
</td>
</tr>
</tbody>
</table>
</details>
</div>
</div>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Cryptography
</h3>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_SettingsSubsection_text"
>
<table
class="mx_CryptographyPanel_sessionInfo"
>
<tbody>
<tr>
<th
scope="row"
>
Session ID:
</th>
<td>
<code />
</td>
</tr>
<tr>
<th
scope="row"
>
Session key:
</th>
<td>
<code>
<strong>
...
</strong>
</code>
</td>
</tr>
</tbody>
</table>
</div>
<div
class="mx_CryptographyPanel_importExportButtons"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
role="button"
tabindex="0"
>
Export E2E room keys
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
role="button"
tabindex="0"
>
Import E2E room keys
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_vY7Q4uEh9K38"
>
<span
class="mx_SettingsFlag_labelText"
>
Only send messages to verified users
</span>
</label>
<div
aria-checked="false"
aria-disabled="false"
aria-label="Only send messages to verified users"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
id="mx_SettingsFlag_vY7Q4uEh9K38"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div