/* Copyright 2024 New Vector Ltd. Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Copyright 2018 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, { lazy, ReactNode } from "react"; import { CryptoEvent, BackupTrustInfo, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; import { isSecureBackupRequired } from "../../../utils/WellKnownUtils"; import Spinner from "../elements/Spinner"; import AccessibleButton from "../elements/AccessibleButton"; import QuestionDialog from "../dialogs/QuestionDialog"; import RestoreKeyBackupDialog from "../dialogs/security/RestoreKeyBackupDialog"; import { accessSecretStorage } from "../../../SecurityManager"; import { SettingsSubsectionText } from "./shared/SettingsSubsection"; interface IState { loading: boolean; error: boolean; backupKeyStored: boolean | null; backupKeyCached: boolean | null; backupKeyWellFormed: boolean | null; secretStorageKeyInAccount: boolean | null; secretStorageReady: boolean | null; /** Information on the current key backup version, as returned by the server. * * `null` could mean any of: * * we haven't yet requested the data from the server. * * we were unable to reach the server. * * the server returned key backup version data we didn't understand or was malformed. * * there is actually no backup on the server. */ backupInfo: KeyBackupInfo | null; /** * Information on whether the backup in `backupInfo` is correctly signed, and whether we have the right key to * decrypt it. * * `undefined` if `backupInfo` is null, or if crypto is not enabled in the client. */ backupTrustInfo: BackupTrustInfo | undefined; /** * If key backup is currently enabled, the backup version we are backing up to. */ activeBackupVersion: string | null; /** * Number of sessions remaining to be backed up. `null` if we have no information on this. */ sessionsRemaining: number | null; } export default class SecureBackupPanel extends React.PureComponent<{}, IState> { private unmounted = false; public constructor(props: {}) { super(props); this.state = { loading: true, error: false, backupKeyStored: null, backupKeyCached: null, backupKeyWellFormed: null, secretStorageKeyInAccount: null, secretStorageReady: null, backupInfo: null, backupTrustInfo: undefined, activeBackupVersion: null, sessionsRemaining: null, }; } public componentDidMount(): void { this.unmounted = false; this.loadBackupStatus(); MatrixClientPeg.safeGet().on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus); MatrixClientPeg.safeGet().on(CryptoEvent.KeyBackupSessionsRemaining, this.onKeyBackupSessionsRemaining); } public componentWillUnmount(): void { this.unmounted = true; if (MatrixClientPeg.get()) { MatrixClientPeg.get()!.removeListener(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus); MatrixClientPeg.get()!.removeListener( CryptoEvent.KeyBackupSessionsRemaining, this.onKeyBackupSessionsRemaining, ); } } private onKeyBackupSessionsRemaining = (sessionsRemaining: number): void => { this.setState({ sessionsRemaining, }); }; private onKeyBackupStatus = (): void => { // This just loads the current backup status rather than forcing // a re-check otherwise we risk causing infinite loops this.loadBackupStatus(); }; private async loadBackupStatus(): Promise { this.setState({ loading: true }); this.getUpdatedDiagnostics(); try { const cli = MatrixClientPeg.safeGet(); const backupInfo = await cli.getKeyBackupVersion(); const backupTrustInfo = backupInfo ? await cli.getCrypto()?.isKeyBackupTrusted(backupInfo) : undefined; const activeBackupVersion = (await cli.getCrypto()?.getActiveSessionBackupVersion()) ?? null; if (this.unmounted) return; this.setState({ loading: false, error: false, backupInfo, backupTrustInfo, activeBackupVersion, }); } catch (e) { logger.log("Unable to fetch key backup status", e); if (this.unmounted) return; this.setState({ loading: false, error: true, backupInfo: null, backupTrustInfo: undefined, activeBackupVersion: null, }); } } private async getUpdatedDiagnostics(): Promise { const cli = MatrixClientPeg.safeGet(); const crypto = cli.getCrypto(); if (!crypto) return; const secretStorage = cli.secretStorage; const backupKeyStored = !!(await cli.isKeyBackupKeyStored()); const backupKeyFromCache = await crypto.getSessionBackupPrivateKey(); const backupKeyCached = !!backupKeyFromCache; const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array; const secretStorageKeyInAccount = await secretStorage.hasKey(); const secretStorageReady = await crypto.isSecretStorageReady(); if (this.unmounted) return; this.setState({ backupKeyStored, backupKeyCached, backupKeyWellFormed, secretStorageKeyInAccount, secretStorageReady, }); } private startNewBackup = (): void => { Modal.createDialog( lazy(() => import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog")), { onFinished: () => { this.loadBackupStatus(); }, }, undefined, /* priority = */ false, /* static = */ true, ); }; private deleteBackup = (): void => { Modal.createDialog(QuestionDialog, { title: _t("settings|security|delete_backup"), description: _t("settings|security|delete_backup_confirm_description"), button: _t("settings|security|delete_backup"), danger: true, onFinished: (proceed) => { if (!proceed) return; this.setState({ loading: true }); const versionToDelete = this.state.backupInfo!.version!; MatrixClientPeg.safeGet() .getCrypto() ?.deleteKeyBackupVersion(versionToDelete) .then(() => { this.loadBackupStatus(); }); }, }); }; private restoreBackup = async (): Promise => { Modal.createDialog(RestoreKeyBackupDialog, undefined, undefined, /* priority = */ false, /* static = */ true); }; private resetSecretStorage = async (): Promise => { this.setState({ error: false }); try { await accessSecretStorage(async (): Promise => {}, { forceReset: true }); } catch (e) { logger.error("Error resetting secret storage", e); if (this.unmounted) return; this.setState({ error: true }); } if (this.unmounted) return; this.loadBackupStatus(); }; public render(): React.ReactNode { const { loading, error, backupKeyStored, backupKeyCached, backupKeyWellFormed, secretStorageKeyInAccount, secretStorageReady, backupInfo, backupTrustInfo, sessionsRemaining, } = this.state; let statusDescription: JSX.Element; let extraDetailsTableRows: JSX.Element | undefined; let extraDetails: JSX.Element | undefined; const actions: JSX.Element[] = []; if (error) { statusDescription = ( {_t("settings|security|error_loading_key_backup_status")} ); } else if (loading) { statusDescription = ; } else if (backupInfo) { let restoreButtonCaption = _t("settings|security|restore_key_backup"); if (this.state.activeBackupVersion !== null) { statusDescription = ( ✅ {_t("settings|security|key_backup_active")} ); } else { statusDescription = ( <> {_t("settings|security|key_backup_inactive", {}, { b: (sub) => {sub} })} {_t("settings|security|key_backup_connect_prompt")} ); restoreButtonCaption = _t("settings|security|key_backup_connect"); } let uploadStatus: ReactNode; if (sessionsRemaining === null) { // No upload status to show when backup disabled. uploadStatus = ""; } else if (sessionsRemaining > 0) { uploadStatus = (
{_t("settings|security|key_backup_in_progress", { sessionsRemaining })}
); } else { uploadStatus = (
{_t("settings|security|key_backup_complete")}
); } let trustedLocally: string | undefined; if (backupTrustInfo?.matchesDecryptionKey) { trustedLocally = _t("settings|security|key_backup_can_be_restored"); } extraDetailsTableRows = ( <> {_t("settings|security|key_backup_latest_version")} {backupInfo.version} ({_t("settings|security|key_backup_algorithm")}{" "} {backupInfo.algorithm}) {_t("settings|security|key_backup_active_version")} {this.state.activeBackupVersion === null ? _t("settings|security|key_backup_active_version_none") : this.state.activeBackupVersion} ); extraDetails = ( <> {uploadStatus}
{trustedLocally}
); actions.push( {restoreButtonCaption} , ); if (!isSecureBackupRequired(MatrixClientPeg.safeGet())) { actions.push( {_t("settings|security|delete_backup")} , ); } } else { statusDescription = ( <> {_t( "settings|security|key_backup_inactive_warning", {}, { b: (sub) => {sub} }, )} {_t("encryption|setup_secure_backup|explainer")} ); actions.push( {_t("encryption|setup_secure_backup|title")} , ); } if (secretStorageKeyInAccount) { actions.push( {_t("action|reset")} , ); } let backupKeyWellFormedText = ""; if (backupKeyCached) { backupKeyWellFormedText = ", "; if (backupKeyWellFormed) { backupKeyWellFormedText += _t("settings|security|backup_key_well_formed"); } else { backupKeyWellFormedText += _t("settings|security|backup_key_unexpected_type"); } } let actionRow: JSX.Element | undefined; if (actions.length) { actionRow =
{actions}
; } return ( <> {_t("settings|security|backup_keys_description")} {statusDescription}
{_t("common|advanced")} {extraDetailsTableRows}
{_t("settings|security|backup_key_stored_status")} {backupKeyStored === true ? _t("settings|security|cross_signing_in_4s") : _t("settings|security|cross_signing_not_stored")}
{_t("settings|security|backup_key_cached_status")} {backupKeyCached ? _t("settings|security|cross_signing_cached") : _t("settings|security|cross_signing_not_cached")} {backupKeyWellFormedText}
{_t("settings|security|4s_public_key_status")} {secretStorageKeyInAccount ? _t("settings|security|4s_public_key_in_account_data") : _t("settings|security|cross_signing_not_found")}
{_t("settings|security|secret_storage_status")} {secretStorageReady ? _t("settings|security|secret_storage_ready") : _t("settings|security|secret_storage_not_ready")}
{extraDetails}
{actionRow} ); } }