element-portable/src/components/views/settings/SecureBackupPanel.tsx
Michael Telatynski 0899165d9e
Move state update listeners from constructor to componentDidMount (#28341)
* Move state update listeners from constructor to componentDidMount

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-01 17:39:08 +00:00

426 lines
16 KiB
TypeScript

/*
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, { ReactNode } from "react";
import { CryptoEvent, BackupTrustInfo, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
import { logger } from "matrix-js-sdk/src/logger";
import type CreateKeyBackupDialog from "../../../async-components/views/dialogs/security/CreateKeyBackupDialog";
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<void> {
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<void> {
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.createDialogAsync(
import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog") as unknown as Promise<
typeof 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<void> => {
Modal.createDialog(RestoreKeyBackupDialog, undefined, undefined, /* priority = */ false, /* static = */ true);
};
private resetSecretStorage = async (): Promise<void> => {
this.setState({ error: false });
try {
await accessSecretStorage(async (): Promise<void> => {}, /* 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 = (
<SettingsSubsectionText className="error">
{_t("settings|security|error_loading_key_backup_status")}
</SettingsSubsectionText>
);
} else if (loading) {
statusDescription = <Spinner />;
} else if (backupInfo) {
let restoreButtonCaption = _t("settings|security|restore_key_backup");
if (this.state.activeBackupVersion !== null) {
statusDescription = (
<SettingsSubsectionText> {_t("settings|security|key_backup_active")}</SettingsSubsectionText>
);
} else {
statusDescription = (
<>
<SettingsSubsectionText>
{_t("settings|security|key_backup_inactive", {}, { b: (sub) => <strong>{sub}</strong> })}
</SettingsSubsectionText>
<SettingsSubsectionText>
{_t("settings|security|key_backup_connect_prompt")}
</SettingsSubsectionText>
</>
);
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 = (
<div>
{_t("settings|security|key_backup_in_progress", { sessionsRemaining })} <br />
</div>
);
} else {
uploadStatus = (
<div>
{_t("settings|security|key_backup_complete")} <br />
</div>
);
}
let trustedLocally: string | undefined;
if (backupTrustInfo?.matchesDecryptionKey) {
trustedLocally = _t("settings|security|key_backup_can_be_restored");
}
extraDetailsTableRows = (
<>
<tr>
<th scope="row">{_t("settings|security|key_backup_latest_version")}</th>
<td>
{backupInfo.version} ({_t("settings|security|key_backup_algorithm")}{" "}
<code>{backupInfo.algorithm}</code>)
</td>
</tr>
<tr>
<th scope="row">{_t("settings|security|key_backup_active_version")}</th>
<td>
{this.state.activeBackupVersion === null
? _t("settings|security|key_backup_active_version_none")
: this.state.activeBackupVersion}
</td>
</tr>
</>
);
extraDetails = (
<>
{uploadStatus}
<div>{trustedLocally}</div>
</>
);
actions.push(
<AccessibleButton key="restore" kind="primary_outline" onClick={this.restoreBackup}>
{restoreButtonCaption}
</AccessibleButton>,
);
if (!isSecureBackupRequired(MatrixClientPeg.safeGet())) {
actions.push(
<AccessibleButton key="delete" kind="danger_outline" onClick={this.deleteBackup}>
{_t("settings|security|delete_backup")}
</AccessibleButton>,
);
}
} else {
statusDescription = (
<>
<SettingsSubsectionText>
{_t(
"settings|security|key_backup_inactive_warning",
{},
{ b: (sub) => <strong>{sub}</strong> },
)}
</SettingsSubsectionText>
<SettingsSubsectionText>{_t("encryption|setup_secure_backup|explainer")}</SettingsSubsectionText>
</>
);
actions.push(
<AccessibleButton key="setup" kind="primary_outline" onClick={this.startNewBackup}>
{_t("encryption|setup_secure_backup|title")}
</AccessibleButton>,
);
}
if (secretStorageKeyInAccount) {
actions.push(
<AccessibleButton key="reset" kind="danger_outline" onClick={this.resetSecretStorage}>
{_t("action|reset")}
</AccessibleButton>,
);
}
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 = <div className="mx_SecureBackupPanel_buttonRow">{actions}</div>;
}
return (
<>
<SettingsSubsectionText>{_t("settings|security|backup_keys_description")}</SettingsSubsectionText>
{statusDescription}
<details>
<summary className="mx_SecureBackupPanel_advanced">{_t("common|advanced")}</summary>
<table className="mx_SecureBackupPanel_statusList">
<tr>
<th scope="row">{_t("settings|security|backup_key_stored_status")}</th>
<td>
{backupKeyStored === true
? _t("settings|security|cross_signing_in_4s")
: _t("settings|security|cross_signing_not_stored")}
</td>
</tr>
<tr>
<th scope="row">{_t("settings|security|backup_key_cached_status")}</th>
<td>
{backupKeyCached
? _t("settings|security|cross_signing_cached")
: _t("settings|security|cross_signing_not_cached")}
{backupKeyWellFormedText}
</td>
</tr>
<tr>
<th scope="row">{_t("settings|security|4s_public_key_status")}</th>
<td>
{secretStorageKeyInAccount
? _t("settings|security|4s_public_key_in_account_data")
: _t("settings|security|cross_signing_not_found")}
</td>
</tr>
<tr>
<th scope="row">{_t("settings|security|secret_storage_status")}</th>
<td>
{secretStorageReady
? _t("settings|security|secret_storage_ready")
: _t("settings|security|secret_storage_not_ready")}
</td>
</tr>
{extraDetailsTableRows}
</table>
{extraDetails}
</details>
{actionRow}
</>
);
}
}