Provide a devtool for manually verifying other devices (#30094)
Also allows doing the same thing via a slash command.
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
Copyright 2024-2025 New Vector Ltd.
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2016 OpenMarket 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, { type ChangeEvent, type JSX, useCallback, useState } from "react";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { _t, UserFriendlyError } from "../../../languageHandler";
|
||||
import { getDeviceCryptoInfo } from "../../../utils/crypto/deviceInfo";
|
||||
import QuestionDialog from "./QuestionDialog";
|
||||
import Modal from "../../../Modal";
|
||||
import InfoDialog from "./InfoDialog";
|
||||
import Field from "../elements/Field";
|
||||
import ErrorDialog from "./ErrorDialog";
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||
|
||||
interface Props {
|
||||
onFinished(confirm?: boolean): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A dialog to allow us to verify devices logged in with clients that can't do
|
||||
* the verification themselves. Intended for use as a dev tool.
|
||||
*
|
||||
* Requires entering the fingerprint ("session key") of the device in an attempt
|
||||
* to prevent users being tricked into verifying a malicious device.
|
||||
*/
|
||||
export function ManualDeviceKeyVerificationDialog({ onFinished }: Readonly<Props>): JSX.Element {
|
||||
const [deviceId, setDeviceId] = useState("");
|
||||
const [fingerprint, setFingerprint] = useState("");
|
||||
|
||||
const client = useMatrixClientContext();
|
||||
|
||||
const onDialogFinished = useCallback(
|
||||
async (confirm: boolean) => {
|
||||
if (confirm) {
|
||||
await manuallyVerifyDevice(client, deviceId, fingerprint);
|
||||
}
|
||||
onFinished(confirm);
|
||||
},
|
||||
[client, deviceId, fingerprint, onFinished],
|
||||
);
|
||||
|
||||
const onDeviceIdChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setDeviceId(e.target.value);
|
||||
}, []);
|
||||
|
||||
const onFingerprintChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setFingerprint(e.target.value);
|
||||
}, []);
|
||||
|
||||
const body = (
|
||||
<div>
|
||||
<p>{_t("encryption|verification|manual|text")}</p>
|
||||
<div className="mx_DeviceVerifyDialog_cryptoSection">
|
||||
<Field
|
||||
className="mx_TextInputDialog_input"
|
||||
type="text"
|
||||
label={_t("encryption|verification|manual|device_id")}
|
||||
value={deviceId}
|
||||
onChange={onDeviceIdChange}
|
||||
/>
|
||||
<Field
|
||||
className="mx_TextInputDialog_input"
|
||||
type="text"
|
||||
label={_t("encryption|verification|manual|fingerprint")}
|
||||
value={fingerprint}
|
||||
onChange={onFingerprintChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<QuestionDialog
|
||||
title={_t("settings|sessions|verify_session")}
|
||||
description={body}
|
||||
button={_t("settings|sessions|verify_session")}
|
||||
onFinished={onDialogFinished}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the supplied fingerprint matches the fingerprint ("session key") of the
|
||||
* device with the supplied device ID, and if so, mark the device as verified.
|
||||
*/
|
||||
export async function manuallyVerifyDevice(client: MatrixClient, deviceId: string, fingerprint: string): Promise<void> {
|
||||
try {
|
||||
await doManuallyVerifyDevice(client, deviceId, fingerprint);
|
||||
|
||||
// Tell the user we verified everything
|
||||
Modal.createDialog(InfoDialog, {
|
||||
title: _t("encryption|verification|manual|success_title"),
|
||||
description: (
|
||||
<div>
|
||||
<p>{_t("encryption|verification|manual|success_description", { deviceId })}</p>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
} catch (e: any) {
|
||||
// Display an error
|
||||
const error = e instanceof UserFriendlyError ? e.translatedMessage : e.toString();
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("encryption|verification|manual|failure_title"),
|
||||
description: (
|
||||
<div>
|
||||
<p>{_t("encryption|verification|manual|failure_description", { deviceId, error })}</p>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function doManuallyVerifyDevice(client: MatrixClient, deviceId: string, fingerprint: string): Promise<void> {
|
||||
const userId = client.getUserId();
|
||||
if (!userId) {
|
||||
throw new UserFriendlyError("encryption|verification|manual|no_userid", {
|
||||
cause: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
const crypto = client.getCrypto();
|
||||
if (!crypto) {
|
||||
throw new UserFriendlyError("encryption|verification|manual|no_crypto");
|
||||
}
|
||||
|
||||
const device = await getDeviceCryptoInfo(client, userId, deviceId);
|
||||
if (!device) {
|
||||
throw new UserFriendlyError("encryption|verification|manual|no_device", {
|
||||
deviceId,
|
||||
cause: undefined,
|
||||
});
|
||||
}
|
||||
const deviceTrust = await crypto.getDeviceVerificationStatus(userId, deviceId);
|
||||
|
||||
if (deviceTrust?.isVerified()) {
|
||||
if (device.getFingerprint() === fingerprint) {
|
||||
throw new UserFriendlyError("encryption|verification|manual|already_verified", {
|
||||
deviceId,
|
||||
cause: undefined,
|
||||
});
|
||||
} else {
|
||||
throw new UserFriendlyError("encryption|verification|manual|already_verified_and_wrong_fingerprint", {
|
||||
deviceId,
|
||||
cause: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (device.getFingerprint() !== fingerprint) {
|
||||
const fprint = device.getFingerprint();
|
||||
throw new UserFriendlyError("encryption|verification|manual|wrong_fingerprint", {
|
||||
fprint,
|
||||
deviceId,
|
||||
fingerprint,
|
||||
cause: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
// We've passed all the checks - do the device verification
|
||||
await crypto.crossSignDevice(deviceId);
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext
|
||||
import BaseTool from "./BaseTool";
|
||||
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import Modal from "../../../../Modal";
|
||||
import { ManualDeviceKeyVerificationDialog } from "../ManualDeviceKeyVerificationDialog";
|
||||
|
||||
interface KeyBackupProps {
|
||||
/**
|
||||
@@ -31,6 +33,16 @@ export function Crypto({ onBack }: KeyBackupProps): JSX.Element {
|
||||
<>
|
||||
<KeyStorage />
|
||||
<CrossSigning />
|
||||
<Session />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
Modal.createDialog(ManualDeviceKeyVerificationDialog);
|
||||
}}
|
||||
>
|
||||
{_t("devtools|manual_device_verification")}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<span>{_t("devtools|crypto|crypto_not_available")}</span>
|
||||
@@ -254,3 +266,39 @@ function getCrossSigningStatus(crossSigningReady: boolean, crossSigningPrivateKe
|
||||
|
||||
return _t("devtools|crypto|cross_signing_not_ready");
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that displays information about the current session.
|
||||
*/
|
||||
function Session(): JSX.Element {
|
||||
const matrixClient = useMatrixClientContext();
|
||||
const sessionData = useAsyncMemo(async () => {
|
||||
const crypto = matrixClient.getCrypto()!;
|
||||
const keys = await crypto.getOwnDeviceKeys();
|
||||
return {
|
||||
fingerprint: keys.ed25519,
|
||||
deviceId: matrixClient.deviceId,
|
||||
};
|
||||
}, [matrixClient]);
|
||||
|
||||
// Show a spinner while loading
|
||||
if (sessionData === undefined) {
|
||||
return <InlineSpinner aria-label={_t("common|loading")} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<table aria-label={_t("devtools|crypto|session")}>
|
||||
<thead>{_t("devtools|crypto|session")}</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{_t("devtools|crypto|device_id")}</th>
|
||||
<td>{sessionData.deviceId}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{_t("devtools|crypto|session_fingerprint")}</th>
|
||||
<td>{sessionData.fingerprint}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user