Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Advanced section to the user settings encryption tab #28804

Draft
wants to merge 11 commits into
base: develop
Choose a base branch
from
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"@types/png-chunks-extract": "^1.0.2",
"@types/react-virtualized": "^9.21.30",
"@vector-im/compound-design-tokens": "^2.1.0",
"@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",
Expand Down
2 changes: 2 additions & 0 deletions res/css/_components.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -353,8 +353,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";
Expand Down
51 changes: 51 additions & 0 deletions res/css/views/settings/encryption/_AdvancedPanel.pcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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_EncryptionDetails,
.mx_OtherSettings {
display: flex;
flex-direction: column;
gap: var(--cpd-space-6x);
width: 100%;
align-items: start;

.mx_EncryptionDetails_session_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%;
margin: 0;
}
}

.mx_EncryptionDetails {
.mx_EncryptionDetails_session {
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_EncryptionDetails_buttons {
display: flex;
gap: var(--cpd-space-4x);
}
}
38 changes: 38 additions & 0 deletions res/css/views/settings/encryption/_ResetIdentityPanel.pcss
Original file line number Diff line number Diff line change
@@ -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;
}
}
85 changes: 43 additions & 42 deletions src/CreateCrossSigning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
};
await cryptoApi.bootstrapCrossSigning({
authUploadDeviceSigningKeys: (makeRequest) => uiAuthCallback(cli, makeRequest),
});
}

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");
}
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;
}
};

await cryptoApi.bootstrapCrossSigning({
authUploadDeviceSigningKeys: doBootstrapUIAuth,
});
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");
}
}
}
144 changes: 144 additions & 0 deletions src/components/views/settings/encryption/AdvancedPanel.tsx
Original file line number Diff line number Diff line change
@@ -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 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_EncryptionDetails" data-testid="encryption-details">
<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>{matrixClient.deviceId}</span>
</div>
<div>
<span>{_t("settings|encryption|advanced|session_key")}</span>
<span>{keys ? keys.ed25519 : <InlineSpinner />}</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="other-settings"
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>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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>
</>
Expand Down
Loading
Loading