pull/28804/merge
Florian Duros 2025-01-08 13:46:57 +00:00 committed by GitHub
commit 53b4cafd30
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 444 additions and 20 deletions

View File

@ -91,7 +91,7 @@
"@sentry/browser": "^8.0.0",
"@types/png-chunks-extract": "^1.0.2",
"@vector-im/compound-design-tokens": "^2.0.1",
"@vector-im/compound-web": "^7.5.0",
"@vector-im/compound-web": "^7.6.1",
"@vector-im/matrix-wysiwyg": "2.38.0",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",

View File

@ -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";

View File

@ -0,0 +1,50 @@
/*
* 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.
*/
.mx_AdvancedPanel_Details,
.mx_OtherSettings {
display: flex;
flex-direction: column;
gap: var(--cpd-space-6x);
width: 100%;
align-items: start;
.mx_AdvancedPanel_Details_title,
.mx_OtherSettings_title {
font: var(--cpd-font-body-lg-semibold);
padding-bottom: var(--cpd-space-2x);
border-bottom: 1px solid var(--cpd-color-gray-400);
width: 100%;
}
}
.mx_AdvancedPanel_Details {
.mx_AdvancedPanel_Details_content {
display: flex;
flex-direction: column;
gap: var(--cpd-space-4x);
width: 100%;
> 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);
}
}

View File

@ -0,0 +1,38 @@
/*
* 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.
*/
.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;
}
}

View File

@ -0,0 +1,146 @@
/*
* 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(
() => {
const crypto = matrixClient.getCrypto();
return crypto ? crypto.getOwnDeviceKeys() : Promise.resolve(null);
},
[matrixClient],
null,
);
return (
<div className="mx_AdvancedPanel_Details">
<div className="mx_AdvancedPanel_Details_content">
<span className="mx_AdvancedPanel_Details_title">
{_t("settings|encryption|advanced|details_title")}
</span>
<div>
<span>{_t("settings|encryption|advanced|session_id")}</span>
<span>{matrixClient.deviceId}</span>
</div>
<div>
<span>{_t("settings|encryption|advanced|session_key")}</span>
<span>{keys ? keys.ed25519 : <InlineSpinner />}</span>
</div>
</div>
<div className="mx_AdvancedPanel_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
className="mx_OtherSettings"
onChange={async (evt) => {
const checked = new FormData(evt.currentTarget).get("neverSendEncrypted") === "on";
await SettingsStore.setValue("blacklistUnverifiedDevices", null, SettingLevel.DEVICE, checked);
}}
>
<span className="mx_OtherSettings_title">
{_t("settings|encryption|advanced|other_people_device_title")}
</span>
<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>
);
}

View File

@ -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";
@ -132,7 +133,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>
</>

View File

@ -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>

View File

@ -0,0 +1,144 @@
/*
* 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 { AuthDict, MatrixClient, UIAResponse } 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";
import { SSOAuthEntry } from "../../auth/InteractiveAuthEntryComponents";
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 resetIdentity(matrixClient);
onFinish(evt);
}}
>
{_t("action|continue")}
</Button>
<Button kind="tertiary" onClick={onCancelClick}>
{_t("action|cancel")}
</Button>
</div>
</EncryptionCard>
</>
);
}
/**
* Resets the identity of the current user.
*/
async function resetIdentity(matrixClient: MatrixClient): Promise<void> {
const crypto = matrixClient.getCrypto();
if (!crypto) return;
await withSecretStorageKeyCache(async () => {
await crypto.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async (makeRequest): Promise<void> =>
uiAuthCallback(matrixClient, makeRequest),
setupNewCrossSigning: true,
});
await crypto.bootstrapSecretStorage({
setupNewKeyBackup: true,
});
});
}
/**
* Handles the UIA flow for resetting the identity.
* @param matrixClient
* @param makeRequest
*/
async function uiAuthCallback(
matrixClient: MatrixClient,
makeRequest: (authData: AuthDict) => Promise<UIAResponse<void>>,
): Promise<void> {
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");
}
}

View File

@ -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 key backup 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 backup 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 (

View File

@ -2463,6 +2463,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 thats 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 youve verified them",
"other_people_device_label": "Never send encrypted messages to unverified devices",
"other_people_device_title": "Other peoples 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",

View File

@ -7,13 +7,14 @@
import React from "react";
import { render } from "jest-matrix-react";
import KeyIcon from "@vector-im/compound-design-tokens/assets/web/icons/key-solid";
import { EncryptionCard } from "../../../../../../src/components/views/settings/encryption/EncryptionCard";
describe("<EncryptionCard />", () => {
it("should render", () => {
const { asFragment } = render(
<EncryptionCard title="My title" description="My description">
<EncryptionCard Icon={KeyIcon} title="My title" description="My description">
Encryption card children
</EncryptionCard>,
);

View File

@ -3493,10 +3493,10 @@
resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-2.1.1.tgz#d6175a99fe4b97688464126f255386990f3048d6"
integrity sha512-QnUi2K14D9KTXxcLQKUU3V75cforZLMwhaaJDNftT8F5mG86950hAM+qhgDNEpEU+pkTffQj0/g/5859YmqWzQ==
"@vector-im/compound-web@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.5.0.tgz#1547af5f0ee27b94f79ab11eee006059f3d09707"
integrity sha512-Xhef8H5WrRmPuanzRBs8rnl+hwbcQnC7nKSCupUczAQ5hjlieBx4vcQYQ/nMkrs4rMGjgfFtR3E18wT5LlML/A==
"@vector-im/compound-web@^7.6.1":
version "7.6.1"
resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.6.1.tgz#c41fc8b2e4c5938041e1f0ff9792f8fbadd9ab87"
integrity sha512-LdHGFslkyky2aNPZwIOY9GgWn1VOUa2EBKHln8HBvpxnYPcs3/A2nb1+6SsJ7+Y0TzKc2HA0rZ3qPDhQ3hjZYQ==
dependencies:
"@floating-ui/react" "^0.27.0"
"@radix-ui/react-context-menu" "^2.2.1"