Add Advanced section to the user settings encryption tab (#28804)
* Make the encryption card more configurable: - Change the icon - Can set the destructive props * Update compound * Add advanced section * Add the `Never send encrypted messages to unverified devices` settings * - Add commercial license - Remove generic type * Rename EncryptionDetails css classes * Use same uiAuthCallback * Use h3 for title * Add tests to `AdvancedPanel` * Add tests to `EncryptionUserSettingsTab` * Add tests to `ResetIdentityPanel` * Get only the recovery section in recovery tests * Add e2e test
This commit is contained in:
@@ -31,49 +31,50 @@ export async function createCrossSigning(cli: MatrixClient): Promise<void> {
|
||||
throw new Error("No crypto API found!");
|
||||
}
|
||||
|
||||
const doBootstrapUIAuth = async (
|
||||
makeRequest: (authData: AuthDict) => Promise<UIAResponse<void>>,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await makeRequest({});
|
||||
} catch (error) {
|
||||
if (!(error instanceof MatrixError) || !error.data || !error.data.flows) {
|
||||
// Not a UIA response
|
||||
throw error;
|
||||
}
|
||||
|
||||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
title: _t("auth|uia|sso_title"),
|
||||
body: _t("auth|uia|sso_preauth_body"),
|
||||
continueText: _t("auth|sso"),
|
||||
continueKind: "primary",
|
||||
},
|
||||
[SSOAuthEntry.PHASE_POSTAUTH]: {
|
||||
title: _t("encryption|confirm_encryption_setup_title"),
|
||||
body: _t("encryption|confirm_encryption_setup_body"),
|
||||
continueText: _t("action|confirm"),
|
||||
continueKind: "primary",
|
||||
},
|
||||
};
|
||||
|
||||
const { finished } = Modal.createDialog(InteractiveAuthDialog, {
|
||||
title: _t("encryption|bootstrap_title"),
|
||||
matrixClient: cli,
|
||||
makeRequest,
|
||||
aestheticsForStagePhases: {
|
||||
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
|
||||
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
|
||||
},
|
||||
});
|
||||
const [confirmed] = await finished;
|
||||
if (!confirmed) {
|
||||
throw new Error("Cross-signing key upload auth canceled");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await cryptoApi.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: doBootstrapUIAuth,
|
||||
authUploadDeviceSigningKeys: (makeRequest) => uiAuthCallback(cli, makeRequest),
|
||||
});
|
||||
}
|
||||
|
||||
export async function uiAuthCallback(
|
||||
matrixClient: MatrixClient,
|
||||
makeRequest: (authData: AuthDict) => Promise<UIAResponse<void>>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await makeRequest({});
|
||||
} catch (error) {
|
||||
if (!(error instanceof MatrixError) || !error.data || !error.data.flows) {
|
||||
// Not a UIA response
|
||||
throw error;
|
||||
}
|
||||
|
||||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
title: _t("auth|uia|sso_title"),
|
||||
body: _t("auth|uia|sso_preauth_body"),
|
||||
continueText: _t("auth|sso"),
|
||||
continueKind: "primary",
|
||||
},
|
||||
[SSOAuthEntry.PHASE_POSTAUTH]: {
|
||||
title: _t("encryption|confirm_encryption_setup_title"),
|
||||
body: _t("encryption|confirm_encryption_setup_body"),
|
||||
continueText: _t("action|confirm"),
|
||||
continueKind: "primary",
|
||||
},
|
||||
};
|
||||
|
||||
const { finished } = Modal.createDialog(InteractiveAuthDialog, {
|
||||
title: _t("encryption|bootstrap_title"),
|
||||
matrixClient,
|
||||
makeRequest,
|
||||
aestheticsForStagePhases: {
|
||||
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
|
||||
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
|
||||
},
|
||||
});
|
||||
const [confirmed] = await finished;
|
||||
if (!confirmed) {
|
||||
throw new Error("Cross-signing key upload auth canceled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
139
src/components/views/settings/encryption/AdvancedPanel.tsx
Normal file
139
src/components/views/settings/encryption/AdvancedPanel.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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, { JSX, lazy, MouseEventHandler } from "react";
|
||||
import { Button, HelpMessage, InlineField, InlineSpinner, Label, Root, ToggleControl } 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";
|
||||
import { SettingLevel } from "../../../../settings/SettingLevel";
|
||||
import { useSettingValueAt } from "../../../../hooks/useSettings";
|
||||
import SettingsStore from "../../../../settings/SettingsStore";
|
||||
|
||||
interface AdvancedPanelProps {
|
||||
/**
|
||||
* Callback for when the user clicks the button to reset their identity.
|
||||
*/
|
||||
onResetIdentityClick: MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The advanced panel of the encryption settings.
|
||||
*/
|
||||
export function AdvancedPanel({ onResetIdentityClick }: AdvancedPanelProps): JSX.Element {
|
||||
return (
|
||||
<SettingsSection heading={_t("settings|encryption|advanced|title")} legacy={false}>
|
||||
<EncryptionDetails onResetIdentityClick={onResetIdentityClick} />
|
||||
<OtherSettings />
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
interface EncryptionDetails {
|
||||
/**
|
||||
* Callback for when the user clicks the button to reset their identity.
|
||||
*/
|
||||
onResetIdentityClick: MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(() => matrixClient.getCrypto()!.getOwnDeviceKeys(), [matrixClient], null);
|
||||
|
||||
return (
|
||||
<div className="mx_EncryptionDetails" data-testid="encryptionDetails">
|
||||
<div className="mx_EncryptionDetails_session">
|
||||
<h3 className="mx_EncryptionDetails_session_title">
|
||||
{_t("settings|encryption|advanced|details_title")}
|
||||
</h3>
|
||||
<div>
|
||||
<span>{_t("settings|encryption|advanced|session_id")}</span>
|
||||
<span data-testid="deviceId">{matrixClient.deviceId}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{_t("settings|encryption|advanced|session_key")}</span>
|
||||
<span data-testid="sessionKey">
|
||||
{keys ? keys.ed25519 : <InlineSpinner aria-label={_t("common|loading")} />}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_EncryptionDetails_buttons">
|
||||
<Button
|
||||
size="sm"
|
||||
kind="secondary"
|
||||
Icon={ShareIcon}
|
||||
onClick={() =>
|
||||
Modal.createDialog(
|
||||
lazy(
|
||||
() => import("../../../../async-components/views/dialogs/security/ExportE2eKeysDialog"),
|
||||
),
|
||||
{ matrixClient },
|
||||
)
|
||||
}
|
||||
>
|
||||
{_t("settings|encryption|advanced|export_keys")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
kind="secondary"
|
||||
Icon={DownloadIcon}
|
||||
onClick={() =>
|
||||
Modal.createDialog(
|
||||
lazy(
|
||||
() => import("../../../../async-components/views/dialogs/security/ImportE2eKeysDialog"),
|
||||
),
|
||||
{ matrixClient },
|
||||
)
|
||||
}
|
||||
>
|
||||
{_t("settings|encryption|advanced|import_keys")}
|
||||
</Button>
|
||||
</div>
|
||||
<Button size="sm" kind="tertiary" destructive={true} onClick={onResetIdentityClick}>
|
||||
{_t("settings|encryption|advanced|reset_identity")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the never send encrypted message to unverified devices setting.
|
||||
*/
|
||||
function OtherSettings(): JSX.Element | null {
|
||||
const blacklistUnverifiedDevices = useSettingValueAt(SettingLevel.DEVICE, "blacklistUnverifiedDevices");
|
||||
const canSetValue = SettingsStore.canSetValue("blacklistUnverifiedDevices", null, SettingLevel.DEVICE);
|
||||
if (!canSetValue) return null;
|
||||
|
||||
return (
|
||||
<Root
|
||||
data-testid="otherSettings"
|
||||
className="mx_OtherSettings"
|
||||
onChange={async (evt) => {
|
||||
const checked = new FormData(evt.currentTarget).get("neverSendEncrypted") === "on";
|
||||
await SettingsStore.setValue("blacklistUnverifiedDevices", null, SettingLevel.DEVICE, checked);
|
||||
}}
|
||||
>
|
||||
<h3 className="mx_OtherSettings_title">{_t("settings|encryption|advanced|other_people_device_title")}</h3>
|
||||
<InlineField
|
||||
name="neverSendEncrypted"
|
||||
control={<ToggleControl name="neverSendEncrypted" defaultChecked={blacklistUnverifiedDevices} />}
|
||||
>
|
||||
<Label>{_t("settings|encryption|advanced|other_people_device_label")}</Label>
|
||||
<HelpMessage>{_t("settings|encryption|advanced|other_people_device_description")}</HelpMessage>
|
||||
</InlineField>
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
TextControl,
|
||||
} from "@vector-im/compound-web";
|
||||
import CopyIcon from "@vector-im/compound-design-tokens/assets/web/icons/copy";
|
||||
import KeyIcon from "@vector-im/compound-design-tokens/assets/web/icons/key-solid";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
@@ -157,7 +158,12 @@ export function ChangeRecoveryKey({
|
||||
pages={pages}
|
||||
onPageClick={onCancelClick}
|
||||
/>
|
||||
<EncryptionCard title={labels.title} description={labels.description} className="mx_ChangeRecoveryKey">
|
||||
<EncryptionCard
|
||||
Icon={KeyIcon}
|
||||
title={labels.title}
|
||||
description={labels.description}
|
||||
className="mx_ChangeRecoveryKey"
|
||||
>
|
||||
{content}
|
||||
</EncryptionCard>
|
||||
</>
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { JSX, PropsWithChildren } from "react";
|
||||
import React, { JSX, PropsWithChildren, ComponentType, SVGAttributes } from "react";
|
||||
import { BigIcon, Heading } from "@vector-im/compound-web";
|
||||
import KeyIcon from "@vector-im/compound-design-tokens/assets/web/icons/key-solid";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface EncryptionCardProps {
|
||||
@@ -22,7 +21,15 @@ interface EncryptionCardProps {
|
||||
/**
|
||||
* The description of the card.
|
||||
*/
|
||||
description: string;
|
||||
description?: string;
|
||||
/**
|
||||
* Whether this icon shows a destructive action.
|
||||
*/
|
||||
destructive?: boolean;
|
||||
/**
|
||||
* The icon to display.
|
||||
*/
|
||||
Icon: ComponentType<SVGAttributes<SVGElement>>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,18 +39,20 @@ export function EncryptionCard({
|
||||
title,
|
||||
description,
|
||||
className,
|
||||
destructive = false,
|
||||
Icon,
|
||||
children,
|
||||
}: PropsWithChildren<EncryptionCardProps>): JSX.Element {
|
||||
return (
|
||||
<div className={classNames("mx_EncryptionCard", className)}>
|
||||
<div className="mx_EncryptionCard_header">
|
||||
<BigIcon>
|
||||
<KeyIcon />
|
||||
<BigIcon destructive={destructive}>
|
||||
<Icon />
|
||||
</BigIcon>
|
||||
<Heading as="h2" size="sm" weight="semibold">
|
||||
{title}
|
||||
</Heading>
|
||||
<span>{description}</span>
|
||||
{description && <span>{description}</span>}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -106,6 +106,7 @@ export function RecoveryPanel({ onChangeRecoveryKeyClick }: RecoveryPanelProps):
|
||||
/>
|
||||
}
|
||||
subHeading={<Subheader state={state} />}
|
||||
data-testid="recoveryPanel"
|
||||
>
|
||||
{content}
|
||||
</SettingsSection>
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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 { 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 { _t } from "../../../../languageHandler";
|
||||
import { EncryptionCard } from "./EncryptionCard";
|
||||
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
||||
import { uiAuthCallback } from "../../../../CreateCrossSigning";
|
||||
|
||||
interface ResetIdentityPanelProps {
|
||||
/**
|
||||
* Called when the identity is reset.
|
||||
*/
|
||||
onFinish: MouseEventHandler<HTMLButtonElement>;
|
||||
/**
|
||||
* 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 (
|
||||
<>
|
||||
<Breadcrumb
|
||||
backLabel={_t("action|back")}
|
||||
onBackClick={onCancelClick}
|
||||
pages={[_t("settings|encryption|title"), _t("settings|encryption|advanced|breadcrumb_page")]}
|
||||
onPageClick={onCancelClick}
|
||||
/>
|
||||
<EncryptionCard
|
||||
Icon={ErrorIcon}
|
||||
destructive={true}
|
||||
title={_t("settings|encryption|advanced|breadcrumb_title")}
|
||||
className="mx_ResetIdentityPanel"
|
||||
>
|
||||
<div className="mx_ResetIdentityPanel_content">
|
||||
<VisualList>
|
||||
<VisualListItem Icon={CheckIcon} success={true}>
|
||||
{_t("settings|encryption|advanced|breadcrumb_first_description")}
|
||||
</VisualListItem>
|
||||
<VisualListItem Icon={InfoIcon}>
|
||||
{_t("settings|encryption|advanced|breadcrumb_second_description")}
|
||||
</VisualListItem>
|
||||
<VisualListItem Icon={InfoIcon}>
|
||||
{_t("settings|encryption|advanced|breadcrumb_third_description")}
|
||||
</VisualListItem>
|
||||
</VisualList>
|
||||
<span>{_t("settings|encryption|advanced|breadcrumb_warning")}</span>
|
||||
</div>
|
||||
<div className="mx_ResetIdentityPanel_footer">
|
||||
<Button
|
||||
destructive={true}
|
||||
onClick={async (evt) => {
|
||||
await matrixClient
|
||||
.getCrypto()
|
||||
?.resetEncryption((makeRequest) => uiAuthCallback(matrixClient, makeRequest));
|
||||
onFinish(evt);
|
||||
}}
|
||||
>
|
||||
{_t("action|continue")}
|
||||
</Button>
|
||||
<Button kind="tertiary" onClick={onCancelClick}>
|
||||
{_t("action|cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</EncryptionCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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.
|
||||
@@ -29,8 +31,9 @@ import { SettingsSubheader } from "../../SettingsSubheader";
|
||||
* 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": The panel to show when the user is resetting their identity.
|
||||
*/
|
||||
type State = "loading" | "main" | "set_up_encryption" | "change_recovery_key" | "set_recovery_key";
|
||||
type State = "loading" | "main" | "set_up_encryption" | "change_recovery_key" | "set_recovery_key" | "reset_identity";
|
||||
|
||||
export function EncryptionUserSettingsTab(): JSX.Element {
|
||||
const [state, setState] = useState<State>("loading");
|
||||
@@ -46,11 +49,15 @@ export function EncryptionUserSettingsTab(): JSX.Element {
|
||||
break;
|
||||
case "main":
|
||||
content = (
|
||||
<RecoveryPanel
|
||||
onChangeRecoveryKeyClick={(setupNewKey) =>
|
||||
setupNewKey ? setState("set_recovery_key") : setState("change_recovery_key")
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<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":
|
||||
@@ -63,6 +70,9 @@ export function EncryptionUserSettingsTab(): JSX.Element {
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "reset_identity":
|
||||
content = <ResetIdentityPanel onCancelClick={() => setState("main")} onFinish={() => setState("main")} />;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -2422,6 +2422,24 @@
|
||||
"enable_markdown": "Enable Markdown",
|
||||
"enable_markdown_description": "Start messages with <code>/plain</code> 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",
|
||||
"other_people_device_description": "By default in encrypted rooms, do not send encrypted messages to anyone until you’ve verified them",
|
||||
"other_people_device_label": "Never send encrypted messages to unverified devices",
|
||||
"other_people_device_title": "Other people’s devices",
|
||||
"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",
|
||||
|
||||
Reference in New Issue
Block a user