Files
blap/src/components/views/settings/CrossSigningPanel.tsx
T
2023-09-21 18:16:04 +01:00

321 lines
13 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { ClientEvent, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
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 } 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<{}, IState> {
private unmounted = false;
public constructor(props: {}) {
super(props);
this.state = {
error: false,
};
}
public componentDidMount(): void {
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<void> {
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,
});
}
/**
* Bootstrapping cross-signing take one of these paths:
* 1. Create cross-signing keys locally and store in secret storage (if it
* already exists on the account).
* 2. Access existing secret storage by requesting passphrase and accessing
* cross-signing keys as needed.
* 3. All keys are loaded and there's nothing to do.
* @param {bool} [forceReset] Bootstrap again even if keys already present
*/
private bootstrapCrossSigning = async ({ forceReset = false }): Promise<void> => {
this.setState({ error: false });
try {
const cli = MatrixClientPeg.safeGet();
await cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async (makeRequest): Promise<void> => {
const { finished } = Modal.createDialog(InteractiveAuthDialog, {
title: _t("Setting up keys"),
matrixClient: cli,
makeRequest,
});
const [confirmed] = await finished;
if (!confirmed) {
throw new Error("Cross-signing key upload auth canceled");
}
},
setupNewCrossSigning: forceReset,
});
} catch (e) {
this.setState({ error: true });
logger.error("Error bootstrapping cross-signing", e);
}
if (this.unmounted) return;
this.getUpdatedStatus();
};
private resetCrossSigning = (): void => {
Modal.createDialog(ConfirmDestroyCrossSigningDialog, {
onFinished: (act) => {
if (!act) return;
this.bootstrapCrossSigning({ forceReset: true });
},
});
};
public render(): React.ReactNode {
const {
error,
crossSigningPublicKeysOnDevice,
crossSigningPrivateKeysInStorage,
masterPrivateKeyCached,
selfSigningPrivateKeyCached,
userSigningPrivateKeyCached,
homeserverSupportsCrossSigning,
crossSigningReady,
} = this.state;
let errorSection;
if (error) {
errorSection = <div className="error">{error.toString()}</div>;
}
let summarisedStatus;
if (homeserverSupportsCrossSigning === undefined) {
summarisedStatus = <Spinner />;
} else if (!homeserverSupportsCrossSigning) {
summarisedStatus = (
<SettingsSubsectionText data-testid="summarised-status">
{_t("Your homeserver does not support cross-signing.")}
</SettingsSubsectionText>
);
} else if (crossSigningReady && crossSigningPrivateKeysInStorage) {
summarisedStatus = (
<SettingsSubsectionText data-testid="summarised-status">
{_t("Cross-signing is ready for use.")}
</SettingsSubsectionText>
);
} else if (crossSigningReady && !crossSigningPrivateKeysInStorage) {
summarisedStatus = (
<SettingsSubsectionText data-testid="summarised-status">
{_t("Cross-signing is ready but keys are not backed up.")}
</SettingsSubsectionText>
);
} else if (crossSigningPrivateKeysInStorage) {
summarisedStatus = (
<SettingsSubsectionText data-testid="summarised-status">
{_t(
"Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.",
)}
</SettingsSubsectionText>
);
} else {
summarisedStatus = (
<SettingsSubsectionText data-testid="summarised-status">
{_t("Cross-signing is not set up.")}
</SettingsSubsectionText>
);
}
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("Set up Secure Backup");
if (crossSigningPrivateKeysInStorage) {
buttonCaption = _t("Verify this session");
}
actions.push(
<AccessibleButton key="setup" kind="primary" onClick={this.onBootstrapClick}>
{buttonCaption}
</AccessibleButton>,
);
}
if (keysExistAnywhere) {
actions.push(
<AccessibleButton key="reset" kind="danger" onClick={this.resetCrossSigning}>
{_t("action|reset")}
</AccessibleButton>,
);
}
let actionRow;
if (actions.length) {
actionRow = <div className="mx_CrossSigningPanel_buttonRow">{actions}</div>;
}
return (
<>
{summarisedStatus}
<details>
<summary>{_t("Advanced")}</summary>
<table className="mx_CrossSigningPanel_statusList">
<tr>
<th scope="row">{_t("settings|security|cross_signing_public_keys")}</th>
<td>
{crossSigningPublicKeysOnDevice
? _t("settings|security|cross_signing_in_memory")
: _t("settings|security|cross_signing_not_found")}
</td>
</tr>
<tr>
<th scope="row">{_t("settings|security|cross_signing_private_keys")}</th>
<td>
{crossSigningPrivateKeysInStorage
? _t("settings|security|cross_signing_in_4s")
: _t("settings|security|cross_signing_not_in_4s")}
</td>
</tr>
<tr>
<th scope="row">{_t("settings|security|cross_signing_master_private_Key")}</th>
<td>
{masterPrivateKeyCached
? _t("settings|security|cross_signing_cached")
: _t("settings|security|cross_signing_not_cached")}
</td>
</tr>
<tr>
<th scope="row">{_t("settings|security|cross_signing_self_signing_private_key")}</th>
<td>
{selfSigningPrivateKeyCached
? _t("settings|security|cross_signing_cached")
: _t("settings|security|cross_signing_not_cached")}
</td>
</tr>
<tr>
<th scope="row">{_t("settings|security|cross_signing_user_signing_private_key")}</th>
<td>
{userSigningPrivateKeyCached
? _t("settings|security|cross_signing_cached")
: _t("settings|security|cross_signing_not_cached")}
</td>
</tr>
<tr>
<th scope="row">{_t("settings|security|cross_signing_homeserver_support")}</th>
<td>
{homeserverSupportsCrossSigning
? _t("settings|security|cross_signing_homeserver_support_exists")
: _t("settings|security|cross_signing_not_found")}
</td>
</tr>
</table>
</details>
{errorSection}
{actionRow}
</>
);
}
}