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:
@@ -1,108 +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 { type Page } from "@playwright/test";
|
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
|
||||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
|
||||||
import { completeCreateSecretStorageDialog } from "./utils.ts";
|
|
||||||
|
|
||||||
async function expectBackupVersionToBe(page: Page, version: string) {
|
|
||||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText(
|
|
||||||
version + " (Algorithm: m.megolm_backup.v1.curve25519-aes-sha2)",
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(version);
|
|
||||||
}
|
|
||||||
|
|
||||||
test.describe("Backups", () => {
|
|
||||||
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
|
||||||
test.use({
|
|
||||||
displayName: "Hanako",
|
|
||||||
});
|
|
||||||
|
|
||||||
test(
|
|
||||||
"Create, delete and recreate a keys backup",
|
|
||||||
{ tag: "@no-webkit" },
|
|
||||||
async ({ page, user, app }, workerInfo) => {
|
|
||||||
// Create a backup
|
|
||||||
const securityTab = await app.settings.openUserSettings("Security & Privacy");
|
|
||||||
|
|
||||||
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
|
||||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
|
||||||
|
|
||||||
const securityKey = await completeCreateSecretStorageDialog(page);
|
|
||||||
|
|
||||||
// Open the settings again
|
|
||||||
await app.settings.openUserSettings("Security & Privacy");
|
|
||||||
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
|
||||||
|
|
||||||
// expand the advanced section to see the active version in the reports
|
|
||||||
await page
|
|
||||||
.locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced")
|
|
||||||
.locator("..")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await expectBackupVersionToBe(page, "1");
|
|
||||||
|
|
||||||
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
|
|
||||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
|
|
||||||
// Delete it
|
|
||||||
await currentDialogLocator.getByTestId("dialog-primary-button").click(); // Click "Delete Backup"
|
|
||||||
|
|
||||||
// Create another
|
|
||||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible();
|
|
||||||
await currentDialogLocator.getByLabel("Recovery Key").fill(securityKey);
|
|
||||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
|
||||||
|
|
||||||
// Should be successful
|
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Success!" })).toBeVisible();
|
|
||||||
await currentDialogLocator.getByRole("button", { name: "OK", exact: true }).click();
|
|
||||||
|
|
||||||
// Open the settings again
|
|
||||||
await app.settings.openUserSettings("Security & Privacy");
|
|
||||||
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
|
||||||
|
|
||||||
// expand the advanced section to see the active version in the reports
|
|
||||||
await page
|
|
||||||
.locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced")
|
|
||||||
.locator("..")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await expectBackupVersionToBe(page, "2");
|
|
||||||
|
|
||||||
// ==
|
|
||||||
// Ensure that if you don't have the secret storage passphrase the backup won't be created
|
|
||||||
// ==
|
|
||||||
|
|
||||||
// First delete version 2
|
|
||||||
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
|
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
|
|
||||||
// Click "Delete Backup"
|
|
||||||
await currentDialogLocator.getByTestId("dialog-primary-button").click();
|
|
||||||
|
|
||||||
// Try to create another
|
|
||||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible();
|
|
||||||
// But cancel the recovery key dialog, to simulate not having the secret storage passphrase
|
|
||||||
await currentDialogLocator.getByTestId("dialog-cancel-button").click();
|
|
||||||
|
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Starting backup…" })).toBeVisible();
|
|
||||||
// check that it failed
|
|
||||||
await expect(currentDialogLocator.getByText("Unable to create key backup")).toBeVisible();
|
|
||||||
// cancel
|
|
||||||
await currentDialogLocator.getByTestId("dialog-cancel-button").click();
|
|
||||||
|
|
||||||
// go back to the settings to check that no backup was created (the setup button should still be there)
|
|
||||||
await app.settings.openUserSettings("Security & Privacy");
|
|
||||||
await expect(securityTab.getByRole("button", { name: "Set up", exact: true })).toBeVisible();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -340,8 +340,6 @@
|
|||||||
@import "./views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss";
|
@import "./views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss";
|
||||||
@import "./views/rooms/wysiwyg_composer/components/_LinkModal.pcss";
|
@import "./views/rooms/wysiwyg_composer/components/_LinkModal.pcss";
|
||||||
@import "./views/settings/_AvatarSetting.pcss";
|
@import "./views/settings/_AvatarSetting.pcss";
|
||||||
@import "./views/settings/_CrossSigningPanel.pcss";
|
|
||||||
@import "./views/settings/_CryptographyPanel.pcss";
|
|
||||||
@import "./views/settings/_FontScalingPanel.pcss";
|
@import "./views/settings/_FontScalingPanel.pcss";
|
||||||
@import "./views/settings/_ImageSizePanel.pcss";
|
@import "./views/settings/_ImageSizePanel.pcss";
|
||||||
@import "./views/settings/_IntegrationManager.pcss";
|
@import "./views/settings/_IntegrationManager.pcss";
|
||||||
@@ -354,7 +352,6 @@
|
|||||||
@import "./views/settings/_PhoneNumbers.pcss";
|
@import "./views/settings/_PhoneNumbers.pcss";
|
||||||
@import "./views/settings/_PowerLevelSelector.pcss";
|
@import "./views/settings/_PowerLevelSelector.pcss";
|
||||||
@import "./views/settings/_RoomProfileSettings.pcss";
|
@import "./views/settings/_RoomProfileSettings.pcss";
|
||||||
@import "./views/settings/_SecureBackupPanel.pcss";
|
|
||||||
@import "./views/settings/_SetIntegrationManager.pcss";
|
@import "./views/settings/_SetIntegrationManager.pcss";
|
||||||
@import "./views/settings/_SettingsFieldset.pcss";
|
@import "./views/settings/_SettingsFieldset.pcss";
|
||||||
@import "./views/settings/_SettingsHeader.pcss";
|
@import "./views/settings/_SettingsHeader.pcss";
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2019 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mx_CrossSigningPanel_statusList {
|
|
||||||
border-spacing: 0;
|
|
||||||
|
|
||||||
th {
|
|
||||||
text-align: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
td,
|
|
||||||
th {
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
&:first-of-type {
|
|
||||||
padding-inline-end: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CrossSigningPanel_buttonRow {
|
|
||||||
margin: 1em 0;
|
|
||||||
|
|
||||||
:nth-child(n + 1) {
|
|
||||||
margin-inline-end: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CrossSigningPanel_advanced {
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
@@ -1,32 +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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mx_CryptographyPanel_sessionInfo {
|
|
||||||
padding: 0em;
|
|
||||||
border-spacing: 0px;
|
|
||||||
}
|
|
||||||
.mx_CryptographyPanel_sessionInfo > tr {
|
|
||||||
vertical-align: baseline;
|
|
||||||
padding: 0em;
|
|
||||||
|
|
||||||
th {
|
|
||||||
text-align: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
td,
|
|
||||||
th {
|
|
||||||
padding: 0 1em 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CryptographyPanel_importExportButtons {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-flow: wrap;
|
|
||||||
row-gap: $spacing-8;
|
|
||||||
column-gap: $spacing-8;
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mx_SecureBackupPanel_deviceName {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SecureBackupPanel_buttonRow {
|
|
||||||
margin: 1em 0;
|
|
||||||
display: inline-flex;
|
|
||||||
flex-flow: wrap;
|
|
||||||
row-gap: 10px;
|
|
||||||
|
|
||||||
:nth-child(n + 1) {
|
|
||||||
margin-inline-end: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SecureBackupPanel_statusList {
|
|
||||||
border-spacing: 0;
|
|
||||||
|
|
||||||
th {
|
|
||||||
text-align: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
td,
|
|
||||||
th {
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
&:first-of-type {
|
|
||||||
padding-inline-end: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SecureBackupPanel_advanced {
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2020 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 { _t } from "../../../../languageHandler";
|
|
||||||
import BaseDialog from "../BaseDialog";
|
|
||||||
import DialogButtons from "../../elements/DialogButtons";
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
onFinished: (success?: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ConfirmDestroyCrossSigningDialog extends React.Component<IProps> {
|
|
||||||
private onConfirm = (): void => {
|
|
||||||
this.props.onFinished(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onDecline = (): void => {
|
|
||||||
this.props.onFinished(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
|
||||||
return (
|
|
||||||
<BaseDialog
|
|
||||||
className="mx_ConfirmDestroyCrossSigningDialog"
|
|
||||||
hasCancel={true}
|
|
||||||
onFinished={this.props.onFinished}
|
|
||||||
title={_t("encryption|destroy_cross_signing_dialog|title")}
|
|
||||||
>
|
|
||||||
<div className="mx_ConfirmDestroyCrossSigningDialog_content">
|
|
||||||
<p>{_t("encryption|destroy_cross_signing_dialog|warning")}</p>
|
|
||||||
</div>
|
|
||||||
<DialogButtons
|
|
||||||
primaryButton={_t("encryption|destroy_cross_signing_dialog|primary_button_text")}
|
|
||||||
onPrimaryButtonClick={this.onConfirm}
|
|
||||||
primaryButtonClass="danger"
|
|
||||||
cancelButton={_t("action|cancel")}
|
|
||||||
onCancel={this.onDecline}
|
|
||||||
/>
|
|
||||||
</BaseDialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2019, 2020 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, { type JSX } from "react";
|
|
||||||
import { ClientEvent, type EmptyObject, type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
|
||||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto-api";
|
|
||||||
|
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
import Modal from "../../../Modal";
|
|
||||||
import Spinner from "../elements/Spinner";
|
|
||||||
import InteractiveAuthDialog from "../dialogs/InteractiveAuthDialog";
|
|
||||||
import ConfirmDestroyCrossSigningDialog from "../dialogs/security/ConfirmDestroyCrossSigningDialog";
|
|
||||||
import SetupEncryptionDialog from "../dialogs/security/SetupEncryptionDialog";
|
|
||||||
import { accessSecretStorage, withSecretStorageKeyCache } from "../../../SecurityManager";
|
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
|
||||||
import { SettingsSubsectionText } from "./shared/SettingsSubsection";
|
|
||||||
|
|
||||||
interface IState {
|
|
||||||
error: boolean;
|
|
||||||
crossSigningPublicKeysOnDevice?: boolean;
|
|
||||||
crossSigningPrivateKeysInStorage?: boolean;
|
|
||||||
masterPrivateKeyCached?: boolean;
|
|
||||||
selfSigningPrivateKeyCached?: boolean;
|
|
||||||
userSigningPrivateKeyCached?: boolean;
|
|
||||||
homeserverSupportsCrossSigning?: boolean;
|
|
||||||
crossSigningReady?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class CrossSigningPanel extends React.PureComponent<EmptyObject, IState> {
|
|
||||||
private unmounted = false;
|
|
||||||
|
|
||||||
public constructor(props: EmptyObject) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
error: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidMount(): void {
|
|
||||||
this.unmounted = false;
|
|
||||||
const cli = MatrixClientPeg.safeGet();
|
|
||||||
cli.on(ClientEvent.AccountData, this.onAccountData);
|
|
||||||
cli.on(CryptoEvent.UserTrustStatusChanged, this.onStatusChanged);
|
|
||||||
cli.on(CryptoEvent.KeysChanged, this.onStatusChanged);
|
|
||||||
this.getUpdatedStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
|
||||||
this.unmounted = true;
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
if (!cli) return;
|
|
||||||
cli.removeListener(ClientEvent.AccountData, this.onAccountData);
|
|
||||||
cli.removeListener(CryptoEvent.UserTrustStatusChanged, this.onStatusChanged);
|
|
||||||
cli.removeListener(CryptoEvent.KeysChanged, this.onStatusChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onAccountData = (event: MatrixEvent): void => {
|
|
||||||
const type = event.getType();
|
|
||||||
if (type.startsWith("m.cross_signing") || type.startsWith("m.secret_storage")) {
|
|
||||||
this.getUpdatedStatus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onBootstrapClick = (): void => {
|
|
||||||
if (this.state.crossSigningPrivateKeysInStorage) {
|
|
||||||
Modal.createDialog(SetupEncryptionDialog, {}, undefined, /* priority = */ false, /* static = */ true);
|
|
||||||
} else {
|
|
||||||
// Trigger the flow to set up secure backup, which is what this will do when in
|
|
||||||
// the appropriate state.
|
|
||||||
accessSecretStorage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onStatusChanged = (): void => {
|
|
||||||
this.getUpdatedStatus();
|
|
||||||
};
|
|
||||||
|
|
||||||
private async getUpdatedStatus(): Promise<void> {
|
|
||||||
const cli = MatrixClientPeg.safeGet();
|
|
||||||
const crypto = cli.getCrypto();
|
|
||||||
if (!crypto) return;
|
|
||||||
|
|
||||||
const crossSigningStatus = await crypto.getCrossSigningStatus();
|
|
||||||
const crossSigningPublicKeysOnDevice = crossSigningStatus.publicKeysOnDevice;
|
|
||||||
const crossSigningPrivateKeysInStorage = crossSigningStatus.privateKeysInSecretStorage;
|
|
||||||
const masterPrivateKeyCached = crossSigningStatus.privateKeysCachedLocally.masterKey;
|
|
||||||
const selfSigningPrivateKeyCached = crossSigningStatus.privateKeysCachedLocally.selfSigningKey;
|
|
||||||
const userSigningPrivateKeyCached = crossSigningStatus.privateKeysCachedLocally.userSigningKey;
|
|
||||||
const homeserverSupportsCrossSigning =
|
|
||||||
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
|
|
||||||
const crossSigningReady = await crypto.isCrossSigningReady();
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
crossSigningPublicKeysOnDevice,
|
|
||||||
crossSigningPrivateKeysInStorage,
|
|
||||||
masterPrivateKeyCached,
|
|
||||||
selfSigningPrivateKeyCached,
|
|
||||||
userSigningPrivateKeyCached,
|
|
||||||
homeserverSupportsCrossSigning,
|
|
||||||
crossSigningReady,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset the user's cross-signing keys.
|
|
||||||
*/
|
|
||||||
private async resetCrossSigning(): Promise<void> {
|
|
||||||
this.setState({ error: false });
|
|
||||||
try {
|
|
||||||
const cli = MatrixClientPeg.safeGet();
|
|
||||||
await withSecretStorageKeyCache(async () => {
|
|
||||||
await cli.getCrypto()!.bootstrapCrossSigning({
|
|
||||||
authUploadDeviceSigningKeys: async (makeRequest): Promise<void> => {
|
|
||||||
const { finished } = Modal.createDialog(InteractiveAuthDialog, {
|
|
||||||
title: _t("encryption|bootstrap_title"),
|
|
||||||
matrixClient: cli,
|
|
||||||
makeRequest,
|
|
||||||
});
|
|
||||||
const [confirmed] = await finished;
|
|
||||||
if (!confirmed) {
|
|
||||||
throw new Error("Cross-signing key upload auth canceled");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setupNewCrossSigning: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
this.setState({ error: true });
|
|
||||||
logger.error("Error bootstrapping cross-signing", e);
|
|
||||||
}
|
|
||||||
if (this.unmounted) return;
|
|
||||||
this.getUpdatedStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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, {
|
|
||||||
onFinished: async (act) => {
|
|
||||||
if (!act) return;
|
|
||||||
this.resetCrossSigning();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
|
||||||
const {
|
|
||||||
error,
|
|
||||||
crossSigningPublicKeysOnDevice,
|
|
||||||
crossSigningPrivateKeysInStorage,
|
|
||||||
masterPrivateKeyCached,
|
|
||||||
selfSigningPrivateKeyCached,
|
|
||||||
userSigningPrivateKeyCached,
|
|
||||||
homeserverSupportsCrossSigning,
|
|
||||||
crossSigningReady,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
let errorSection;
|
|
||||||
if (error) {
|
|
||||||
errorSection = <div className="error">{error.toString()}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let summarisedStatus;
|
|
||||||
if (homeserverSupportsCrossSigning === undefined) {
|
|
||||||
summarisedStatus = <Spinner />;
|
|
||||||
} else if (!homeserverSupportsCrossSigning) {
|
|
||||||
summarisedStatus = (
|
|
||||||
<SettingsSubsectionText data-testid="summarised-status">
|
|
||||||
{_t("encryption|cross_signing_unsupported")}
|
|
||||||
</SettingsSubsectionText>
|
|
||||||
);
|
|
||||||
} else if (crossSigningReady && crossSigningPrivateKeysInStorage) {
|
|
||||||
summarisedStatus = (
|
|
||||||
<SettingsSubsectionText data-testid="summarised-status">
|
|
||||||
✅ {_t("encryption|cross_signing_ready")}
|
|
||||||
</SettingsSubsectionText>
|
|
||||||
);
|
|
||||||
} else if (crossSigningReady && !crossSigningPrivateKeysInStorage) {
|
|
||||||
summarisedStatus = (
|
|
||||||
<SettingsSubsectionText data-testid="summarised-status">
|
|
||||||
⚠️ {_t("encryption|cross_signing_ready_no_backup")}
|
|
||||||
</SettingsSubsectionText>
|
|
||||||
);
|
|
||||||
} else if (crossSigningPrivateKeysInStorage) {
|
|
||||||
summarisedStatus = (
|
|
||||||
<SettingsSubsectionText data-testid="summarised-status">
|
|
||||||
{_t("encryption|cross_signing_untrusted")}
|
|
||||||
</SettingsSubsectionText>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
summarisedStatus = (
|
|
||||||
<SettingsSubsectionText data-testid="summarised-status">
|
|
||||||
{_t("encryption|cross_signing_not_ready")}
|
|
||||||
</SettingsSubsectionText>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const keysExistAnywhere =
|
|
||||||
crossSigningPublicKeysOnDevice ||
|
|
||||||
crossSigningPrivateKeysInStorage ||
|
|
||||||
masterPrivateKeyCached ||
|
|
||||||
selfSigningPrivateKeyCached ||
|
|
||||||
userSigningPrivateKeyCached;
|
|
||||||
const keysExistEverywhere =
|
|
||||||
crossSigningPublicKeysOnDevice &&
|
|
||||||
crossSigningPrivateKeysInStorage &&
|
|
||||||
masterPrivateKeyCached &&
|
|
||||||
selfSigningPrivateKeyCached &&
|
|
||||||
userSigningPrivateKeyCached;
|
|
||||||
|
|
||||||
const actions: JSX.Element[] = [];
|
|
||||||
|
|
||||||
// TODO: determine how better to expose this to users in addition to prompts at login/toast
|
|
||||||
if (!keysExistEverywhere && homeserverSupportsCrossSigning) {
|
|
||||||
let buttonCaption = _t("encryption|set_up_toast_title");
|
|
||||||
if (crossSigningPrivateKeysInStorage) {
|
|
||||||
buttonCaption = _t("encryption|verify_toast_title");
|
|
||||||
}
|
|
||||||
actions.push(
|
|
||||||
<AccessibleButton key="setup" kind="primary_outline" onClick={this.onBootstrapClick}>
|
|
||||||
{buttonCaption}
|
|
||||||
</AccessibleButton>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keysExistAnywhere) {
|
|
||||||
actions.push(
|
|
||||||
<AccessibleButton key="reset" kind="danger_outline" onClick={this.onResetCrossSigningClick}>
|
|
||||||
{_t("action|reset")}
|
|
||||||
</AccessibleButton>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let actionRow;
|
|
||||||
if (actions.length) {
|
|
||||||
actionRow = <div className="mx_CrossSigningPanel_buttonRow">{actions}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{summarisedStatus}
|
|
||||||
<details>
|
|
||||||
<summary className="mx_CrossSigningPanel_advanced">{_t("common|advanced")}</summary>
|
|
||||||
<table className="mx_CrossSigningPanel_statusList">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{_t("settings|security|cross_signing_public_keys")}</th>
|
|
||||||
<td>
|
|
||||||
{crossSigningPublicKeysOnDevice
|
|
||||||
? _t("settings|security|cross_signing_in_memory")
|
|
||||||
: _t("settings|security|cross_signing_not_found")}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{_t("settings|security|cross_signing_private_keys")}</th>
|
|
||||||
<td>
|
|
||||||
{crossSigningPrivateKeysInStorage
|
|
||||||
? _t("settings|security|cross_signing_in_4s")
|
|
||||||
: _t("settings|security|cross_signing_not_in_4s")}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{_t("settings|security|cross_signing_master_private_Key")}</th>
|
|
||||||
<td>
|
|
||||||
{masterPrivateKeyCached
|
|
||||||
? _t("settings|security|cross_signing_cached")
|
|
||||||
: _t("settings|security|cross_signing_not_cached")}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{_t("settings|security|cross_signing_self_signing_private_key")}</th>
|
|
||||||
<td>
|
|
||||||
{selfSigningPrivateKeyCached
|
|
||||||
? _t("settings|security|cross_signing_cached")
|
|
||||||
: _t("settings|security|cross_signing_not_cached")}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{_t("settings|security|cross_signing_user_signing_private_key")}</th>
|
|
||||||
<td>
|
|
||||||
{userSigningPrivateKeyCached
|
|
||||||
? _t("settings|security|cross_signing_cached")
|
|
||||||
: _t("settings|security|cross_signing_not_cached")}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{_t("settings|security|cross_signing_homeserver_support")}</th>
|
|
||||||
<td>
|
|
||||||
{homeserverSupportsCrossSigning
|
|
||||||
? _t("settings|security|cross_signing_homeserver_support_exists")
|
|
||||||
: _t("settings|security|cross_signing_not_found")}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</details>
|
|
||||||
{errorSection}
|
|
||||||
{actionRow}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2021 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, { type JSX, lazy } from "react";
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
|
||||||
import { type EmptyObject } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
import Modal from "../../../Modal";
|
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
|
||||||
import * as FormattingUtils from "../../../utils/FormattingUtils";
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
|
||||||
import SettingsFlag from "../elements/SettingsFlag";
|
|
||||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
|
||||||
import { SettingsSubsection, SettingsSubsectionText } from "./shared/SettingsSubsection";
|
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
|
||||||
|
|
||||||
interface IState {
|
|
||||||
/** The device's base64-encoded Ed25519 identity key, or:
|
|
||||||
*
|
|
||||||
* * `undefined`: not yet loaded
|
|
||||||
* * `null`: encryption is not supported (or the crypto stack was not correctly initialized)
|
|
||||||
*/
|
|
||||||
deviceIdentityKey: string | undefined | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class CryptographyPanel extends React.Component<EmptyObject, IState> {
|
|
||||||
public static contextType = MatrixClientContext;
|
|
||||||
declare public context: React.ContextType<typeof MatrixClientContext>;
|
|
||||||
|
|
||||||
public constructor(props: EmptyObject, context: React.ContextType<typeof MatrixClientContext>) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
if (!context.getCrypto()) {
|
|
||||||
this.state = { deviceIdentityKey: null };
|
|
||||||
} else {
|
|
||||||
this.state = { deviceIdentityKey: undefined };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidMount(): void {
|
|
||||||
if (this.state.deviceIdentityKey === undefined) {
|
|
||||||
this.context
|
|
||||||
.getCrypto()
|
|
||||||
?.getOwnDeviceKeys()
|
|
||||||
.then((keys) => {
|
|
||||||
this.setState({ deviceIdentityKey: keys.ed25519 });
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
logger.error(`CryptographyPanel: Error fetching own device keys: ${e}`);
|
|
||||||
this.setState({ deviceIdentityKey: null });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
|
||||||
const client = this.context;
|
|
||||||
const deviceId = client.deviceId;
|
|
||||||
let identityKey = this.state.deviceIdentityKey;
|
|
||||||
if (identityKey === undefined) {
|
|
||||||
// Should show a spinner here really, but since this will be very transitional, I can't be doing with the
|
|
||||||
// necessary styling.
|
|
||||||
identityKey = "...";
|
|
||||||
} else if (identityKey === null) {
|
|
||||||
identityKey = _t("encryption|not_supported");
|
|
||||||
} else {
|
|
||||||
identityKey = FormattingUtils.formatCryptoKey(identityKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
let importExportButtons: JSX.Element | undefined;
|
|
||||||
if (client.getCrypto()) {
|
|
||||||
importExportButtons = (
|
|
||||||
<div className="mx_CryptographyPanel_importExportButtons">
|
|
||||||
<AccessibleButton kind="primary_outline" onClick={this.onExportE2eKeysClicked}>
|
|
||||||
{_t("settings|security|export_megolm_keys")}
|
|
||||||
</AccessibleButton>
|
|
||||||
<AccessibleButton kind="primary_outline" onClick={this.onImportE2eKeysClicked}>
|
|
||||||
{_t("settings|security|import_megolm_keys")}
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let noSendUnverifiedSetting: JSX.Element | undefined;
|
|
||||||
if (SettingsStore.canSetValue("blacklistUnverifiedDevices", null, SettingLevel.DEVICE)) {
|
|
||||||
noSendUnverifiedSetting = (
|
|
||||||
<SettingsFlag
|
|
||||||
name="blacklistUnverifiedDevices"
|
|
||||||
level={SettingLevel.DEVICE}
|
|
||||||
onChange={this.updateBlacklistDevicesFlag}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SettingsSubsection heading={_t("settings|security|cryptography_section")}>
|
|
||||||
<SettingsSubsectionText>
|
|
||||||
<table className="mx_CryptographyPanel_sessionInfo">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{_t("settings|security|session_id")}</th>
|
|
||||||
<td>
|
|
||||||
<code>{deviceId}</code>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{_t("settings|security|session_key")}</th>
|
|
||||||
<td>
|
|
||||||
<code>
|
|
||||||
<strong>{identityKey}</strong>
|
|
||||||
</code>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</SettingsSubsectionText>
|
|
||||||
{importExportButtons}
|
|
||||||
{noSendUnverifiedSetting}
|
|
||||||
</SettingsSubsection>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onExportE2eKeysClicked = (): void => {
|
|
||||||
Modal.createDialog(
|
|
||||||
lazy(() => import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog")),
|
|
||||||
{ matrixClient: this.context },
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onImportE2eKeysClicked = (): void => {
|
|
||||||
Modal.createDialog(
|
|
||||||
lazy(() => import("../../../async-components/views/dialogs/security/ImportE2eKeysDialog")),
|
|
||||||
{ matrixClient: this.context },
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private updateBlacklistDevicesFlag = (checked: boolean): void => {
|
|
||||||
const crypto = this.context.getCrypto();
|
|
||||||
if (crypto) crypto.globalBlacklistUnverifiedDevices = checked;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,421 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
|
|
||||||
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, { lazy, type JSX, type ReactNode } from "react";
|
|
||||||
import { CryptoEvent, type BackupTrustInfo, type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
|
||||||
import { type EmptyObject } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
import Modal from "../../../Modal";
|
|
||||||
import { isSecureBackupRequired } from "../../../utils/WellKnownUtils";
|
|
||||||
import Spinner from "../elements/Spinner";
|
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
|
||||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
|
||||||
import RestoreKeyBackupDialog from "../dialogs/security/RestoreKeyBackupDialog";
|
|
||||||
import { accessSecretStorage } from "../../../SecurityManager";
|
|
||||||
import { SettingsSubsectionText } from "./shared/SettingsSubsection";
|
|
||||||
|
|
||||||
interface IState {
|
|
||||||
loading: boolean;
|
|
||||||
error: boolean;
|
|
||||||
backupKeyStored: boolean | null;
|
|
||||||
backupKeyCached: boolean | null;
|
|
||||||
backupKeyWellFormed: boolean | null;
|
|
||||||
secretStorageKeyInAccount: boolean | null;
|
|
||||||
secretStorageReady: boolean | null;
|
|
||||||
|
|
||||||
/** Information on the current key backup version, as returned by the server.
|
|
||||||
*
|
|
||||||
* `null` could mean any of:
|
|
||||||
* * we haven't yet requested the data from the server.
|
|
||||||
* * we were unable to reach the server.
|
|
||||||
* * the server returned key backup version data we didn't understand or was malformed.
|
|
||||||
* * there is actually no backup on the server.
|
|
||||||
*/
|
|
||||||
backupInfo: KeyBackupInfo | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Information on whether the backup in `backupInfo` is correctly signed, and whether we have the right key to
|
|
||||||
* decrypt it.
|
|
||||||
*
|
|
||||||
* `undefined` if `backupInfo` is null, or if crypto is not enabled in the client.
|
|
||||||
*/
|
|
||||||
backupTrustInfo: BackupTrustInfo | undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If key backup is currently enabled, the backup version we are backing up to.
|
|
||||||
*/
|
|
||||||
activeBackupVersion: string | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Number of sessions remaining to be backed up. `null` if we have no information on this.
|
|
||||||
*/
|
|
||||||
sessionsRemaining: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class SecureBackupPanel extends React.PureComponent<EmptyObject, IState> {
|
|
||||||
private unmounted = false;
|
|
||||||
|
|
||||||
public constructor(props: EmptyObject) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
loading: true,
|
|
||||||
error: false,
|
|
||||||
backupKeyStored: null,
|
|
||||||
backupKeyCached: null,
|
|
||||||
backupKeyWellFormed: null,
|
|
||||||
secretStorageKeyInAccount: null,
|
|
||||||
secretStorageReady: null,
|
|
||||||
backupInfo: null,
|
|
||||||
backupTrustInfo: undefined,
|
|
||||||
activeBackupVersion: null,
|
|
||||||
sessionsRemaining: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidMount(): void {
|
|
||||||
this.unmounted = false;
|
|
||||||
this.loadBackupStatus();
|
|
||||||
|
|
||||||
MatrixClientPeg.safeGet().on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
|
|
||||||
MatrixClientPeg.safeGet().on(CryptoEvent.KeyBackupSessionsRemaining, this.onKeyBackupSessionsRemaining);
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
|
||||||
this.unmounted = true;
|
|
||||||
|
|
||||||
if (MatrixClientPeg.get()) {
|
|
||||||
MatrixClientPeg.get()!.removeListener(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
|
|
||||||
MatrixClientPeg.get()!.removeListener(
|
|
||||||
CryptoEvent.KeyBackupSessionsRemaining,
|
|
||||||
this.onKeyBackupSessionsRemaining,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onKeyBackupSessionsRemaining = (sessionsRemaining: number): void => {
|
|
||||||
this.setState({
|
|
||||||
sessionsRemaining,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onKeyBackupStatus = (): void => {
|
|
||||||
// This just loads the current backup status rather than forcing
|
|
||||||
// a re-check otherwise we risk causing infinite loops
|
|
||||||
this.loadBackupStatus();
|
|
||||||
};
|
|
||||||
|
|
||||||
private async loadBackupStatus(): Promise<void> {
|
|
||||||
this.setState({ loading: true });
|
|
||||||
this.getUpdatedDiagnostics();
|
|
||||||
try {
|
|
||||||
const cli = MatrixClientPeg.safeGet();
|
|
||||||
const backupInfo = (await cli.getCrypto()?.getKeyBackupInfo()) ?? null;
|
|
||||||
const backupTrustInfo = backupInfo ? await cli.getCrypto()?.isKeyBackupTrusted(backupInfo) : undefined;
|
|
||||||
|
|
||||||
const activeBackupVersion = (await cli.getCrypto()?.getActiveSessionBackupVersion()) ?? null;
|
|
||||||
|
|
||||||
if (this.unmounted) return;
|
|
||||||
this.setState({
|
|
||||||
loading: false,
|
|
||||||
error: false,
|
|
||||||
backupInfo,
|
|
||||||
backupTrustInfo,
|
|
||||||
activeBackupVersion,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.log("Unable to fetch key backup status", e);
|
|
||||||
if (this.unmounted) return;
|
|
||||||
this.setState({
|
|
||||||
loading: false,
|
|
||||||
error: true,
|
|
||||||
backupInfo: null,
|
|
||||||
backupTrustInfo: undefined,
|
|
||||||
activeBackupVersion: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getUpdatedDiagnostics(): Promise<void> {
|
|
||||||
const cli = MatrixClientPeg.safeGet();
|
|
||||||
const crypto = cli.getCrypto();
|
|
||||||
if (!crypto) return;
|
|
||||||
|
|
||||||
const secretStorage = cli.secretStorage;
|
|
||||||
|
|
||||||
const backupKeyStored = !!(await cli.isKeyBackupKeyStored());
|
|
||||||
const backupKeyFromCache = await crypto.getSessionBackupPrivateKey();
|
|
||||||
const backupKeyCached = !!backupKeyFromCache;
|
|
||||||
const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array;
|
|
||||||
const secretStorageKeyInAccount = await secretStorage.hasKey();
|
|
||||||
const secretStorageReady = await crypto.isSecretStorageReady();
|
|
||||||
|
|
||||||
if (this.unmounted) return;
|
|
||||||
this.setState({
|
|
||||||
backupKeyStored,
|
|
||||||
backupKeyCached,
|
|
||||||
backupKeyWellFormed,
|
|
||||||
secretStorageKeyInAccount,
|
|
||||||
secretStorageReady,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private startNewBackup = (): void => {
|
|
||||||
Modal.createDialog(
|
|
||||||
lazy(() => import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog")),
|
|
||||||
{
|
|
||||||
onFinished: () => {
|
|
||||||
this.loadBackupStatus();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
/* priority = */ false,
|
|
||||||
/* static = */ true,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private deleteBackup = (): void => {
|
|
||||||
Modal.createDialog(QuestionDialog, {
|
|
||||||
title: _t("settings|security|delete_backup"),
|
|
||||||
description: _t("settings|security|delete_backup_confirm_description"),
|
|
||||||
button: _t("settings|security|delete_backup"),
|
|
||||||
danger: true,
|
|
||||||
onFinished: (proceed) => {
|
|
||||||
if (!proceed) return;
|
|
||||||
this.setState({ loading: true });
|
|
||||||
const versionToDelete = this.state.backupInfo!.version!;
|
|
||||||
// deleteKeyBackupVersion fires a key backup status event
|
|
||||||
// which will update the UI
|
|
||||||
MatrixClientPeg.safeGet().getCrypto()?.deleteKeyBackupVersion(versionToDelete);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private restoreBackup = async (): Promise<void> => {
|
|
||||||
Modal.createDialog(RestoreKeyBackupDialog, undefined, undefined, /* priority = */ false, /* static = */ true);
|
|
||||||
};
|
|
||||||
|
|
||||||
private resetSecretStorage = async (): Promise<void> => {
|
|
||||||
this.setState({ error: false });
|
|
||||||
try {
|
|
||||||
await accessSecretStorage(async (): Promise<void> => {}, { forceReset: true });
|
|
||||||
} catch (e) {
|
|
||||||
logger.error("Error resetting secret storage", e);
|
|
||||||
if (this.unmounted) return;
|
|
||||||
this.setState({ error: true });
|
|
||||||
}
|
|
||||||
if (this.unmounted) return;
|
|
||||||
this.loadBackupStatus();
|
|
||||||
};
|
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
|
||||||
const {
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
backupKeyStored,
|
|
||||||
backupKeyCached,
|
|
||||||
backupKeyWellFormed,
|
|
||||||
secretStorageKeyInAccount,
|
|
||||||
secretStorageReady,
|
|
||||||
backupInfo,
|
|
||||||
backupTrustInfo,
|
|
||||||
sessionsRemaining,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
let statusDescription: JSX.Element;
|
|
||||||
let extraDetailsTableRows: JSX.Element | undefined;
|
|
||||||
let extraDetails: JSX.Element | undefined;
|
|
||||||
const actions: JSX.Element[] = [];
|
|
||||||
if (error) {
|
|
||||||
statusDescription = (
|
|
||||||
<SettingsSubsectionText className="error">
|
|
||||||
{_t("settings|security|error_loading_key_backup_status")}
|
|
||||||
</SettingsSubsectionText>
|
|
||||||
);
|
|
||||||
} else if (loading) {
|
|
||||||
statusDescription = <Spinner />;
|
|
||||||
} else if (backupInfo) {
|
|
||||||
let restoreButtonCaption = _t("settings|security|restore_key_backup");
|
|
||||||
|
|
||||||
if (this.state.activeBackupVersion !== null) {
|
|
||||||
statusDescription = (
|
|
||||||
<SettingsSubsectionText>✅ {_t("settings|security|key_backup_active")}</SettingsSubsectionText>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
statusDescription = (
|
|
||||||
<>
|
|
||||||
<SettingsSubsectionText>
|
|
||||||
{_t("settings|security|key_backup_inactive", {}, { b: (sub) => <strong>{sub}</strong> })}
|
|
||||||
</SettingsSubsectionText>
|
|
||||||
<SettingsSubsectionText>
|
|
||||||
{_t("settings|security|key_backup_connect_prompt")}
|
|
||||||
</SettingsSubsectionText>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
restoreButtonCaption = _t("settings|security|key_backup_connect");
|
|
||||||
}
|
|
||||||
|
|
||||||
let uploadStatus: ReactNode;
|
|
||||||
if (sessionsRemaining === null) {
|
|
||||||
// No upload status to show when backup disabled.
|
|
||||||
uploadStatus = "";
|
|
||||||
} else if (sessionsRemaining > 0) {
|
|
||||||
uploadStatus = (
|
|
||||||
<div>
|
|
||||||
{_t("settings|security|key_backup_in_progress", { sessionsRemaining })} <br />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
uploadStatus = (
|
|
||||||
<div>
|
|
||||||
{_t("settings|security|key_backup_complete")} <br />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let trustedLocally: string | undefined;
|
|
||||||
if (backupTrustInfo?.matchesDecryptionKey) {
|
|
||||||
trustedLocally = _t("settings|security|key_backup_can_be_restored");
|
|
||||||
}
|
|
||||||
|
|
||||||
extraDetailsTableRows = (
|
|
||||||
<>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{_t("settings|security|key_backup_latest_version")}</th>
|
|
||||||
<td>
|
|
||||||
{backupInfo.version} ({_t("settings|security|key_backup_algorithm")}{" "}
|
|
||||||
<code>{backupInfo.algorithm}</code>)
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{_t("settings|security|key_backup_active_version")}</th>
|
|
||||||
<td>
|
|
||||||
{this.state.activeBackupVersion === null
|
|
||||||
? _t("settings|security|key_backup_active_version_none")
|
|
||||||
: this.state.activeBackupVersion}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
extraDetails = (
|
|
||||||
<>
|
|
||||||
{uploadStatus}
|
|
||||||
<div>{trustedLocally}</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
actions.push(
|
|
||||||
<AccessibleButton key="restore" kind="primary_outline" onClick={this.restoreBackup}>
|
|
||||||
{restoreButtonCaption}
|
|
||||||
</AccessibleButton>,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isSecureBackupRequired(MatrixClientPeg.safeGet())) {
|
|
||||||
actions.push(
|
|
||||||
<AccessibleButton key="delete" kind="danger_outline" onClick={this.deleteBackup}>
|
|
||||||
{_t("settings|security|delete_backup")}
|
|
||||||
</AccessibleButton>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
statusDescription = (
|
|
||||||
<>
|
|
||||||
<SettingsSubsectionText>
|
|
||||||
{_t(
|
|
||||||
"settings|security|key_backup_inactive_warning",
|
|
||||||
{},
|
|
||||||
{ b: (sub) => <strong>{sub}</strong> },
|
|
||||||
)}
|
|
||||||
</SettingsSubsectionText>
|
|
||||||
<SettingsSubsectionText>{_t("encryption|setup_secure_backup|explainer")}</SettingsSubsectionText>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
actions.push(
|
|
||||||
<AccessibleButton key="setup" kind="primary_outline" onClick={this.startNewBackup}>
|
|
||||||
{_t("encryption|setup_secure_backup|title")}
|
|
||||||
</AccessibleButton>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (secretStorageKeyInAccount) {
|
|
||||||
actions.push(
|
|
||||||
<AccessibleButton key="reset" kind="danger_outline" onClick={this.resetSecretStorage}>
|
|
||||||
{_t("action|reset")}
|
|
||||||
</AccessibleButton>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let backupKeyWellFormedText = "";
|
|
||||||
if (backupKeyCached) {
|
|
||||||
backupKeyWellFormedText = ", ";
|
|
||||||
if (backupKeyWellFormed) {
|
|
||||||
backupKeyWellFormedText += _t("settings|security|backup_key_well_formed");
|
|
||||||
} else {
|
|
||||||
backupKeyWellFormedText += _t("settings|security|backup_key_unexpected_type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let actionRow: JSX.Element | undefined;
|
|
||||||
if (actions.length) {
|
|
||||||
actionRow = <div className="mx_SecureBackupPanel_buttonRow">{actions}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SettingsSubsectionText>{_t("settings|security|backup_keys_description")}</SettingsSubsectionText>
|
|
||||||
{statusDescription}
|
|
||||||
<details>
|
|
||||||
<summary className="mx_SecureBackupPanel_advanced">{_t("common|advanced")}</summary>
|
|
||||||
<table className="mx_SecureBackupPanel_statusList">
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{_t("settings|security|backup_key_stored_status")}</th>
|
|
||||||
<td>
|
|
||||||
{backupKeyStored === true
|
|
||||||
? _t("settings|security|cross_signing_in_4s")
|
|
||||||
: _t("settings|security|cross_signing_not_stored")}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{_t("settings|security|backup_key_cached_status")}</th>
|
|
||||||
<td>
|
|
||||||
{backupKeyCached
|
|
||||||
? _t("settings|security|cross_signing_cached")
|
|
||||||
: _t("settings|security|cross_signing_not_cached")}
|
|
||||||
{backupKeyWellFormedText}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{_t("settings|security|4s_public_key_status")}</th>
|
|
||||||
<td>
|
|
||||||
{secretStorageKeyInAccount
|
|
||||||
? _t("settings|security|4s_public_key_in_account_data")
|
|
||||||
: _t("settings|security|cross_signing_not_found")}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{_t("settings|security|secret_storage_status")}</th>
|
|
||||||
<td>
|
|
||||||
{secretStorageReady
|
|
||||||
? _t("settings|security|secret_storage_ready")
|
|
||||||
: _t("settings|security|secret_storage_not_ready")}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{extraDetailsTableRows}
|
|
||||||
</table>
|
|
||||||
{extraDetails}
|
|
||||||
</details>
|
|
||||||
{actionRow}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,13 +17,10 @@ import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
|||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
import dis from "../../../../../dispatcher/dispatcher";
|
import dis from "../../../../../dispatcher/dispatcher";
|
||||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||||
import SecureBackupPanel from "../../SecureBackupPanel";
|
|
||||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||||
import { type ActionPayload } from "../../../../../dispatcher/payloads";
|
import { type ActionPayload } from "../../../../../dispatcher/payloads";
|
||||||
import CryptographyPanel from "../../CryptographyPanel";
|
|
||||||
import SettingsFlag from "../../../elements/SettingsFlag";
|
import SettingsFlag from "../../../elements/SettingsFlag";
|
||||||
import CrossSigningPanel from "../../CrossSigningPanel";
|
|
||||||
import EventIndexPanel from "../../EventIndexPanel";
|
import EventIndexPanel from "../../EventIndexPanel";
|
||||||
import InlineSpinner from "../../../elements/InlineSpinner";
|
import InlineSpinner from "../../../elements/InlineSpinner";
|
||||||
import { PosthogAnalytics } from "../../../../../PosthogAnalytics";
|
import { PosthogAnalytics } from "../../../../../PosthogAnalytics";
|
||||||
@@ -42,21 +39,20 @@ interface IIgnoredUserProps {
|
|||||||
inProgress: boolean;
|
inProgress: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DehydratedDeviceStatus: React.FC = () => {
|
const SecureBackup: React.FC = () => {
|
||||||
const { dehydratedDeviceId } = useOwnDevices();
|
const { dehydratedDeviceId } = useOwnDevices();
|
||||||
|
if (!dehydratedDeviceId) return null;
|
||||||
|
|
||||||
if (dehydratedDeviceId) {
|
return (
|
||||||
return (
|
<SettingsSubsection heading={_t("common|secure_backup")}>
|
||||||
<div className="mx_SettingsSubsection_content">
|
<div className="mx_SettingsSubsection_content">
|
||||||
<div className="mx_SettingsFlag_label">{_t("settings|security|dehydrated_device_enabled")}</div>
|
<div className="mx_SettingsFlag_label">{_t("settings|security|dehydrated_device_enabled")}</div>
|
||||||
<div className="mx_SettingsSubsection_text">
|
<div className="mx_SettingsSubsection_text">
|
||||||
{_t("settings|security|dehydrated_device_description")}
|
{_t("settings|security|dehydrated_device_description")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</SettingsSubsection>
|
||||||
} else {
|
);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export class IgnoredUser extends React.Component<IIgnoredUserProps> {
|
export class IgnoredUser extends React.Component<IIgnoredUserProps> {
|
||||||
@@ -297,12 +293,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
const secureBackup = (
|
const secureBackup = <SecureBackup />;
|
||||||
<SettingsSubsection heading={_t("common|secure_backup")}>
|
|
||||||
<SecureBackupPanel />
|
|
||||||
<DehydratedDeviceStatus />
|
|
||||||
</SettingsSubsection>
|
|
||||||
);
|
|
||||||
|
|
||||||
const eventIndex = (
|
const eventIndex = (
|
||||||
<SettingsSubsection heading={_t("settings|security|message_search_section")}>
|
<SettingsSubsection heading={_t("settings|security|message_search_section")}>
|
||||||
@@ -310,16 +301,6 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
|
|||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
);
|
);
|
||||||
|
|
||||||
// XXX: There's no such panel in the current cross-signing designs, but
|
|
||||||
// it's useful to have for testing the feature. If there's no interest
|
|
||||||
// in having advanced details here once all flows are implemented, we
|
|
||||||
// can remove this.
|
|
||||||
const crossSigning = (
|
|
||||||
<SettingsSubsection heading={_t("common|cross_signing")}>
|
|
||||||
<CrossSigningPanel />
|
|
||||||
</SettingsSubsection>
|
|
||||||
);
|
|
||||||
|
|
||||||
let warning;
|
let warning;
|
||||||
if (!privateShouldBeEncrypted(MatrixClientPeg.safeGet())) {
|
if (!privateShouldBeEncrypted(MatrixClientPeg.safeGet())) {
|
||||||
warning = (
|
warning = (
|
||||||
@@ -379,8 +360,6 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
|
|||||||
<SettingsSection heading={_t("settings|security|encryption_section")}>
|
<SettingsSection heading={_t("settings|security|encryption_section")}>
|
||||||
{secureBackup}
|
{secureBackup}
|
||||||
{eventIndex}
|
{eventIndex}
|
||||||
{crossSigning}
|
|
||||||
<CryptographyPanel />
|
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
<SettingsSection heading={_t("common|privacy")}>
|
<SettingsSection heading={_t("common|privacy")}>
|
||||||
<DiscoverySettings />
|
<DiscoverySettings />
|
||||||
|
|||||||
@@ -487,7 +487,6 @@
|
|||||||
"capabilities": "Capabilities",
|
"capabilities": "Capabilities",
|
||||||
"copied": "Copied!",
|
"copied": "Copied!",
|
||||||
"credits": "Credits",
|
"credits": "Credits",
|
||||||
"cross_signing": "Cross-signing",
|
|
||||||
"dark": "Dark",
|
"dark": "Dark",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"deselect_all": "Deselect all",
|
"deselect_all": "Deselect all",
|
||||||
@@ -931,22 +930,12 @@
|
|||||||
"cancel_entering_passphrase_title": "Cancel entering passphrase?",
|
"cancel_entering_passphrase_title": "Cancel entering passphrase?",
|
||||||
"confirm_encryption_setup_body": "Click the button below to confirm setting up encryption.",
|
"confirm_encryption_setup_body": "Click the button below to confirm setting up encryption.",
|
||||||
"confirm_encryption_setup_title": "Confirm encryption setup",
|
"confirm_encryption_setup_title": "Confirm encryption setup",
|
||||||
"cross_signing_not_ready": "Cross-signing is not set up.",
|
|
||||||
"cross_signing_ready": "Cross-signing is ready for use.",
|
|
||||||
"cross_signing_ready_no_backup": "Cross-signing is ready but keys are not backed up.",
|
|
||||||
"cross_signing_room_normal": "This room is end-to-end encrypted",
|
"cross_signing_room_normal": "This room is end-to-end encrypted",
|
||||||
"cross_signing_room_verified": "Everyone in this room is verified",
|
"cross_signing_room_verified": "Everyone in this room is verified",
|
||||||
"cross_signing_room_warning": "Someone is using an unknown session",
|
"cross_signing_room_warning": "Someone is using an unknown session",
|
||||||
"cross_signing_unsupported": "Your homeserver does not support cross-signing.",
|
|
||||||
"cross_signing_untrusted": "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.",
|
|
||||||
"cross_signing_user_normal": "You have not verified this user.",
|
"cross_signing_user_normal": "You have not verified this user.",
|
||||||
"cross_signing_user_verified": "You have verified this user. This user has verified all of their sessions.",
|
"cross_signing_user_verified": "You have verified this user. This user has verified all of their sessions.",
|
||||||
"cross_signing_user_warning": "This user has not verified all of their sessions.",
|
"cross_signing_user_warning": "This user has not verified all of their sessions.",
|
||||||
"destroy_cross_signing_dialog": {
|
|
||||||
"primary_button_text": "Clear cross-signing keys",
|
|
||||||
"title": "Destroy cross-signing keys?",
|
|
||||||
"warning": "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from."
|
|
||||||
},
|
|
||||||
"enter_recovery_key": "Enter recovery key",
|
"enter_recovery_key": "Enter recovery key",
|
||||||
"event_shield_reason_authenticity_not_guaranteed": "The authenticity of this encrypted message can't be guaranteed on this device.",
|
"event_shield_reason_authenticity_not_guaranteed": "The authenticity of this encrypted message can't be guaranteed on this device.",
|
||||||
"event_shield_reason_mismatched_sender_key": "Encrypted by an unverified session",
|
"event_shield_reason_mismatched_sender_key": "Encrypted by an unverified session",
|
||||||
@@ -973,7 +962,6 @@
|
|||||||
"title": "New Recovery Method",
|
"title": "New Recovery Method",
|
||||||
"warning": "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings."
|
"warning": "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings."
|
||||||
},
|
},
|
||||||
"not_supported": "<not supported>",
|
|
||||||
"pinned_identity_changed": "%(displayName)s's (<b>%(userId)s</b>) identity was reset. <a>Learn more</a>",
|
"pinned_identity_changed": "%(displayName)s's (<b>%(userId)s</b>) identity was reset. <a>Learn more</a>",
|
||||||
"pinned_identity_changed_no_displayname": "<b>%(userId)s</b>'s identity was reset. <a>Learn more</a>",
|
"pinned_identity_changed_no_displayname": "<b>%(userId)s</b>'s identity was reset. <a>Learn more</a>",
|
||||||
"recovery_method_removed": {
|
"recovery_method_removed": {
|
||||||
@@ -989,8 +977,7 @@
|
|||||||
"set_up_toast_description": "Safeguard against losing access to encrypted messages & data",
|
"set_up_toast_description": "Safeguard against losing access to encrypted messages & data",
|
||||||
"set_up_toast_title": "Set up Secure Backup",
|
"set_up_toast_title": "Set up Secure Backup",
|
||||||
"setup_secure_backup": {
|
"setup_secure_backup": {
|
||||||
"explainer": "Back up your keys before signing out to avoid losing them.",
|
"explainer": "Back up your keys before signing out to avoid losing them."
|
||||||
"title": "Set up"
|
|
||||||
},
|
},
|
||||||
"udd": {
|
"udd": {
|
||||||
"interactive_verification_button": "Interactively verify by emoji",
|
"interactive_verification_button": "Interactively verify by emoji",
|
||||||
@@ -2832,57 +2819,20 @@
|
|||||||
"prompt_invite": "Prompt before sending invites to potentially invalid matrix IDs",
|
"prompt_invite": "Prompt before sending invites to potentially invalid matrix IDs",
|
||||||
"replace_plain_emoji": "Automatically replace plain text Emoji",
|
"replace_plain_emoji": "Automatically replace plain text Emoji",
|
||||||
"security": {
|
"security": {
|
||||||
"4s_public_key_in_account_data": "in account data",
|
|
||||||
"4s_public_key_status": "Secret storage public key:",
|
|
||||||
"analytics_description": "Share anonymous data to help us identify issues. Nothing personal. No third parties.",
|
"analytics_description": "Share anonymous data to help us identify issues. Nothing personal. No third parties.",
|
||||||
"backup_key_cached_status": "Backup key cached:",
|
|
||||||
"backup_key_stored_status": "Backup key stored:",
|
|
||||||
"backup_key_unexpected_type": "unexpected type",
|
|
||||||
"backup_key_well_formed": "well formed",
|
|
||||||
"backup_keys_description": "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.",
|
|
||||||
"bulk_options_accept_all_invites": "Accept all %(invitedRooms)s invites",
|
"bulk_options_accept_all_invites": "Accept all %(invitedRooms)s invites",
|
||||||
"bulk_options_reject_all_invites": "Reject all %(invitedRooms)s invites",
|
"bulk_options_reject_all_invites": "Reject all %(invitedRooms)s invites",
|
||||||
"bulk_options_section": "Bulk options",
|
"bulk_options_section": "Bulk options",
|
||||||
"cross_signing_cached": "cached locally",
|
|
||||||
"cross_signing_homeserver_support": "Homeserver feature support:",
|
|
||||||
"cross_signing_homeserver_support_exists": "exists",
|
|
||||||
"cross_signing_in_4s": "in secret storage",
|
|
||||||
"cross_signing_in_memory": "in memory",
|
|
||||||
"cross_signing_master_private_Key": "Master private key:",
|
|
||||||
"cross_signing_not_cached": "not found locally",
|
|
||||||
"cross_signing_not_found": "not found",
|
|
||||||
"cross_signing_not_in_4s": "not found in storage",
|
|
||||||
"cross_signing_not_stored": "not stored",
|
|
||||||
"cross_signing_private_keys": "Cross-signing private keys:",
|
|
||||||
"cross_signing_public_keys": "Cross-signing public keys:",
|
|
||||||
"cross_signing_self_signing_private_key": "Self signing private key:",
|
|
||||||
"cross_signing_user_signing_private_key": "User signing private key:",
|
|
||||||
"cryptography_section": "Cryptography",
|
|
||||||
"dehydrated_device_description": "The offline device feature allows you to receive encrypted messages even when you are not logged in to any devices",
|
"dehydrated_device_description": "The offline device feature allows you to receive encrypted messages even when you are not logged in to any devices",
|
||||||
"dehydrated_device_enabled": "Offline device enabled",
|
"dehydrated_device_enabled": "Offline device enabled",
|
||||||
"delete_backup": "Delete Backup",
|
|
||||||
"delete_backup_confirm_description": "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.",
|
|
||||||
"dialog_title": "<strong>Settings:</strong> Security & Privacy",
|
"dialog_title": "<strong>Settings:</strong> Security & Privacy",
|
||||||
"e2ee_default_disabled_warning": "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.",
|
"e2ee_default_disabled_warning": "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.",
|
||||||
"enable_message_search": "Enable message search in encrypted rooms",
|
"enable_message_search": "Enable message search in encrypted rooms",
|
||||||
"encryption_section": "Encryption",
|
"encryption_section": "Encryption",
|
||||||
"error_loading_key_backup_status": "Unable to load key backup status",
|
|
||||||
"export_megolm_keys": "Export E2E room keys",
|
|
||||||
"ignore_users_empty": "You have no ignored users.",
|
"ignore_users_empty": "You have no ignored users.",
|
||||||
"ignore_users_section": "Ignored users",
|
"ignore_users_section": "Ignored users",
|
||||||
"import_megolm_keys": "Import E2E room keys",
|
|
||||||
"key_backup_active": "This session is backing up your keys.",
|
|
||||||
"key_backup_active_version": "Active backup version:",
|
|
||||||
"key_backup_active_version_none": "None",
|
|
||||||
"key_backup_algorithm": "Algorithm:",
|
"key_backup_algorithm": "Algorithm:",
|
||||||
"key_backup_can_be_restored": "This backup can be restored on this session",
|
|
||||||
"key_backup_complete": "All keys backed up",
|
|
||||||
"key_backup_connect": "Connect this session to Key Backup",
|
"key_backup_connect": "Connect this session to Key Backup",
|
||||||
"key_backup_connect_prompt": "Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.",
|
|
||||||
"key_backup_in_progress": "Backing up %(sessionsRemaining)s keys…",
|
|
||||||
"key_backup_inactive": "This session is <b>not backing up your keys</b>, but you do have an existing backup you can restore from and add to going forward.",
|
|
||||||
"key_backup_inactive_warning": "Your keys are <b>not being backed up from this session</b>.",
|
|
||||||
"key_backup_latest_version": "Latest backup version on server:",
|
|
||||||
"message_search_disable_warning": "If disabled, messages from encrypted rooms won't appear in search results.",
|
"message_search_disable_warning": "If disabled, messages from encrypted rooms won't appear in search results.",
|
||||||
"message_search_disabled": "Securely cache encrypted messages locally for them to appear in search results.",
|
"message_search_disabled": "Securely cache encrypted messages locally for them to appear in search results.",
|
||||||
"message_search_enabled": {
|
"message_search_enabled": {
|
||||||
@@ -2902,13 +2852,7 @@
|
|||||||
"message_search_unsupported": "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.",
|
"message_search_unsupported": "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.",
|
||||||
"message_search_unsupported_web": "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.",
|
"message_search_unsupported_web": "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.",
|
||||||
"record_session_details": "Record the client name, version, and url to recognise sessions more easily in session manager",
|
"record_session_details": "Record the client name, version, and url to recognise sessions more easily in session manager",
|
||||||
"restore_key_backup": "Restore from Backup",
|
|
||||||
"secret_storage_not_ready": "not ready",
|
|
||||||
"secret_storage_ready": "ready",
|
|
||||||
"secret_storage_status": "Secret storage:",
|
|
||||||
"send_analytics": "Send analytics data",
|
"send_analytics": "Send analytics data",
|
||||||
"session_id": "Session ID:",
|
|
||||||
"session_key": "Session key:",
|
|
||||||
"strict_encryption": "Only send messages to verified users"
|
"strict_encryption": "Only send messages to verified users"
|
||||||
},
|
},
|
||||||
"send_read_receipts": "Send read receipts",
|
"send_read_receipts": "Send read receipts",
|
||||||
|
|||||||
@@ -56,17 +56,6 @@ export function formatBytes(bytes: number, decimals = 2): string {
|
|||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* format a key into groups of 4 characters, for easier visual inspection
|
|
||||||
*
|
|
||||||
* @param {string} key key to format
|
|
||||||
*
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
export function formatCryptoKey(key: string): string {
|
|
||||||
return key.match(/.{1,4}/g)!.join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUserNameColorClass(userId: string): string {
|
export function getUserNameColorClass(userId: string): string {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
const number = useIdColorHash(userId);
|
const number = useIdColorHash(userId);
|
||||||
|
|||||||
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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><not supported></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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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>
|
|
||||||
`;
|
|
||||||
@@ -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>
|
|
||||||
`;
|
|
||||||
@@ -120,90 +120,6 @@ exports[`<SecurityUserSettingsTab /> renders security section 1`] = `
|
|||||||
<div
|
<div
|
||||||
class="mx_SettingsSection_subSections"
|
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
|
<div
|
||||||
class="mx_SettingsSubsection"
|
class="mx_SettingsSubsection"
|
||||||
>
|
>
|
||||||
@@ -240,203 +156,6 @@ exports[`<SecurityUserSettingsTab /> renders security section 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user