/* Copyright 2024 New Vector Ltd. Copyright 2019, 2020 The Matrix.org Foundation C.I.C. 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 from "react"; import { ClientEvent, EmptyObject, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { CryptoEvent } from "matrix-js-sdk/src/crypto-api"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; import Spinner from "../elements/Spinner"; import InteractiveAuthDialog from "../dialogs/InteractiveAuthDialog"; import ConfirmDestroyCrossSigningDialog from "../dialogs/security/ConfirmDestroyCrossSigningDialog"; import SetupEncryptionDialog from "../dialogs/security/SetupEncryptionDialog"; import { accessSecretStorage, withSecretStorageKeyCache } from "../../../SecurityManager"; import AccessibleButton from "../elements/AccessibleButton"; import { SettingsSubsectionText } from "./shared/SettingsSubsection"; interface IState { error: boolean; crossSigningPublicKeysOnDevice?: boolean; crossSigningPrivateKeysInStorage?: boolean; masterPrivateKeyCached?: boolean; selfSigningPrivateKeyCached?: boolean; userSigningPrivateKeyCached?: boolean; homeserverSupportsCrossSigning?: boolean; crossSigningReady?: boolean; } export default class CrossSigningPanel extends React.PureComponent { private unmounted = false; public constructor(props: EmptyObject) { super(props); this.state = { error: false, }; } public componentDidMount(): void { this.unmounted = false; const cli = MatrixClientPeg.safeGet(); cli.on(ClientEvent.AccountData, this.onAccountData); cli.on(CryptoEvent.UserTrustStatusChanged, this.onStatusChanged); cli.on(CryptoEvent.KeysChanged, this.onStatusChanged); this.getUpdatedStatus(); } public componentWillUnmount(): void { this.unmounted = true; const cli = MatrixClientPeg.get(); if (!cli) return; cli.removeListener(ClientEvent.AccountData, this.onAccountData); cli.removeListener(CryptoEvent.UserTrustStatusChanged, this.onStatusChanged); cli.removeListener(CryptoEvent.KeysChanged, this.onStatusChanged); } private onAccountData = (event: MatrixEvent): void => { const type = event.getType(); if (type.startsWith("m.cross_signing") || type.startsWith("m.secret_storage")) { this.getUpdatedStatus(); } }; private onBootstrapClick = (): void => { if (this.state.crossSigningPrivateKeysInStorage) { Modal.createDialog(SetupEncryptionDialog, {}, undefined, /* priority = */ false, /* static = */ true); } else { // Trigger the flow to set up secure backup, which is what this will do when in // the appropriate state. accessSecretStorage(); } }; private onStatusChanged = (): void => { this.getUpdatedStatus(); }; private async getUpdatedStatus(): Promise { const cli = MatrixClientPeg.safeGet(); const crypto = cli.getCrypto(); if (!crypto) return; const crossSigningStatus = await crypto.getCrossSigningStatus(); const crossSigningPublicKeysOnDevice = crossSigningStatus.publicKeysOnDevice; const crossSigningPrivateKeysInStorage = crossSigningStatus.privateKeysInSecretStorage; const masterPrivateKeyCached = crossSigningStatus.privateKeysCachedLocally.masterKey; const selfSigningPrivateKeyCached = crossSigningStatus.privateKeysCachedLocally.selfSigningKey; const userSigningPrivateKeyCached = crossSigningStatus.privateKeysCachedLocally.userSigningKey; const homeserverSupportsCrossSigning = await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"); const crossSigningReady = await crypto.isCrossSigningReady(); this.setState({ crossSigningPublicKeysOnDevice, crossSigningPrivateKeysInStorage, masterPrivateKeyCached, selfSigningPrivateKeyCached, userSigningPrivateKeyCached, homeserverSupportsCrossSigning, crossSigningReady, }); } /** * Reset the user's cross-signing keys. */ private async resetCrossSigning(): Promise { this.setState({ error: false }); try { const cli = MatrixClientPeg.safeGet(); await withSecretStorageKeyCache(async () => { await cli.getCrypto()!.bootstrapCrossSigning({ authUploadDeviceSigningKeys: async (makeRequest): Promise => { const { finished } = Modal.createDialog(InteractiveAuthDialog, { title: _t("encryption|bootstrap_title"), matrixClient: cli, makeRequest, }); const [confirmed] = await finished; if (!confirmed) { throw new Error("Cross-signing key upload auth canceled"); } }, setupNewCrossSigning: true, }); }); } catch (e) { this.setState({ error: true }); logger.error("Error bootstrapping cross-signing", e); } if (this.unmounted) return; this.getUpdatedStatus(); } /** * Callback for when the user clicks the "reset cross signing" button. * * Shows a confirmation dialog, and then does the reset if confirmed. */ private onResetCrossSigningClick = (): void => { Modal.createDialog(ConfirmDestroyCrossSigningDialog, { onFinished: async (act) => { if (!act) return; this.resetCrossSigning(); }, }); }; public render(): React.ReactNode { const { error, crossSigningPublicKeysOnDevice, crossSigningPrivateKeysInStorage, masterPrivateKeyCached, selfSigningPrivateKeyCached, userSigningPrivateKeyCached, homeserverSupportsCrossSigning, crossSigningReady, } = this.state; let errorSection; if (error) { errorSection =
{error.toString()}
; } let summarisedStatus; if (homeserverSupportsCrossSigning === undefined) { summarisedStatus = ; } else if (!homeserverSupportsCrossSigning) { summarisedStatus = ( {_t("encryption|cross_signing_unsupported")} ); } else if (crossSigningReady && crossSigningPrivateKeysInStorage) { summarisedStatus = ( ✅ {_t("encryption|cross_signing_ready")} ); } else if (crossSigningReady && !crossSigningPrivateKeysInStorage) { summarisedStatus = ( ⚠️ {_t("encryption|cross_signing_ready_no_backup")} ); } else if (crossSigningPrivateKeysInStorage) { summarisedStatus = ( {_t("encryption|cross_signing_untrusted")} ); } else { summarisedStatus = ( {_t("encryption|cross_signing_not_ready")} ); } const keysExistAnywhere = crossSigningPublicKeysOnDevice || crossSigningPrivateKeysInStorage || masterPrivateKeyCached || selfSigningPrivateKeyCached || userSigningPrivateKeyCached; const keysExistEverywhere = crossSigningPublicKeysOnDevice && crossSigningPrivateKeysInStorage && masterPrivateKeyCached && selfSigningPrivateKeyCached && userSigningPrivateKeyCached; const actions: JSX.Element[] = []; // TODO: determine how better to expose this to users in addition to prompts at login/toast if (!keysExistEverywhere && homeserverSupportsCrossSigning) { let buttonCaption = _t("encryption|set_up_toast_title"); if (crossSigningPrivateKeysInStorage) { buttonCaption = _t("encryption|verify_toast_title"); } actions.push( {buttonCaption} , ); } if (keysExistAnywhere) { actions.push( {_t("action|reset")} , ); } let actionRow; if (actions.length) { actionRow =
{actions}
; } return ( <> {summarisedStatus}
{_t("common|advanced")}
{_t("settings|security|cross_signing_public_keys")} {crossSigningPublicKeysOnDevice ? _t("settings|security|cross_signing_in_memory") : _t("settings|security|cross_signing_not_found")}
{_t("settings|security|cross_signing_private_keys")} {crossSigningPrivateKeysInStorage ? _t("settings|security|cross_signing_in_4s") : _t("settings|security|cross_signing_not_in_4s")}
{_t("settings|security|cross_signing_master_private_Key")} {masterPrivateKeyCached ? _t("settings|security|cross_signing_cached") : _t("settings|security|cross_signing_not_cached")}
{_t("settings|security|cross_signing_self_signing_private_key")} {selfSigningPrivateKeyCached ? _t("settings|security|cross_signing_cached") : _t("settings|security|cross_signing_not_cached")}
{_t("settings|security|cross_signing_user_signing_private_key")} {userSigningPrivateKeyCached ? _t("settings|security|cross_signing_cached") : _t("settings|security|cross_signing_not_cached")}
{_t("settings|security|cross_signing_homeserver_support")} {homeserverSupportsCrossSigning ? _t("settings|security|cross_signing_homeserver_support_exists") : _t("settings|security|cross_signing_not_found")}
{errorSection} {actionRow} ); } }