Compare commits
45 Commits
2c42e95265
...
dbkr/key_s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0162c82673 | ||
|
|
a5cec9e59e | ||
|
|
98edffac98 | ||
|
|
96a70f24a6 | ||
|
|
d1aef9f09a | ||
|
|
c4525b97b6 | ||
|
|
de183113e2 | ||
|
|
f0d9e05f85 | ||
|
|
9c4625d6a1 | ||
|
|
0c5f2b07b4 | ||
|
|
64f84cb4f8 | ||
|
|
7149b3d019 | ||
|
|
2ef05c5cb9 | ||
|
|
8ca4a8b6ec | ||
|
|
e8483e0186 | ||
|
|
f586c43a26 | ||
|
|
cfd55a6887 | ||
|
|
7c2d9f4954 | ||
|
|
1178d77fb8 | ||
|
|
25f8fe2009 | ||
|
|
fc9bc0903c | ||
|
|
f818d6e600 | ||
|
|
16c76cb20d | ||
|
|
40f9bd9480 | ||
|
|
e70afdb04f | ||
|
|
4a3a37323e | ||
|
|
df4c23bec7 | ||
|
|
4ea6a33497 | ||
|
|
87d44a7792 | ||
|
|
6b238d1fdc | ||
|
|
1b99071dfc | ||
|
|
a26efc58f1 | ||
|
|
130458783f | ||
|
|
5ac200492c | ||
|
|
aa6de76a8b | ||
|
|
e408715335 | ||
|
|
b7b2ea3448 | ||
|
|
6a20703ebc | ||
|
|
98114c8060 | ||
|
|
4db196e6bd | ||
|
|
4ec09f0063 | ||
|
|
97057de900 | ||
|
|
4eb07d4dda | ||
|
|
98950ded60 | ||
|
|
5a74298d66 |
@@ -111,4 +111,21 @@ test.describe("Encryption tab", () => {
|
||||
// The user is prompted to reset their identity
|
||||
await expect(dialog.getByText("Forgot your recovery key? You’ll need to reset your identity.")).toBeVisible();
|
||||
});
|
||||
|
||||
test("should warn before turning off key storage", { tag: "@screenshot" }, async ({ page, app, util }) => {
|
||||
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||
await util.openEncryptionTab();
|
||||
|
||||
await page.getByRole("checkbox", { name: "Allow key storage" }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("delete-key-storage-confirm.png");
|
||||
|
||||
await page.getByRole("button", { name: "Delete key storage" }).click();
|
||||
|
||||
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).not.toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@@ -48,6 +48,7 @@
|
||||
@import "./components/views/settings/devices/_FilteredDeviceListHeader.pcss";
|
||||
@import "./components/views/settings/devices/_SecurityRecommendations.pcss";
|
||||
@import "./components/views/settings/devices/_SelectableDeviceTile.pcss";
|
||||
@import "./components/views/settings/encryption/_KeyStoragePanel.pcss";
|
||||
@import "./components/views/settings/shared/_SettingsSubsection.pcss";
|
||||
@import "./components/views/settings/shared/_SettingsSubsectionHeading.pcss";
|
||||
@import "./components/views/spaces/_QuickThemeSwitcher.pcss";
|
||||
@@ -357,9 +358,9 @@
|
||||
@import "./views/settings/_UserProfileSettings.pcss";
|
||||
@import "./views/settings/encryption/_AdvancedPanel.pcss";
|
||||
@import "./views/settings/encryption/_ChangeRecoveryKey.pcss";
|
||||
@import "./views/settings/encryption/_DestructiveComponent.pcss";
|
||||
@import "./views/settings/encryption/_EncryptionCard.pcss";
|
||||
@import "./views/settings/encryption/_RecoveryPanelOutOfSync.pcss";
|
||||
@import "./views/settings/encryption/_ResetIdentityPanel.pcss";
|
||||
@import "./views/settings/tabs/_SettingsBanner.pcss";
|
||||
@import "./views/settings/tabs/_SettingsIndent.pcss";
|
||||
@import "./views/settings/tabs/_SettingsSection.pcss";
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.mx_KeyBackupPanel_toggleRow {
|
||||
flex-direction: row;
|
||||
}
|
||||
@@ -5,8 +5,11 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_ResetIdentityPanel {
|
||||
.mx_ResetIdentityPanel_content {
|
||||
/**
|
||||
* Shared by multiple components that confirm a destructive action in the user settings dialog.
|
||||
*/
|
||||
.mx_DestructiveComponent {
|
||||
.mx_DestructiveComponent_content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--cpd-space-3x);
|
||||
@@ -17,7 +20,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ResetIdentityPanel_footer {
|
||||
.mx_DestructiveComponent_footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--cpd-space-4x);
|
||||
@@ -318,8 +318,11 @@ export default class DeviceListener {
|
||||
// prompt the user to enter their recovery key.
|
||||
showSetupEncryptionToast(SetupKind.KEY_STORAGE_OUT_OF_SYNC);
|
||||
} else if (defaultKeyId === null) {
|
||||
// the user just hasn't set up 4S yet: prompt them to do so
|
||||
showSetupEncryptionToast(SetupKind.SET_UP_RECOVERY);
|
||||
// the user just hasn't set up 4S yet: prompt them to do so (unless they've explicitly said no to backups)
|
||||
const disabledEvent = cli.getAccountData("m.org.matrix.custom.backup_disabled");
|
||||
if (!disabledEvent || !disabledEvent.getContent()?.disabled) {
|
||||
showSetupEncryptionToast(SetupKind.SET_UP_RECOVERY);
|
||||
}
|
||||
} else {
|
||||
// some other condition... yikes! Show the 'set up encryption' toast: this is what we previously did
|
||||
// in 'other' situations. Possibly we should consider prompting for a full reset in this case?
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
Copyright 2025 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 { useCallback, useEffect, useState } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
||||
|
||||
interface KeyStoragePanelState {
|
||||
/**
|
||||
* Whether key storage is enabled, or 'undefined' if the state is still loading.
|
||||
*/
|
||||
isEnabled: boolean | undefined;
|
||||
|
||||
/**
|
||||
* A function that can be called to enable or disable key storage.
|
||||
* @param enable True to turn key storage on or false to turn it off
|
||||
*/
|
||||
setEnabled: (enable: boolean) => void;
|
||||
|
||||
/**
|
||||
* True if the state is still loading for the first time
|
||||
*/
|
||||
loading: boolean;
|
||||
|
||||
/**
|
||||
* True if the status is in the process of being changed
|
||||
*/
|
||||
busy: boolean;
|
||||
}
|
||||
|
||||
export function useKeyStoragePanelViewModel(): KeyStoragePanelState {
|
||||
const [isEnabled, setIsEnabled] = useState<boolean | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(true);
|
||||
// Whilst the change is being made, the toggle will reflect the pending value rather than the actual state
|
||||
const [pendingValue, setPendingValue] = useState<boolean | undefined>(undefined);
|
||||
|
||||
const matrixClient = useMatrixClientContext();
|
||||
|
||||
const checkStatus = useCallback(async () => {
|
||||
const crypto = matrixClient.getCrypto();
|
||||
if (!crypto) {
|
||||
logger.error("Can't check key backup status: no crypto module available");
|
||||
return;
|
||||
}
|
||||
const info = await crypto.getKeyBackupInfo();
|
||||
setIsEnabled(Boolean(info?.version));
|
||||
}, [matrixClient]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await checkStatus();
|
||||
setLoading(false);
|
||||
})();
|
||||
}, [checkStatus]);
|
||||
|
||||
const setEnabled = useCallback(
|
||||
async (enable: boolean) => {
|
||||
setPendingValue(enable);
|
||||
try {
|
||||
const crypto = matrixClient.getCrypto();
|
||||
if (!crypto) {
|
||||
logger.error("Can't change key backup status: no crypto module available");
|
||||
return;
|
||||
}
|
||||
if (enable) {
|
||||
const currentKeyBackup = await crypto.checkKeyBackupAndEnable();
|
||||
if (currentKeyBackup === null) {
|
||||
await crypto.resetKeyBackup();
|
||||
}
|
||||
|
||||
// resetKeyBackup fires this off in the background without waiting, so we need to do it
|
||||
// explicitly and wait for it, otherwise it won't be enabled yet when we check again.
|
||||
await crypto.checkKeyBackupAndEnable();
|
||||
|
||||
// Set the flag so that EX no longer thinks the user wants backup disabled
|
||||
await matrixClient.setAccountData("m.org.matrix.custom.backup_disabled", { disabled: false });
|
||||
} else {
|
||||
// Get the key backup version we're using
|
||||
const info = await crypto.getKeyBackupInfo();
|
||||
if (!info?.version) {
|
||||
logger.error("Can't delete key backup version: no version available");
|
||||
return;
|
||||
}
|
||||
|
||||
// Bye bye backup
|
||||
await crypto.deleteKeyBackupVersion(info.version);
|
||||
|
||||
// also turn off 4S, since this is also storing keys on the server.
|
||||
// Delete the cross signing keys from secret storage
|
||||
await matrixClient.deleteAccountData("m.cross_signing.master");
|
||||
await matrixClient.deleteAccountData("m.cross_signing.self_signing");
|
||||
await matrixClient.deleteAccountData("m.cross_signing.user_signing");
|
||||
// and the key backup key (we just turned it off anyway)
|
||||
await matrixClient.deleteAccountData("m.megolm_backup.v1");
|
||||
|
||||
// Delete the key information
|
||||
const defaultKey = await matrixClient.secretStorage.getDefaultKeyId();
|
||||
if (defaultKey) {
|
||||
await matrixClient.deleteAccountData(`m.secret_storage.key.${defaultKey}`);
|
||||
|
||||
// ...and the default key pointer
|
||||
await matrixClient.deleteAccountData("m.secret_storage.default_key");
|
||||
}
|
||||
|
||||
// finally, set a flag to say that the user doesn't want key backup.
|
||||
// Element X uses this to determine whether to set up automatically,
|
||||
// so this will stop EX turning it back on spontaneously.
|
||||
await matrixClient.setAccountData("m.org.matrix.custom.backup_disabled", { disabled: true });
|
||||
}
|
||||
|
||||
await checkStatus();
|
||||
} finally {
|
||||
setPendingValue(undefined);
|
||||
}
|
||||
},
|
||||
[setPendingValue, checkStatus, matrixClient],
|
||||
);
|
||||
|
||||
return {
|
||||
isEnabled: pendingValue ?? isEnabled,
|
||||
setEnabled,
|
||||
loading,
|
||||
busy: pendingValue !== undefined,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2025 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 { Breadcrumb, Button, VisualList, VisualListItem } from "@vector-im/compound-web";
|
||||
import CrossIcon from "@vector-im/compound-design-tokens/assets/web/icons/close";
|
||||
import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error";
|
||||
import React, { useCallback, useState } from "react";
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { EncryptionCard } from "./EncryptionCard";
|
||||
import { useKeyStoragePanelViewModel } from "../../../viewmodels/settings/encryption/KeyStoragePanelViewModel";
|
||||
import SdkConfig from "../../../../SdkConfig";
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Called when the user either cancels the operation or key storage has been disabled
|
||||
*/
|
||||
onFinish: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms that the user really wants to turn off and delete their key storage
|
||||
*/
|
||||
export function DeleteKeyStoragePanel({ onFinish }: Props): JSX.Element {
|
||||
const { setEnabled } = useKeyStoragePanelViewModel();
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const onDeleteClick = useCallback(async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await setEnabled(false);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
onFinish();
|
||||
}, [setEnabled, onFinish]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb
|
||||
backLabel={_t("action|back")}
|
||||
onBackClick={onFinish}
|
||||
pages={[_t("settings|encryption|title"), _t("settings|encryption|delete_key_storage|breadcrumb_page")]}
|
||||
onPageClick={onFinish}
|
||||
/>
|
||||
<EncryptionCard
|
||||
Icon={ErrorIcon}
|
||||
destructive={true}
|
||||
title={_t("settings|encryption|delete_key_storage|title")}
|
||||
className="mx_DestructiveComponent"
|
||||
>
|
||||
<div className="mx_DestructiveComponent_content">
|
||||
{_t("settings|encryption|delete_key_storage|description")}
|
||||
<VisualList>
|
||||
<VisualListItem Icon={CrossIcon} destructive={true}>
|
||||
{_t("settings|encryption|delete_key_storage|list_first")}
|
||||
</VisualListItem>
|
||||
<VisualListItem Icon={CrossIcon} destructive={true}>
|
||||
{_t("settings|encryption|delete_key_storage|list_second", { brand: SdkConfig.get().brand })}
|
||||
</VisualListItem>
|
||||
</VisualList>
|
||||
</div>
|
||||
<div className="mx_DestructiveComponent_footer">
|
||||
<Button destructive={true} onClick={onDeleteClick} disabled={busy}>
|
||||
{_t("settings|encryption|delete_key_storage|confirm")}
|
||||
</Button>
|
||||
<Button kind="tertiary" onClick={onFinish}>
|
||||
{_t("action|cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</EncryptionCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
73
src/components/views/settings/encryption/KeyStoragePanel.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2025 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, { useCallback } from "react";
|
||||
import { InlineField, InlineSpinner, Label, Root, ToggleControl } from "@vector-im/compound-web";
|
||||
|
||||
import type { FormEvent } from "react";
|
||||
import { SettingsSection } from "../shared/SettingsSection";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { SettingsHeader } from "../SettingsHeader";
|
||||
import { useKeyStoragePanelViewModel } from "../../../viewmodels/settings/encryption/KeyStoragePanelViewModel";
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Called when the user turns off the "allow key storage" toggle
|
||||
*/
|
||||
onKeyStorageDisableClick: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component allows the user to set up or change their recovery key.
|
||||
*/
|
||||
export const KeyStoragePanel: React.FC<Props> = ({ onKeyStorageDisableClick }) => {
|
||||
const { isEnabled, setEnabled, loading, busy } = useKeyStoragePanelViewModel();
|
||||
|
||||
const onKeyBackupChange = useCallback(
|
||||
(e: FormEvent<HTMLInputElement>) => {
|
||||
if (e.currentTarget.checked) {
|
||||
setEnabled(true);
|
||||
} else {
|
||||
onKeyStorageDisableClick();
|
||||
}
|
||||
},
|
||||
[setEnabled, onKeyStorageDisableClick],
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return <InlineSpinner aria-label={_t("common|loading")} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsSection
|
||||
legacy={false}
|
||||
heading={
|
||||
<SettingsHeader
|
||||
hasRecommendedTag={isEnabled === false}
|
||||
label={_t("settings|encryption|key_storage|title")}
|
||||
/>
|
||||
}
|
||||
subHeading={_t("settings|encryption|key_storage|description", undefined, {
|
||||
a: (sub) => (
|
||||
<a href="https://element.io/help#encryption5" target="_blank" rel="noreferrer noopener">
|
||||
{sub}
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
>
|
||||
<Root className="mx_KeyBackupPanel_toggleRow">
|
||||
<InlineField
|
||||
name="keyStorage"
|
||||
control={<ToggleControl name="keyStorage" checked={isEnabled} onChange={onKeyBackupChange} />}
|
||||
>
|
||||
<Label>{_t("settings|encryption|key_storage|allow_key_storage")}</Label>
|
||||
</InlineField>
|
||||
{busy && <InlineSpinner />}
|
||||
</Root>
|
||||
</SettingsSection>
|
||||
);
|
||||
};
|
||||
@@ -58,9 +58,9 @@ export function ResetIdentityPanel({ onCancelClick, onFinish, variant }: ResetId
|
||||
? _t("settings|encryption|advanced|breadcrumb_title_forgot")
|
||||
: _t("settings|encryption|advanced|breadcrumb_title")
|
||||
}
|
||||
className="mx_ResetIdentityPanel"
|
||||
className="mx_DestructiveComponent"
|
||||
>
|
||||
<div className="mx_ResetIdentityPanel_content">
|
||||
<div className="mx_DestructiveComponent_content">
|
||||
<VisualList>
|
||||
<VisualListItem Icon={CheckIcon} success={true}>
|
||||
{_t("settings|encryption|advanced|breadcrumb_first_description")}
|
||||
@@ -74,7 +74,7 @@ export function ResetIdentityPanel({ onCancelClick, onFinish, variant }: ResetId
|
||||
</VisualList>
|
||||
{variant === "compromised" && <span>{_t("settings|encryption|advanced|breadcrumb_warning")}</span>}
|
||||
</div>
|
||||
<div className="mx_ResetIdentityPanel_footer">
|
||||
<div className="mx_DestructiveComponent_footer">
|
||||
<Button
|
||||
destructive={true}
|
||||
onClick={async (evt) => {
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
import React, { type JSX, useCallback, useEffect, useState } from "react";
|
||||
import { Button, InlineSpinner, Separator } from "@vector-im/compound-web";
|
||||
import ComputerIcon from "@vector-im/compound-design-tokens/assets/web/icons/computer";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import type { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { RecoveryPanel } from "../../encryption/RecoveryPanel";
|
||||
import { ChangeRecoveryKey } from "../../encryption/ChangeRecoveryKey";
|
||||
@@ -21,6 +23,9 @@ import { SettingsSubheader } from "../../SettingsSubheader";
|
||||
import { AdvancedPanel } from "../../encryption/AdvancedPanel";
|
||||
import { ResetIdentityPanel } from "../../encryption/ResetIdentityPanel";
|
||||
import { RecoveryPanelOutOfSync } from "../../encryption/RecoveryPanelOutOfSync";
|
||||
import { useTypedEventEmitter } from "../../../../../hooks/useEventEmitter";
|
||||
import { KeyStoragePanel } from "../../encryption/KeyStoragePanel";
|
||||
import { DeleteKeyStoragePanel } from "../../encryption/DeleteKeyStoragePanel";
|
||||
|
||||
/**
|
||||
* The state in the encryption settings tab.
|
||||
@@ -34,8 +39,10 @@ import { RecoveryPanelOutOfSync } from "../../encryption/RecoveryPanelOutOfSync"
|
||||
* This happens when the user doesn't have a key a recovery key and the user clicks on "Set up recovery key" button of the RecoveryPanel.
|
||||
* - "reset_identity_compromised": The panel to show when the user is resetting their identity, in te case where their key is compromised.
|
||||
* - "reset_identity_forgot": The panel to show when the user is resetting their identity, in the case where they forgot their recovery key.
|
||||
* - `secrets_not_cached`: The secrets are not cached locally. This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets.
|
||||
* - "secrets_not_cached": The secrets are not cached locally. This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets.
|
||||
* If the "set_up_encryption" and "secrets_not_cached" conditions are both filled, "set_up_encryption" prevails.
|
||||
* - "key_storage_delete": The confirmation page asking if the user realy wants to turn off key storage
|
||||
* - "key_storage_disabled": The user has chosen to disable key storage and options are unavailable as a result.
|
||||
*/
|
||||
export type State =
|
||||
| "loading"
|
||||
@@ -45,7 +52,9 @@ export type State =
|
||||
| "set_recovery_key"
|
||||
| "reset_identity_compromised"
|
||||
| "reset_identity_forgot"
|
||||
| "secrets_not_cached";
|
||||
| "secrets_not_cached"
|
||||
| "key_storage_delete"
|
||||
| "key_storage_disabled";
|
||||
|
||||
interface EncryptionUserSettingsTabProps {
|
||||
/**
|
||||
@@ -63,6 +72,7 @@ export function EncryptionUserSettingsTab({ initialState = "loading" }: Encrypti
|
||||
const checkEncryptionState = useCheckEncryptionState(state, setState);
|
||||
|
||||
let content: JSX.Element;
|
||||
|
||||
switch (state) {
|
||||
case "loading":
|
||||
content = <InlineSpinner aria-label={_t("common|loading")} />;
|
||||
@@ -78,16 +88,23 @@ export function EncryptionUserSettingsTab({ initialState = "loading" }: Encrypti
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "key_storage_disabled":
|
||||
case "main":
|
||||
content = (
|
||||
<>
|
||||
<RecoveryPanel
|
||||
onChangeRecoveryKeyClick={(setupNewKey) =>
|
||||
setupNewKey ? setState("set_recovery_key") : setState("change_recovery_key")
|
||||
}
|
||||
/>
|
||||
<KeyStoragePanel onKeyStorageDisableClick={() => setState("key_storage_delete")} />
|
||||
<Separator kind="section" />
|
||||
<AdvancedPanel onResetIdentityClick={() => setState("reset_identity_compromised")} />
|
||||
{state === "main" && (
|
||||
<>
|
||||
<RecoveryPanel
|
||||
onChangeRecoveryKeyClick={(setupNewKey) =>
|
||||
setupNewKey ? setState("set_recovery_key") : setState("change_recovery_key")
|
||||
}
|
||||
/>
|
||||
<Separator kind="section" />
|
||||
</>
|
||||
)}
|
||||
<AdvancedPanel onResetIdentityClick={() => setState("reset_identity_compromised")} />{" "}
|
||||
</>
|
||||
);
|
||||
break;
|
||||
@@ -111,6 +128,9 @@ export function EncryptionUserSettingsTab({ initialState = "loading" }: Encrypti
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "key_storage_delete":
|
||||
content = <DeleteKeyStoragePanel onFinish={() => setState("main")} />;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -124,6 +144,7 @@ export function EncryptionUserSettingsTab({ initialState = "loading" }: Encrypti
|
||||
* Hook to check if the user needs:
|
||||
* - to go through the SetupEncryption flow.
|
||||
* - to enter their recovery key, if the secrets are not cached locally.
|
||||
* ...and also whether key backup is enabled.
|
||||
*
|
||||
* If the user needs to set up the encryption, the state will be set to "set_up_encryption".
|
||||
* If the user secrets are not cached, the state will be set to "secrets_not_cached".
|
||||
@@ -146,8 +167,14 @@ function useCheckEncryptionState(state: State, setState: (state: State) => void)
|
||||
const cachedSecrets = (await crypto.getCrossSigningStatus()).privateKeysCachedLocally;
|
||||
const secretsOk = cachedSecrets.masterKey && cachedSecrets.selfSigningKey && cachedSecrets.userSigningKey;
|
||||
|
||||
if (isCrossSigningReady && secretsOk) setState("main");
|
||||
// Also check the key backup status
|
||||
const backupInfo = await crypto.getKeyBackupInfo();
|
||||
|
||||
const keyStorageEnabled = Boolean(backupInfo?.version);
|
||||
|
||||
if (isCrossSigningReady && keyStorageEnabled && secretsOk) setState("main");
|
||||
else if (!isCrossSigningReady) setState("set_up_encryption");
|
||||
else if (!keyStorageEnabled) setState("key_storage_disabled");
|
||||
else setState("secrets_not_cached");
|
||||
}, [matrixClient, setState]);
|
||||
|
||||
@@ -156,6 +183,14 @@ function useCheckEncryptionState(state: State, setState: (state: State) => void)
|
||||
if (state === "loading") checkEncryptionState();
|
||||
}, [checkEncryptionState, state]);
|
||||
|
||||
useTypedEventEmitter(matrixClient, ClientEvent.AccountData, (event: MatrixEvent): void => {
|
||||
const type = event.getType();
|
||||
// Recheck the status if this account data has been updated as this implies it has changed
|
||||
if (type === "m.org.matrix.custom.backup_disabled") {
|
||||
checkEncryptionState();
|
||||
}
|
||||
});
|
||||
|
||||
// Also return the callback so that the component can re-run the logic.
|
||||
return checkEncryptionState;
|
||||
}
|
||||
|
||||
@@ -2482,10 +2482,23 @@
|
||||
"session_key": "Session key:",
|
||||
"title": "Advanced"
|
||||
},
|
||||
"delete_key_storage": {
|
||||
"breadcrumb_page": "Delete key storage",
|
||||
"confirm": "Delete key storage",
|
||||
"description": "Deleting key storage will remove your cryptographic identity and message keys from the server and turn off the following security features:",
|
||||
"list_first": "You will not have encrypted message history on new devices",
|
||||
"list_second": "You will lose access to your encrypted messages if you are signed out of %(brand)s everywhere",
|
||||
"title": "Are you sure you want to turn off key storage and delete it?"
|
||||
},
|
||||
"device_not_verified_button": "Verify this device",
|
||||
"device_not_verified_description": "You need to verify this device in order to view your encryption settings.",
|
||||
"device_not_verified_title": "Device not verified",
|
||||
"dialog_title": "<strong>Settings:</strong> Encryption",
|
||||
"key_storage": {
|
||||
"allow_key_storage": "Allow key storage",
|
||||
"description": "Store your cryptographic identity and message keys securely on the server. This will allow you to view your message history on any new devices. <a>Learn more</a>",
|
||||
"title": "Key storage"
|
||||
},
|
||||
"recovery": {
|
||||
"change_recovery_confirm_button": "Confirm new recovery key",
|
||||
"change_recovery_confirm_description": "Enter your new recovery key below to finish. Your old one will no longer work.",
|
||||
|
||||
@@ -154,6 +154,7 @@ export function createTestClient(): MatrixClient {
|
||||
resetEncryption: jest.fn(),
|
||||
getSessionBackupPrivateKey: jest.fn().mockResolvedValue(null),
|
||||
isSecretStorageReady: jest.fn().mockResolvedValue(false),
|
||||
deleteKeyBackupVersion: jest.fn(),
|
||||
}),
|
||||
|
||||
getPushActionsForEvent: jest.fn(),
|
||||
@@ -192,6 +193,7 @@ export function createTestClient(): MatrixClient {
|
||||
}),
|
||||
mxcUrlToHttp: jest.fn().mockImplementation((mxc: string) => `http://this.is.a.url/${mxc.substring(6)}`),
|
||||
setAccountData: jest.fn(),
|
||||
deleteAccountData: jest.fn(),
|
||||
setRoomAccountData: jest.fn(),
|
||||
setRoomTopic: jest.fn(),
|
||||
setRoomReadMarkers: jest.fn().mockResolvedValue({}),
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
Copyright 2025 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 { renderHook } from "jest-matrix-react";
|
||||
import { act } from "react";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import type { KeyBackupCheck, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
import { useKeyStoragePanelViewModel } from "../../../../../../src/components/viewmodels/settings/encryption/KeyStoragePanelViewModel";
|
||||
import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
|
||||
describe("KeyStoragePanelViewModel", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = createTestClient();
|
||||
});
|
||||
|
||||
it("should update the pending value immediately", async () => {
|
||||
const { result } = renderHook(
|
||||
() => useKeyStoragePanelViewModel(),
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
act(() => {
|
||||
result.current.setEnabled(true);
|
||||
});
|
||||
expect(result.current.isEnabled).toBe(true);
|
||||
expect(result.current.busy).toBe(true);
|
||||
});
|
||||
|
||||
it("should call resetKeyBackup if there is no backup currently", async () => {
|
||||
mocked(matrixClient.getCrypto()!.checkKeyBackupAndEnable).mockResolvedValue(null);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useKeyStoragePanelViewModel(),
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
|
||||
await result.current.setEnabled(true);
|
||||
expect(mocked(matrixClient.getCrypto()!.resetKeyBackup)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not call resetKeyBackup if there is a backup currently", async () => {
|
||||
mocked(matrixClient.getCrypto()!.checkKeyBackupAndEnable).mockResolvedValue({} as KeyBackupCheck);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useKeyStoragePanelViewModel(),
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
|
||||
await result.current.setEnabled(true);
|
||||
expect(mocked(matrixClient.getCrypto()!.resetKeyBackup)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should set account data flag when enabling", async () => {
|
||||
mocked(matrixClient.getCrypto()!.checkKeyBackupAndEnable).mockResolvedValue(null);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useKeyStoragePanelViewModel(),
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
|
||||
await result.current.setEnabled(true);
|
||||
expect(mocked(matrixClient.setAccountData)).toHaveBeenCalledWith("m.org.matrix.custom.backup_disabled", {
|
||||
disabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should delete backup when disabling", async () => {
|
||||
mocked(matrixClient.getCrypto()!.checkKeyBackupAndEnable).mockResolvedValue({} as KeyBackupCheck);
|
||||
mocked(matrixClient.getCrypto()!.getKeyBackupInfo).mockResolvedValue({ version: "99" } as KeyBackupInfo);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useKeyStoragePanelViewModel(),
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
|
||||
await result.current.setEnabled(false);
|
||||
|
||||
expect(mocked(matrixClient.getCrypto()!.deleteKeyBackupVersion)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should delete data stored in 4s when disabling", async () => {
|
||||
mocked(matrixClient.getCrypto()!.checkKeyBackupAndEnable).mockResolvedValue({} as KeyBackupCheck);
|
||||
mocked(matrixClient.getCrypto()!.getKeyBackupInfo).mockResolvedValue({ version: "99" } as KeyBackupInfo);
|
||||
mocked(matrixClient.secretStorage.getDefaultKeyId).mockResolvedValue("thekey");
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useKeyStoragePanelViewModel(),
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
|
||||
await result.current.setEnabled(false);
|
||||
|
||||
expect(mocked(matrixClient.deleteAccountData)).toHaveBeenCalledWith("m.cross_signing.master");
|
||||
expect(mocked(matrixClient.deleteAccountData)).toHaveBeenCalledWith("m.cross_signing.self_signing");
|
||||
expect(mocked(matrixClient.deleteAccountData)).toHaveBeenCalledWith("m.cross_signing.user_signing");
|
||||
expect(mocked(matrixClient.deleteAccountData)).toHaveBeenCalledWith("m.megolm_backup.v1");
|
||||
expect(mocked(matrixClient.deleteAccountData)).toHaveBeenCalledWith("m.secret_storage.default_key");
|
||||
expect(mocked(matrixClient.deleteAccountData)).toHaveBeenCalledWith("m.secret_storage.key.thekey");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2025 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 from "react";
|
||||
import { render, screen, waitFor } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { mocked } from "jest-mock";
|
||||
import { defer } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
import { DeleteKeyStoragePanel } from "../../../../../../src/components/views/settings/encryption/DeleteKeyStoragePanel";
|
||||
import { useKeyStoragePanelViewModel } from "../../../../../../src/components/viewmodels/settings/encryption/KeyStoragePanelViewModel";
|
||||
|
||||
jest.mock("../../../../../../src/components/viewmodels/settings/encryption/KeyStoragePanelViewModel", () => ({
|
||||
useKeyStoragePanelViewModel: jest.fn().mockReturnValue({
|
||||
setEnabled: jest.fn(),
|
||||
isEnabled: true,
|
||||
loading: false,
|
||||
busy: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("<DeleteKeyStoragePanel />", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = createTestClient();
|
||||
});
|
||||
|
||||
it("should match snapshot", async () => {
|
||||
const { asFragment } = render(
|
||||
<DeleteKeyStoragePanel onFinish={() => {}} />,
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should call onFinished when cancel pressed", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const onFinish = jest.fn();
|
||||
render(<DeleteKeyStoragePanel onFinish={onFinish} />, withClientContextRenderOptions(matrixClient));
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Cancel" }));
|
||||
expect(onFinish).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call disable key storage when confirm pressed", async () => {
|
||||
const setEnabled = jest.fn();
|
||||
|
||||
mocked(useKeyStoragePanelViewModel).mockReturnValue({
|
||||
setEnabled,
|
||||
isEnabled: true,
|
||||
loading: false,
|
||||
busy: false,
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
const onFinish = jest.fn();
|
||||
render(<DeleteKeyStoragePanel onFinish={onFinish} />, withClientContextRenderOptions(matrixClient));
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Delete key storage" }));
|
||||
|
||||
expect(setEnabled).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("should wait with button disabled while setEnabled runs", async () => {
|
||||
const setEnabledDefer = defer();
|
||||
|
||||
mocked(useKeyStoragePanelViewModel).mockReturnValue({
|
||||
setEnabled: jest.fn().mockReturnValue(setEnabledDefer.promise),
|
||||
isEnabled: true,
|
||||
loading: false,
|
||||
busy: false,
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
const onFinish = jest.fn();
|
||||
render(<DeleteKeyStoragePanel onFinish={onFinish} />, withClientContextRenderOptions(matrixClient));
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Delete key storage" }));
|
||||
|
||||
expect(onFinish).not.toHaveBeenCalled();
|
||||
expect(screen.getByRole("button", { name: "Delete key storage" })).toHaveAttribute("aria-disabled", "true");
|
||||
setEnabledDefer.resolve();
|
||||
await waitFor(() => expect(onFinish).toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,155 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<DeleteKeyStoragePanel /> should match snapshot 1`] = `
|
||||
<DocumentFragment>
|
||||
<nav
|
||||
class="_breadcrumb_ikpbb_17"
|
||||
>
|
||||
<button
|
||||
aria-label="Back"
|
||||
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m13.3 17.3-4.6-4.6a.877.877 0 0 1-.213-.325A1.106 1.106 0 0 1 8.425 12c0-.133.02-.258.062-.375A.878.878 0 0 1 8.7 11.3l4.6-4.6a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7L10.8 12l3.9 3.9a.949.949 0 0 1 .275.7.948.948 0 0 1-.275.7.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<ol
|
||||
class="_pages_ikpbb_26"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
class="_link_ue21z_17"
|
||||
data-kind="primary"
|
||||
data-size="small"
|
||||
rel="noreferrer noopener"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Encryption
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
aria-current="page"
|
||||
class="_last-page_ikpbb_39"
|
||||
>
|
||||
Delete key storage
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div
|
||||
class="mx_EncryptionCard mx_DestructiveComponent"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard_header"
|
||||
>
|
||||
<div
|
||||
class="_content_md016_17 _destructive_md016_43"
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
|
||||
>
|
||||
Are you sure you want to turn off key storage and delete it?
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DestructiveComponent_content"
|
||||
>
|
||||
Deleting key storage will remove your cryptographic identity and message keys from the server and turn off the following security features:
|
||||
<ul
|
||||
class="_visual-list_4dcf8_17"
|
||||
>
|
||||
<li
|
||||
class="_visual-list-item_bqeu7_17"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_visual-list-item-icon_bqeu7_26 _visual-list-item-icon-destructive_bqeu7_35"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
|
||||
/>
|
||||
</svg>
|
||||
You will not have encrypted message history on new devices
|
||||
</li>
|
||||
<li
|
||||
class="_visual-list-item_bqeu7_17"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_visual-list-item-icon_bqeu7_26 _visual-list-item-icon-destructive_bqeu7_35"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
|
||||
/>
|
||||
</svg>
|
||||
You will lose access to your encrypted messages if you are signed out of Element everywhere
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DestructiveComponent_footer"
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
class="_button_i91xf_17 _destructive_i91xf_116"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Delete key storage
|
||||
</button>
|
||||
<button
|
||||
class="_button_i91xf_17"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -55,7 +55,7 @@ exports[`<ResetIdentityPanel /> should display the 'forgot recovery key' variant
|
||||
</ol>
|
||||
</nav>
|
||||
<div
|
||||
class="mx_EncryptionCard mx_ResetIdentityPanel"
|
||||
class="mx_EncryptionCard mx_DestructiveComponent"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard_header"
|
||||
@@ -83,7 +83,7 @@ exports[`<ResetIdentityPanel /> should display the 'forgot recovery key' variant
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="mx_ResetIdentityPanel_content"
|
||||
class="mx_DestructiveComponent_content"
|
||||
>
|
||||
<ul
|
||||
class="_visual-list_4dcf8_17"
|
||||
@@ -155,7 +155,7 @@ exports[`<ResetIdentityPanel /> should display the 'forgot recovery key' variant
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
class="mx_ResetIdentityPanel_footer"
|
||||
class="mx_DestructiveComponent_footer"
|
||||
>
|
||||
<button
|
||||
class="_button_i91xf_17 _destructive_i91xf_116"
|
||||
@@ -235,7 +235,7 @@ exports[`<ResetIdentityPanel /> should reset the encryption when the continue bu
|
||||
</ol>
|
||||
</nav>
|
||||
<div
|
||||
class="mx_EncryptionCard mx_ResetIdentityPanel"
|
||||
class="mx_EncryptionCard mx_DestructiveComponent"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard_header"
|
||||
@@ -263,7 +263,7 @@ exports[`<ResetIdentityPanel /> should reset the encryption when the continue bu
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="mx_ResetIdentityPanel_content"
|
||||
class="mx_DestructiveComponent_content"
|
||||
>
|
||||
<ul
|
||||
class="_visual-list_4dcf8_17"
|
||||
@@ -338,7 +338,7 @@ exports[`<ResetIdentityPanel /> should reset the encryption when the continue bu
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_ResetIdentityPanel_footer"
|
||||
class="mx_DestructiveComponent_footer"
|
||||
>
|
||||
<button
|
||||
class="_button_i91xf_17 _destructive_i91xf_116"
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import { act, render, screen } from "jest-matrix-react";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { waitFor } from "@testing-library/dom";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { ClientEvent, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import type { KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
import {
|
||||
EncryptionUserSettingsTab,
|
||||
type State,
|
||||
@@ -66,12 +68,24 @@ describe("<EncryptionUserSettingsTab />", () => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should display the recovery panel when the encryption is set up", async () => {
|
||||
it("should display the recovery panel when key storage is enabled", async () => {
|
||||
jest.spyOn(matrixClient.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({
|
||||
version: "1",
|
||||
} as KeyBackupInfo);
|
||||
renderComponent();
|
||||
await waitFor(() => expect(screen.getByText("Recovery")).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it("should not display the recovery panel when key storage is not enabled", async () => {
|
||||
jest.spyOn(matrixClient.getCrypto()!, "getKeyBackupInfo").mockResolvedValue(null);
|
||||
renderComponent();
|
||||
await expect(screen.queryByText("Recovery")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display the recovery out of sync panel when secrets are not cached", async () => {
|
||||
jest.spyOn(matrixClient.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({
|
||||
version: "1",
|
||||
} as KeyBackupInfo);
|
||||
// Secrets are not cached
|
||||
jest.spyOn(matrixClient.getCrypto()!, "getCrossSigningStatus").mockResolvedValue({
|
||||
privateKeysInSecretStorage: true,
|
||||
@@ -96,6 +110,9 @@ describe("<EncryptionUserSettingsTab />", () => {
|
||||
});
|
||||
|
||||
it("should display the change recovery key panel when the user clicks on the change recovery button", async () => {
|
||||
jest.spyOn(matrixClient.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({
|
||||
version: "1",
|
||||
} as KeyBackupInfo);
|
||||
const user = userEvent.setup();
|
||||
|
||||
const { asFragment } = renderComponent();
|
||||
@@ -109,6 +126,9 @@ describe("<EncryptionUserSettingsTab />", () => {
|
||||
});
|
||||
|
||||
it("should display the set up recovery key when the user clicks on the set up recovery key button", async () => {
|
||||
jest.spyOn(matrixClient.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({
|
||||
version: "1",
|
||||
} as KeyBackupInfo);
|
||||
jest.spyOn(matrixClient.secretStorage, "getDefaultKeyId").mockResolvedValue(null);
|
||||
const user = userEvent.setup();
|
||||
|
||||
@@ -123,6 +143,10 @@ describe("<EncryptionUserSettingsTab />", () => {
|
||||
});
|
||||
|
||||
it("should display the reset identity panel when the user clicks on the reset cryptographic identity panel", async () => {
|
||||
jest.spyOn(matrixClient.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({
|
||||
version: "1",
|
||||
} as KeyBackupInfo);
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
const { asFragment } = renderComponent();
|
||||
@@ -137,17 +161,48 @@ describe("<EncryptionUserSettingsTab />", () => {
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should enter reset flow when showResetIdentity is set", () => {
|
||||
it("should enter reset flow when showResetIdentity is set", async () => {
|
||||
jest.spyOn(matrixClient.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({
|
||||
version: "1",
|
||||
} as KeyBackupInfo);
|
||||
|
||||
renderComponent({ initialState: "reset_identity_forgot" });
|
||||
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Forgot your recovery key? You’ll need to reset your identity." }),
|
||||
await expect(
|
||||
await screen.findByRole("heading", {
|
||||
name: "Forgot your recovery key? You’ll need to reset your identity.",
|
||||
}),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
it("should update when backup_disabled account data is changed", async () => {
|
||||
jest.spyOn(matrixClient.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({
|
||||
version: "1",
|
||||
} as KeyBackupInfo);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await expect(await screen.findByRole("heading", { name: "Recovery" })).toBeVisible();
|
||||
|
||||
jest.spyOn(matrixClient.getCrypto()!, "getKeyBackupInfo").mockResolvedValue(null);
|
||||
|
||||
act(() => {
|
||||
const accountDataEvent = new MatrixEvent({ type: "m.org.matrix.custom.backup_disabled" });
|
||||
matrixClient.emit(ClientEvent.AccountData, accountDataEvent);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("heading", { name: "Recovery" })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("should re-check the encryption state and displays the correct panel when the user clicks cancel the reset identity flow", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
jest.spyOn(matrixClient.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({
|
||||
version: "1",
|
||||
} as KeyBackupInfo);
|
||||
|
||||
// Secrets are not cached
|
||||
jest.spyOn(matrixClient.getCrypto()!, "getCrossSigningStatus").mockResolvedValue({
|
||||
privateKeysInSecretStorage: true,
|
||||
|
||||
@@ -226,7 +226,7 @@ exports[`<EncryptionUserSettingsTab /> should display the reset identity panel w
|
||||
</ol>
|
||||
</nav>
|
||||
<div
|
||||
class="mx_EncryptionCard mx_ResetIdentityPanel"
|
||||
class="mx_EncryptionCard mx_DestructiveComponent"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard_header"
|
||||
@@ -254,7 +254,7 @@ exports[`<EncryptionUserSettingsTab /> should display the reset identity panel w
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="mx_ResetIdentityPanel_content"
|
||||
class="mx_DestructiveComponent_content"
|
||||
>
|
||||
<ul
|
||||
class="_visual-list_4dcf8_17"
|
||||
@@ -329,7 +329,7 @@ exports[`<EncryptionUserSettingsTab /> should display the reset identity panel w
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_ResetIdentityPanel_footer"
|
||||
class="mx_DestructiveComponent_footer"
|
||||
>
|
||||
<button
|
||||
class="_button_i91xf_17 _destructive_i91xf_116"
|
||||
|
||||