You've already forked element-web
mirror of
https://github.com/element-hq/element-web.git
synced 2025-08-08 03:42:14 +03:00
Provide a devtool for manually verifying other devices (#30094)
Also allows doing the same thing via a slash command.
This commit is contained in:
@@ -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"),
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
||||
|
@@ -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"
|
||||
},
|
||||
|
@@ -157,6 +157,7 @@ export function createTestClient(): MatrixClient {
|
||||
getSessionBackupPrivateKey: jest.fn().mockResolvedValue(null),
|
||||
isSecretStorageReady: jest.fn().mockResolvedValue(false),
|
||||
deleteKeyBackupVersion: jest.fn(),
|
||||
crossSignDevice: jest.fn(),
|
||||
}),
|
||||
|
||||
getPushActionsForEvent: jest.fn(),
|
||||
|
@@ -21,6 +21,7 @@ import { WidgetType } from "../../src/widgets/WidgetType";
|
||||
import { warnSelfDemote } from "../../src/components/views/right_panel/UserInfo";
|
||||
import dispatcher from "../../src/dispatcher/dispatcher";
|
||||
import { SettingLevel } from "../../src/settings/SettingLevel";
|
||||
import ErrorDialog from "../../src/components/views/dialogs/ErrorDialog";
|
||||
|
||||
jest.mock("../../src/components/views/right_panel/UserInfo");
|
||||
|
||||
@@ -253,6 +254,20 @@ describe("SlashCommands", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("/verify", () => {
|
||||
it("should return usage if no args", () => {
|
||||
const command = findCommand("verify")!;
|
||||
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
|
||||
});
|
||||
|
||||
it("should show an error if device is not found", async () => {
|
||||
const spy = jest.spyOn(Modal, "createDialog");
|
||||
const command = findCommand("verify")!;
|
||||
await command.run(client, roomId, null, "mydeviceid myfingerprint").promise;
|
||||
expect(spy).toHaveBeenCalledWith(ErrorDialog, expect.objectContaining({ title: "Verification failed" }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("/addwidget", () => {
|
||||
it("should parse html iframe snippets", async () => {
|
||||
jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true);
|
||||
|
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Copyright 2024-2025 New Vector Ltd.
|
||||
* Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 from "react";
|
||||
import { act, fireEvent, render, screen, waitFor } from "jest-matrix-react";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { DeviceVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { stubClient } from "../../../../test-utils";
|
||||
import { ManualDeviceKeyVerificationDialog } from "../../../../../src/components/views/dialogs/ManualDeviceKeyVerificationDialog";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
|
||||
describe("ManualDeviceKeyVerificationDialog", () => {
|
||||
let mockClient: MatrixClient;
|
||||
|
||||
function renderDialog(onFinished: (confirm: boolean) => void) {
|
||||
return render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<ManualDeviceKeyVerificationDialog onFinished={onFinished} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = stubClient();
|
||||
mockExistingDevices();
|
||||
});
|
||||
|
||||
it("should render correctly", () => {
|
||||
// When we render a dialog populated with data
|
||||
const { dialog } = populateDialog("XYZ", "ABCDEFGH");
|
||||
|
||||
// Then the dialog looks as expected
|
||||
expect(dialog.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should call onFinished and crossSignDevice if we click Verify", async () => {
|
||||
// Given a dialog populated with correct data
|
||||
const { dialog, onFinished } = populateDialog("DEVICEID", "FINGERPRINT");
|
||||
|
||||
// When we click Verify session
|
||||
dialog.getByRole("button", { name: "Verify session" }).click();
|
||||
|
||||
// Then crossSignDevice is called
|
||||
await waitFor(async () => {
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
expect(mockClient.getCrypto()?.crossSignDevice).toHaveBeenCalledWith("DEVICEID");
|
||||
});
|
||||
});
|
||||
|
||||
it("should not call crossSignDevice if fingerprint is wrong", async () => {
|
||||
// Given a dialog populated with incorrect fingerprint
|
||||
const { dialog, onFinished } = populateDialog("DEVICEID", "WRONG_FINGERPRINT");
|
||||
|
||||
// When we click Verify session
|
||||
act(() => dialog.getByRole("button", { name: "Verify session" }).click());
|
||||
|
||||
// Then crossSignDevice is not called
|
||||
await waitFor(async () => {
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
expect(mockClient.getCrypto()?.crossSignDevice).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// And an error is displayed
|
||||
expect(
|
||||
screen.getByText(
|
||||
"the supplied fingerprint 'WRONG_FINGERPRINT' does not match the device fingerprint, 'FINGERPRINT'",
|
||||
{ exact: false },
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
it("should not call crossSignDevice if device is already verified", async () => {
|
||||
// Given a dialog populated with a correct fingerprint for a verified device
|
||||
const { dialog, onFinished } = populateDialog("VERIFIED_DEVICEID", "VERIFIED_FINGERPRINT");
|
||||
|
||||
// When we click Verify session
|
||||
act(() => dialog.getByRole("button", { name: "Verify session" }).click());
|
||||
|
||||
// Then crossSignDevice is not called
|
||||
await waitFor(async () => {
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
expect(mockClient.getCrypto()?.crossSignDevice).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// And an error is displayed
|
||||
expect(screen.getByText("Failed to verify 'VERIFIED_DEVICEID': This device is already verified")).toBeVisible();
|
||||
});
|
||||
|
||||
it("should not call crossSignDevice if device is already verified and fingerprint is wrong", async () => {
|
||||
// Given a dialog populated with an incorrect fingerprint for a verified device
|
||||
const { dialog, onFinished } = populateDialog("VERIFIED_DEVICEID", "WRONG_FINGERPRINT");
|
||||
|
||||
// When we click Verify session
|
||||
act(() => dialog.getByRole("button", { name: "Verify session" }).click());
|
||||
|
||||
// Then crossSignDevice is not called
|
||||
await waitFor(async () => {
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
expect(mockClient.getCrypto()?.crossSignDevice).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// And an error is displayed
|
||||
expect(
|
||||
screen.getByText("The supplied fingerprint does not match, but the device is already verified!", {
|
||||
exact: false,
|
||||
}),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
it("should not call crossSignDevice if device is not found", async () => {
|
||||
// Given a dialog populated with incorrect device ID
|
||||
const { dialog, onFinished } = populateDialog("WRONG_DEVICE_ID", "FINGERPRINT");
|
||||
|
||||
// When we click Verify session
|
||||
act(() => dialog.getByRole("button", { name: "Verify session" }).click());
|
||||
|
||||
// Then crossSignDevice is not called
|
||||
await waitFor(async () => {
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
expect(mockClient.getCrypto()?.crossSignDevice).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// And an error is displayed
|
||||
expect(screen.getByText("device 'WRONG_DEVICE_ID' was not found", { exact: false })).toBeVisible();
|
||||
});
|
||||
|
||||
it("should call onFinished but not crossSignDevice if we click Cancel", () => {
|
||||
// Given a dialog populated with correct data
|
||||
const { dialog, onFinished } = populateDialog("DEVICEID", "FINGERPRINT");
|
||||
|
||||
// When we click cancel
|
||||
dialog.getByRole("button", { name: "Cancel" }).click();
|
||||
|
||||
// Then only onFinished is called
|
||||
expect(onFinished).toHaveBeenCalledWith(false);
|
||||
expect(mockClient.getCrypto()?.crossSignDevice).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
function unverifiedDevice(): DeviceVerificationStatus {
|
||||
return new DeviceVerificationStatus({});
|
||||
}
|
||||
|
||||
function verifiedDevice(): DeviceVerificationStatus {
|
||||
return new DeviceVerificationStatus({
|
||||
signedByOwner: true,
|
||||
crossSigningVerified: true,
|
||||
tofu: true,
|
||||
localVerified: true,
|
||||
trustCrossSignedDevices: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up two devices: DEVICEID, which is unverified, and VERIFIED_DEVICEID, which is verified.
|
||||
*/
|
||||
function mockExistingDevices() {
|
||||
mockClient.getCrypto()!.getDeviceVerificationStatus = jest
|
||||
.fn()
|
||||
.mockImplementation(async (_userId, deviceId) =>
|
||||
deviceId === "DEVICEID" ? unverifiedDevice() : verifiedDevice(),
|
||||
);
|
||||
|
||||
mockClient.getCrypto()!.getUserDeviceInfo = jest.fn().mockImplementation(async (userIds) => {
|
||||
const userDevices = new Map();
|
||||
userDevices.set("DEVICEID", { getFingerprint: jest.fn().mockReturnValue("FINGERPRINT") });
|
||||
userDevices.set("VERIFIED_DEVICEID", { getFingerprint: jest.fn().mockReturnValue("VERIFIED_FINGERPRINT") });
|
||||
|
||||
const deviceMap = new Map();
|
||||
for (const userId of userIds) {
|
||||
deviceMap.set(userId, userDevices);
|
||||
}
|
||||
return deviceMap;
|
||||
});
|
||||
}
|
||||
|
||||
function populateDialog(deviceId: string, fingerprint: string) {
|
||||
const onFinished = jest.fn();
|
||||
const dialog = renderDialog(onFinished);
|
||||
const deviceIdBox = dialog.getByRole("textbox", { name: "Device ID" });
|
||||
const fingerprintBox = dialog.getByRole("textbox", { name: "Fingerprint (session key)" });
|
||||
fireEvent.change(deviceIdBox, { target: { value: deviceId } });
|
||||
fireEvent.change(fingerprintBox, { target: { value: fingerprint } });
|
||||
return { dialog, onFinished };
|
||||
}
|
||||
});
|
@@ -0,0 +1,107 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ManualDeviceKeyVerificationDialog should render correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-describedby="mx_Dialog_content"
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_QuestionDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Verify session
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_content"
|
||||
id="mx_Dialog_content"
|
||||
>
|
||||
<div>
|
||||
<p>
|
||||
Supply the ID and fingerprint of one of your own devices to verify it.
|
||||
</p>
|
||||
<div
|
||||
class="mx_DeviceVerifyDialog_cryptoSection"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_input mx_TextInputDialog_input"
|
||||
>
|
||||
<input
|
||||
id="mx_Field_1"
|
||||
label="Device ID"
|
||||
placeholder="Device ID"
|
||||
type="text"
|
||||
value="XYZ"
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_1"
|
||||
>
|
||||
Device ID
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Field mx_Field_input mx_TextInputDialog_input"
|
||||
>
|
||||
<input
|
||||
id="mx_Field_2"
|
||||
label="Fingerprint (session key)"
|
||||
placeholder="Fingerprint (session key)"
|
||||
type="text"
|
||||
value="ABCDEFGH"
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_2"
|
||||
>
|
||||
Fingerprint (session key)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
data-testid="dialog-cancel-button"
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
type="button"
|
||||
>
|
||||
Verify session
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
Reference in New Issue
Block a user