Files
blap/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx
T
Hubert Chathi ebd5df633e Handle cross-signing keys missing locally and/or from secret storage (#31367)
* show correct toast when cross-signing keys missing

If cross-signing keys are missing both locally and in 4S, show a new toast
saying that identity needs resetting, rather than saying that the device
needs to be verified.

* refactor: make DeviceListener in charge of device state

- move enum from SetupEncryptionToast to DeviceListener
- DeviceListener has public method to get device state
- DeviceListener emits events to update device state

* reset key backup when needed in RecoveryPanelOutOfSync

brings RecoveryPanelOutOfSync in line with SetupEncryptionToast behaviour

* update strings to agree with designs from Figma

* use DeviceListener to determine EncryptionUserSettingsTab display

rather than using its own logic

* prompt to reset identity in Encryption Settings when needed

* fix type

* calculate device state even if we aren't going to show a toast

* update snapshot

* make logs more accurate

* add tests

* make the bot use a different access token/device

* only log in a new session when requested

* Mark properties as read-only

Co-authored-by: Skye Elliot <actuallyori@gmail.com>

* remove some duplicate strings

* make accessToken optional instead of using empty string

* switch from enum to string union as per review

* apply other changes from review

* handle errors in accessSecretStorage

* remove incorrect testid

---------

Co-authored-by: Skye Elliot <actuallyori@gmail.com>
2025-12-19 17:00:50 +00:00

247 lines
10 KiB
TypeScript

/*
* Copyright 2024 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, { type JSX, useState } from "react";
import { Button, Separator } from "@vector-im/compound-web";
import ComputerIcon from "@vector-im/compound-design-tokens/assets/web/icons/computer";
import SettingsTab from "../SettingsTab";
import { RecoveryPanel } from "../../encryption/RecoveryPanel";
import { ChangeRecoveryKey } from "../../encryption/ChangeRecoveryKey";
import { _t } from "../../../../../languageHandler";
import Modal from "../../../../../Modal";
import SetupEncryptionDialog from "../../../dialogs/security/SetupEncryptionDialog";
import { SettingsSection } from "../../shared/SettingsSection";
import { SettingsSubheader } from "../../SettingsSubheader";
import { AdvancedPanel } from "../../encryption/AdvancedPanel";
import { ResetIdentityPanel } from "../../encryption/ResetIdentityPanel";
import { type ResetIdentityBodyVariant } from "../../encryption/ResetIdentityBody";
import { RecoveryPanelOutOfSync } from "../../encryption/RecoveryPanelOutOfSync";
import { useTypedEventEmitterState } from "../../../../../hooks/useEventEmitter";
import { KeyStoragePanel } from "../../encryption/KeyStoragePanel";
import { DeleteKeyStoragePanel } from "../../encryption/DeleteKeyStoragePanel";
import DeviceListener, { DeviceListenerEvents, type DeviceState } from "../../../../../DeviceListener";
import { useKeyStoragePanelViewModel } from "../../../../viewmodels/settings/encryption/KeyStoragePanelViewModel";
/**
* The state in the encryption settings tab.
* - "main": The main panel with all the sections (Key storage, recovery, advanced).
* - "change_recovery_key": The panel to show when the user is changing their recovery key.
* This happens when the user has a recovery key and the user clicks on "Change recovery key" button of the RecoveryPanel.
* - "set_recovery_key": The panel to show when the user is setting up their recovery key.
* 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 the 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.
* - "reset_identity_sync_failed": The panel to show when the user us resetting their identity, in the case where recovery failed.
* - "reset_identity_cant_recover": The panel to show when the user is resetting their identity, in the case where they can't use recovery.
* - "key_storage_delete": The confirmation page asking if the user really wants to turn off key storage.
*/
export type State =
| "main"
| "change_recovery_key"
| "set_recovery_key"
| "reset_identity_compromised"
| "reset_identity_forgot"
| "reset_identity_sync_failed"
| "reset_identity_cant_recover"
| "key_storage_delete";
interface Props {
/**
* If the tab should start in a state other than the default
*/
initialState?: State;
}
/**
* The encryption settings tab.
*/
export function EncryptionUserSettingsTab({ initialState = "main" }: Readonly<Props>): JSX.Element {
const [state, setState] = useState<State>(initialState);
const deviceState = useTypedEventEmitterState(
DeviceListener.sharedInstance(),
DeviceListenerEvents.DeviceState,
(state?: DeviceState): DeviceState => {
return state ?? DeviceListener.sharedInstance().getDeviceState();
},
);
const { isEnabled: isBackupEnabled } = useKeyStoragePanelViewModel();
let content: JSX.Element;
switch (state) {
case "main":
switch (deviceState) {
// some device states require action from the user rather than showing the main settings screen
case "verify_this_session":
content = <SetUpEncryptionPanel onFinish={() => setState("main")} />;
break;
case "key_storage_out_of_sync":
content = (
<RecoveryPanelOutOfSync
onFinish={() => setState("main")}
onForgotRecoveryKey={() => setState("reset_identity_forgot")}
onAccessSecretStorageFailed={async () => {
const needsCrossSigningReset =
await DeviceListener.sharedInstance().keyStorageOutOfSyncNeedsCrossSigningReset(
true,
);
setState(needsCrossSigningReset ? "reset_identity_sync_failed" : "change_recovery_key");
}}
/>
);
break;
case "identity_needs_reset":
content = (
<IdentityNeedsResetNoticePanel onContinue={() => setState("reset_identity_cant_recover")} />
);
break;
default:
content = (
<>
<KeyStoragePanel onKeyStorageDisableClick={() => setState("key_storage_delete")} />
<Separator kind="section" />
{/* We only show the "Recovery" panel if key storage is enabled.*/}
{isBackupEnabled && (
<>
<RecoveryPanel
onChangeRecoveryKeyClick={(setupNewKey) =>
setupNewKey ? setState("set_recovery_key") : setState("change_recovery_key")
}
/>
<Separator kind="section" />
</>
)}
<AdvancedPanel onResetIdentityClick={() => setState("reset_identity_compromised")} />
</>
);
break;
}
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_compromised":
case "reset_identity_forgot":
case "reset_identity_sync_failed":
case "reset_identity_cant_recover":
content = (
<ResetIdentityPanel
variant={findResetVariant(state)}
onCancelClick={() => setState("main")}
onReset={() => setState("main")}
/>
);
break;
case "key_storage_delete":
content = <DeleteKeyStoragePanel onFinish={() => setState("main")} />;
break;
}
return (
<SettingsTab className="mx_EncryptionUserSettingsTab" data-testid="encryptionTab">
{content}
</SettingsTab>
);
}
/**
* Given what state we want the tab to be in, what variant of the
* ResetIdentityPanel do we need?
*/
function findResetVariant(state: State): ResetIdentityBodyVariant {
switch (state) {
case "reset_identity_compromised":
return "compromised";
case "reset_identity_sync_failed":
return "sync_failed";
case "reset_identity_cant_recover":
return "no_verification_method";
default:
case "reset_identity_forgot":
return "forgot";
}
}
interface SetUpEncryptionPanelProps {
/**
* Callback to call when the user has finished setting up encryption.
*/
onFinish: () => void;
}
/**
* Panel to show when the user needs to go through the SetupEncryption flow.
*/
function SetUpEncryptionPanel({ onFinish }: SetUpEncryptionPanelProps): JSX.Element {
// Strictly speaking, the SetupEncryptionDialog may make the user do things other than
// verify their device (in particular, if they manage to get here without cross-signing keys existing);
// however the common case is that they will be asked to verify, so we just show buttons and headings
// that talk about verification.
return (
<SettingsSection
legacy={false}
heading={_t("settings|encryption|device_not_verified_title")}
subHeading={
<SettingsSubheader
stateMessage={_t("settings|encryption|device_not_verified_description")}
state="error"
/>
}
>
<Button
size="sm"
Icon={ComputerIcon}
onClick={() => {
const { finished } = Modal.createDialog(SetupEncryptionDialog);
finished.then(onFinish);
}}
>
{_t("settings|encryption|device_not_verified_button")}
</Button>
</SettingsSection>
);
}
interface IdentityNeedsResetNoticePanelProps {
/**
* Callback to call when the user has finished setting up encryption.
*/
onContinue: () => void;
}
/**
* Panel to tell the user that they need to reset their identity.
*/
function IdentityNeedsResetNoticePanel({ onContinue }: Readonly<IdentityNeedsResetNoticePanelProps>): JSX.Element {
return (
<SettingsSection
legacy={false}
heading={_t("encryption|key_storage_out_of_sync")}
subHeading={
<SettingsSubheader state="error" stateMessage={_t("encryption|identity_needs_reset_description")} />
}
>
<div>
<Button size="sm" kind="primary" onClick={onContinue}>
{_t("encryption|continue_with_reset")}
</Button>
</div>
</SettingsSection>
);
}