Add key storage toggle to Encryption settings
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.mx_KeyBackupPanel_toggleRow {
|
||||
flex-direction: row;
|
||||
}
|
||||
@@ -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,118 @@
|
||||
/*
|
||||
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 {
|
||||
// 'null' means no backup is active. 'undefined' means we're still loading.
|
||||
isEnabled: boolean | undefined;
|
||||
setEnabled: (enable: boolean) => void;
|
||||
loading: boolean; // true if the state is still loading for the first time
|
||||
busy: boolean; // true if the status is in the process of being changed
|
||||
}
|
||||
|
||||
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 defaultKeyEvent = matrixClient.getAccountData("m.secret_storage.default_key");
|
||||
if (defaultKeyEvent) {
|
||||
if (defaultKeyEvent.getContent()?.key) {
|
||||
await matrixClient.deleteAccountData(
|
||||
`m.secret_storage.key.${defaultKeyEvent.getContent().key}`,
|
||||
);
|
||||
}
|
||||
// ...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 prevent EX from turning it back on.
|
||||
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,
|
||||
};
|
||||
}
|
||||
61
src/components/views/settings/encryption/KeyStoragePanel.tsx
Normal file
61
src/components/views/settings/encryption/KeyStoragePanel.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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, { FormEvent, JSX, useCallback } from "react";
|
||||
import { InlineField, InlineSpinner, Label, Root, ToggleControl } from "@vector-im/compound-web";
|
||||
|
||||
import { SettingsSection } from "../shared/SettingsSection";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { SettingsHeader } from "../SettingsHeader";
|
||||
import { useKeyStoragePanelViewModel } from "../../../viewmodels/settings/encryption/KeyStoragePanelViewModel";
|
||||
|
||||
/**
|
||||
* This component allows the user to set up or change their recovery key.
|
||||
*/
|
||||
export function KeyBackupPanel(): JSX.Element {
|
||||
const { isEnabled, setEnabled, loading, busy } = useKeyStoragePanelViewModel();
|
||||
|
||||
const onKeyBackupChange = useCallback(
|
||||
(e: FormEvent<HTMLInputElement>) => {
|
||||
setEnabled(e.currentTarget.checked);
|
||||
},
|
||||
[setEnabled],
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return <InlineSpinner />;
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,11 @@ import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import { SettingsSubheader } from "../../SettingsSubheader";
|
||||
import { AdvancedPanel } from "../../encryption/AdvancedPanel";
|
||||
import { ResetIdentityPanel } from "../../encryption/ResetIdentityPanel";
|
||||
import { KeyBackupPanel } from "../../encryption/KeyStoragePanel";
|
||||
import Spinner from "../../../elements/Spinner";
|
||||
import { useEventEmitter } from "../../../../../hooks/useEventEmitter";
|
||||
import { MatrixEvent } from "matrix-js-sdk";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
/**
|
||||
* The state in the encryption settings tab.
|
||||
@@ -35,44 +40,85 @@ import { ResetIdentityPanel } from "../../encryption/ResetIdentityPanel";
|
||||
*/
|
||||
type State = "loading" | "main" | "set_up_encryption" | "change_recovery_key" | "set_recovery_key" | "reset_identity";
|
||||
|
||||
const useKeyBackupIsEnabled = (): boolean | undefined => {
|
||||
const [isEnabled, setIsEnabled] = useState<boolean | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const matrixClient = useMatrixClientContext();
|
||||
|
||||
const checkStatus = useCallback(async () => {
|
||||
const crypto = matrixClient.getCrypto()!;
|
||||
const info = await crypto.getKeyBackupInfo();
|
||||
setIsEnabled(Boolean(info?.version));
|
||||
}, [matrixClient]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await checkStatus();
|
||||
setLoading(false);
|
||||
})();
|
||||
}, [checkStatus]);
|
||||
|
||||
useEventEmitter(matrixClient, ClientEvent.AccountData, (event: MatrixEvent): void => {
|
||||
const type = event.getType();
|
||||
if (type === "m.org.matrix.custom.backup_disabled") {
|
||||
checkStatus();
|
||||
}
|
||||
});
|
||||
|
||||
return loading ? undefined : isEnabled;
|
||||
};
|
||||
|
||||
export function EncryptionUserSettingsTab(): JSX.Element {
|
||||
const [state, setState] = useState<State>("loading");
|
||||
const setUpEncryptionRequired = useSetUpEncryptionRequired(setState);
|
||||
const keyBackupIsEnabled = useKeyBackupIsEnabled();
|
||||
|
||||
let content: JSX.Element;
|
||||
switch (state) {
|
||||
case "loading":
|
||||
content = <InlineSpinner aria-label={_t("common|loading")} />;
|
||||
break;
|
||||
case "set_up_encryption":
|
||||
content = <SetUpEncryptionPanel onFinish={setUpEncryptionRequired} />;
|
||||
break;
|
||||
case "main":
|
||||
content = (
|
||||
<>
|
||||
<RecoveryPanel
|
||||
onChangeRecoveryKeyClick={(setupNewKey) =>
|
||||
setupNewKey ? setState("set_recovery_key") : setState("change_recovery_key")
|
||||
}
|
||||
if (keyBackupIsEnabled === undefined) {
|
||||
content = <Spinner />;
|
||||
} else {
|
||||
switch (state) {
|
||||
case "loading":
|
||||
content = <InlineSpinner aria-label={_t("common|loading")} />;
|
||||
break;
|
||||
case "set_up_encryption":
|
||||
content = <SetUpEncryptionPanel onFinish={setUpEncryptionRequired} />;
|
||||
break;
|
||||
case "main":
|
||||
content = (
|
||||
<>
|
||||
<KeyBackupPanel />
|
||||
{keyBackupIsEnabled && (
|
||||
<>
|
||||
<RecoveryPanel
|
||||
onChangeRecoveryKeyClick={(setupNewKey) =>
|
||||
setupNewKey ? setState("set_recovery_key") : setState("change_recovery_key")
|
||||
}
|
||||
/>
|
||||
<Separator kind="section" />
|
||||
</>
|
||||
)}
|
||||
<AdvancedPanel onResetIdentityClick={() => setState("reset_identity")} />
|
||||
</>
|
||||
);
|
||||
break;
|
||||
case "change_recovery_key":
|
||||
case "set_recovery_key":
|
||||
content = (
|
||||
<ChangeRecoveryKey
|
||||
userHasRecoveryKey={state === "change_recovery_key"}
|
||||
onCancelClick={() => setState("main")}
|
||||
onFinish={() => setState("main")}
|
||||
/>
|
||||
<Separator kind="section" />
|
||||
<AdvancedPanel onResetIdentityClick={() => setState("reset_identity")} />
|
||||
</>
|
||||
);
|
||||
break;
|
||||
case "change_recovery_key":
|
||||
case "set_recovery_key":
|
||||
content = (
|
||||
<ChangeRecoveryKey
|
||||
userHasRecoveryKey={state === "change_recovery_key"}
|
||||
onCancelClick={() => setState("main")}
|
||||
onFinish={() => setState("main")}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "reset_identity":
|
||||
content = <ResetIdentityPanel onCancelClick={() => setState("main")} onFinish={() => setState("main")} />;
|
||||
break;
|
||||
);
|
||||
break;
|
||||
case "reset_identity":
|
||||
content = (
|
||||
<ResetIdentityPanel onCancelClick={() => setState("main")} onFinish={() => setState("main")} />
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -2482,6 +2482,11 @@
|
||||
"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": {
|
||||
"title": "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>",
|
||||
"allow_key_storage": "Allow 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.",
|
||||
|
||||
Reference in New Issue
Block a user