diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 805b6056e95..63d19cf6c60 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -352,8 +352,10 @@ @import "./views/settings/_ThemeChoicePanel.pcss"; @import "./views/settings/_UpdateCheckButton.pcss"; @import "./views/settings/_UserProfileSettings.pcss"; +@import "./views/settings/encryption/_AdvancedPanel.pcss"; @import "./views/settings/encryption/_ChangeRecoveryKey.pcss"; @import "./views/settings/encryption/_EncryptionCard.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"; diff --git a/res/css/views/settings/encryption/_AdvancedPanel.pcss b/res/css/views/settings/encryption/_AdvancedPanel.pcss new file mode 100644 index 00000000000..46cccac5d44 --- /dev/null +++ b/res/css/views/settings/encryption/_AdvancedPanel.pcss @@ -0,0 +1,45 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only + * Please see LICENSE files in the repository root for full details. + */ + +.mx_AdvancedPanel_Details { + display: flex; + flex-direction: column; + gap: var(--cpd-space-6x); + width: 100%; + align-items: start; + + .mx_AdvancedPanel_Details_content { + display: flex; + flex-direction: column; + gap: var(--cpd-space-4x); + width: 100%; + + > span { + font: var(--cpd-font-body-lg-semibold); + padding-bottom: var(--cpd-space-2x); + border-bottom: 1px solid var(--cpd-color-gray-400); + } + + > div { + display: flex; + + > span { + width: 50%; + word-wrap: break-word; + } + } + + > div:nth-child(odd) { + background-color: var(--cpd-color-gray-200); + } + } + + .mx_AdvancedPanel_buttons { + display: flex; + gap: var(--cpd-space-4x); + } +} diff --git a/res/css/views/settings/encryption/_ResetIdentityPanel.pcss b/res/css/views/settings/encryption/_ResetIdentityPanel.pcss new file mode 100644 index 00000000000..b09d4f420cc --- /dev/null +++ b/res/css/views/settings/encryption/_ResetIdentityPanel.pcss @@ -0,0 +1,38 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only + * Please see LICENSE files in the repository root for full details. + */ + +.mx_ResetIdentityPanel { + .mx_ResetIdentityPanel_content { + display: flex; + flex-direction: column; + gap: var(--cpd-space-3x); + + > ul { + margin: 0; + list-style-type: none; + display: flex; + flex-direction: column; + gap: var(--cpd-space-1x); + + > li { + padding: var(--cpd-space-2x) var(--cpd-space-3x); + } + } + + > span { + font: var(--cpd-font-body-md-medium); + text-align: center; + } + } + + .mx_ResetIdentityPanel_footer { + display: flex; + flex-direction: column; + gap: var(--cpd-space-4x); + justify-content: center; + } +} diff --git a/src/components/views/settings/encryption/AdvancedPanel.tsx b/src/components/views/settings/encryption/AdvancedPanel.tsx new file mode 100644 index 00000000000..96b3031b3b5 --- /dev/null +++ b/src/components/views/settings/encryption/AdvancedPanel.tsx @@ -0,0 +1,109 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only + * Please see LICENSE files in the repository root for full details. + */ + +import React, { JSX, lazy, MouseEventHandler } from "react"; +import { Button, InlineSpinner } from "@vector-im/compound-web"; +import DownloadIcon from "@vector-im/compound-design-tokens/assets/web/icons/download"; +import ShareIcon from "@vector-im/compound-design-tokens/assets/web/icons/share"; + +import { _t } from "../../../../languageHandler"; +import { SettingsSection } from "../shared/SettingsSection"; +import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; +import { useAsyncMemo } from "../../../../hooks/useAsyncMemo"; +import Modal from "../../../../Modal"; + +interface AdvancedPanelProps { + /** + * Callback for when the user clicks the button to reset their identity. + */ + onResetIdentityClick: MouseEventHandler; +} + +/** + * The advanced panel of the encryption settings. + */ +export function AdvancedPanel({ onResetIdentityClick }: AdvancedPanelProps): JSX.Element { + return ( + + + + ); +} + +interface EncryptionDetails { + /** + * Callback for when the user clicks the button to reset their identity. + */ + onResetIdentityClick: MouseEventHandler; +} + +/** + * The encryption details section of the advanced panel. + */ +function EncryptionDetails({ onResetIdentityClick }: EncryptionDetails): JSX.Element { + const matrixClient = useMatrixClientContext(); + // Null when the keys are not loaded yet + const keys = useAsyncMemo( + () => { + const crypto = matrixClient.getCrypto(); + return crypto ? crypto.getOwnDeviceKeys() : Promise.resolve(null); + }, + [matrixClient], + null, + ); + + return ( +
+
+ {_t("settings|encryption|advanced|details_title")} +
+ {_t("settings|encryption|advanced|session_id")} + {matrixClient.deviceId} +
+
+ {_t("settings|encryption|advanced|session_key")} + {keys ? keys.ed25519 : } +
+
+
+ + +
+ +
+ ); +} diff --git a/src/components/views/settings/encryption/ResetIdentityPanel.tsx b/src/components/views/settings/encryption/ResetIdentityPanel.tsx new file mode 100644 index 00000000000..8526fdf57e8 --- /dev/null +++ b/src/components/views/settings/encryption/ResetIdentityPanel.tsx @@ -0,0 +1,106 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only + * Please see LICENSE files in the repository root for full details. + */ + +import { Breadcrumb, Button, VisualList, VisualListItem } from "@vector-im/compound-web"; +import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check"; +import InfoIcon from "@vector-im/compound-design-tokens/assets/web/icons/info"; +import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error"; +import React, { MouseEventHandler } from "react"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; + +import { _t } from "../../../../languageHandler"; +import { EncryptionCard } from "./EncryptionCard"; +import { withSecretStorageKeyCache } from "../../../../SecurityManager"; +import Modal from "../../../../Modal"; +import InteractiveAuthDialog from "../../dialogs/InteractiveAuthDialog"; +import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; + +interface ResetIdentityPanelProps { + /** + * Called when the identity is reset. + */ + onFinish: MouseEventHandler; + /** + * Called when the cancel button is clicked or when we go back in the breadcrumbs. + */ + onCancelClick: () => void; +} + +/** + * The panel for resetting the identity of the current user. + */ +export function ResetIdentityPanel({ onCancelClick, onFinish }: ResetIdentityPanelProps): JSX.Element { + const matrixClient = useMatrixClientContext(); + + return ( + <> + + +
+ + + {_t("settings|encryption|advanced|breadcrumb_first_description")} + + + {_t("settings|encryption|advanced|breadcrumb_second_description")} + + + {_t("settings|encryption|advanced|breadcrumb_third_description")} + + + {_t("settings|encryption|advanced|breadcrumb_warning")} +
+
+ + +
+
+ + ); +} + +/** + * Resets the identity of the current user. + */ +async function resetIdentity(matrixClient: MatrixClient): Promise { + await withSecretStorageKeyCache(async () => { + await matrixClient.getCrypto()!.bootstrapCrossSigning({ + authUploadDeviceSigningKeys: async (makeRequest): Promise => { + const { finished } = Modal.createDialog(InteractiveAuthDialog, { + title: _t("encryption|bootstrap_title"), + matrixClient: matrixClient, + makeRequest, + }); + const [confirmed] = await finished; + if (!confirmed) { + throw new Error("Cross-signing key upload auth canceled"); + } + }, + setupNewCrossSigning: true, + }); + }); +} diff --git a/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx b/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx index 97e6ed1ab53..b66859f0d08 100644 --- a/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @@ -6,7 +6,7 @@ */ import React, { JSX, useCallback, useEffect, useState } from "react"; -import { Button, InlineSpinner } from "@vector-im/compound-web"; +import { Button, InlineSpinner, Separator } from "@vector-im/compound-web"; import ComputerIcon from "@vector-im/compound-design-tokens/assets/web/icons/computer"; import SettingsTab from "../SettingsTab"; @@ -18,6 +18,8 @@ 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"; /** * The state in the encryption settings tab. @@ -26,8 +28,15 @@ import { SettingsSubheader } from "../../SettingsSubheader"; * - "verification_required": The panel to show when the user needs to verify their session. * - "change_recovery_key": The panel to show when the user is changing their recovery key. * - "set_recovery_key": The panel to show when the user is setting up their recovery key. + * - "reset_identity": The panel to show when the user is resetting their identity. */ -type State = "loading" | "main" | "verification_required" | "change_recovery_key" | "set_recovery_key"; +type State = + | "loading" + | "main" + | "verification_required" + | "change_recovery_key" + | "set_recovery_key" + | "reset_identity"; export function EncryptionUserSettingsTab(): JSX.Element { const [state, setState] = useState("loading"); @@ -43,10 +52,14 @@ export function EncryptionUserSettingsTab(): JSX.Element { break; case "main": content = ( - setState("change_recovery_key")} - onSetUpRecoveryClick={() => setState("set_recovery_key")} - /> + <> + setState("change_recovery_key")} + onSetUpRecoveryClick={() => setState("set_recovery_key")} + /> + + setState("reset_identity")} /> + ); break; case "change_recovery_key": @@ -59,6 +72,9 @@ export function EncryptionUserSettingsTab(): JSX.Element { /> ); break; + case "reset_identity": + content = setState("main")} onFinish={() => setState("main")} />; + break; } return ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e0052fbf563..591b834be8f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2464,6 +2464,21 @@ "enable_markdown": "Enable Markdown", "enable_markdown_description": "Start messages with /plain to send without markdown.", "encryption": { + "advanced": { + "breadcrumb_first_description": "Your account details, contacts, preferences, and chat list will be kept", + "breadcrumb_page": "Reset encryption", + "breadcrumb_second_description": "You will lose any message history that’s stored only on the server", + "breadcrumb_third_description": "You will need to verify all your existing devices and contacts again", + "breadcrumb_title": "Are you sure you want to reset your identity?", + "breadcrumb_warning": "Only do this if you believe your account has been compromised.", + "details_title": "Encryption details", + "export_keys": "Export keys", + "import_keys": "Import keys", + "reset_identity": "Reset cryptographic identity", + "session_id": "Session ID:", + "session_key": "Session key:", + "title": "Advanced" + }, "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",