Provide a devtool for manually verifying other devices (#30094)

Also allows doing the same thing via a slash command.
This commit is contained in:
Andy Balaam
2025-06-10 11:55:05 +01:00
committed by GitHub
parent a333856c50
commit e7d940160a
8 changed files with 572 additions and 0 deletions

View File

@@ -60,6 +60,7 @@ import { deop, op } from "./slash-commands/op";
import { CommandCategories } from "./slash-commands/interface";
import { Command } from "./slash-commands/command";
import { goto, join } from "./slash-commands/join";
import { manuallyVerifyDevice } from "./components/views/dialogs/ManualDeviceKeyVerificationDialog";
export { CommandCategories, Command };
@@ -663,6 +664,24 @@ export const Commands = [
category: CommandCategories.admin,
renderingTypes: [TimelineRenderingType.Room],
}),
new Command({
command: "verify",
args: "<device-id> <device-fingerprint>",
description: _td("slash_command|verify"),
runFn: function (cli, _roomId, _threadId, args) {
if (args) {
const matches = args.match(/^(\S+) +(\S+)$/);
if (matches) {
const deviceId = matches[1];
const fingerprint = matches[2];
return success(manuallyVerifyDevice(cli, deviceId, fingerprint));
}
}
return reject(this.getUsage());
},
category: CommandCategories.advanced,
renderingTypes: [TimelineRenderingType.Room],
}),
new Command({
command: "discardsession",
description: _td("slash_command|discardsession"),

View File

@@ -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);
}

View File

@@ -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>
);
}

View File

@@ -786,6 +786,7 @@
"cross_signing_status": "Cross-signing status:",
"cross_signing_untrusted": "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.",
"crypto_not_available": "Cryptographic module is not available",
"device_id": "Device ID",
"key_backup_active_version": "Active backup version:",
"key_backup_active_version_none": "None",
"key_backup_inactive_warning": "Your keys are not being backed up from this session.",
@@ -798,6 +799,8 @@
"secret_storage_ready": "ready",
"secret_storage_status": "Secret storage:",
"self_signing_private_key_cached_status": "Self signing private key:",
"session": "Session",
"session_fingerprint": "Fingerprint (session key)",
"title": "End-to-end encryption",
"user_signing_private_key_cached_status": "User signing private key:"
},
@@ -823,6 +826,7 @@
"low_bandwidth_mode": "Low bandwidth mode",
"low_bandwidth_mode_description": "Requires compatible homeserver.",
"main_timeline": "Main timeline",
"manual_device_verification": "Manual device verification",
"no_receipt_found": "No receipt found",
"notification_state": "Notification state is <strong>%(notificationState)s</strong>",
"notifications_debug": "Notifications debug",
@@ -1007,6 +1011,21 @@
"incoming_sas_dialog_waiting": "Waiting for partner to confirm…",
"incoming_sas_user_dialog_text_1": "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.",
"incoming_sas_user_dialog_text_2": "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.",
"manual": {
"already_verified": "This device is already verified",
"already_verified_and_wrong_fingerprint": "The supplied fingerprint does not match, but the device is already verified!",
"device_id": "Device ID",
"failure_description": "Failed to verify '%(deviceId)s': %(error)s",
"failure_title": "Verification failed",
"fingerprint": "Fingerprint (session key)",
"no_crypto": "Unable to verify device - crypto is not enabled",
"no_device": "Unable to verify device - device '%(deviceId)s' was not found",
"no_userid": "Unable to verify device - cannot find our User ID",
"success_description": "The device (%(deviceId)s) is now cross-signed",
"success_title": "Verification successful",
"text": "Supply the ID and fingerprint of one of your own devices to verify it.",
"wrong_fingerprint": "Unable to verify device '%(deviceId)s' - the supplied fingerprint '%(fingerprint)s' does not match the device fingerprint, '%(fprint)s'"
},
"no_key_or_device": "It looks like you don't have a Recovery Key or any other devices you can verify against. This device will not be able to access old encrypted messages. In order to verify your identity on this device, you'll need to reset your verification keys.",
"no_support_qr_emoji": "The device you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.",
"other_party_cancelled": "The other party cancelled the verification.",
@@ -3133,6 +3152,7 @@
"upgraderoom": "Upgrades a room to a new version",
"upgraderoom_permission_error": "You do not have the required permissions to use this command.",
"usage": "Usage",
"verify": "Manually verify one of your own devices",
"view": "Views room with given address",
"whois": "Displays information about a user"
},