Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Element-R: Use MatrixClient.CryptoApi.getUserVerificationStatus instead of MatrixClient.checkUserTrust in MKeyVerificationConclusion.tsx #11717

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 95 additions & 101 deletions src/components/views/messages/MKeyVerificationConclusion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,131 +14,125 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React from "react";
import React, { useState } from "react";
import classNames from "classnames";
import { MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
import { MatrixEvent, EventType, MatrixClient } from "matrix-js-sdk/src/matrix";
import { VerificationPhase, VerificationRequest, VerificationRequestEvent } from "matrix-js-sdk/src/crypto-api";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";

import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from "../../../languageHandler";
import { getNameForEventRoom, userLabelForEventRoom } from "../../../utils/KeyVerificationStateObserver";
import EventTileBubble from "./EventTileBubble";
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";

interface IProps {
/* the MatrixEvent to show */
mxEvent: MatrixEvent;
timestamp?: JSX.Element;
}

export default class MKeyVerificationConclusion extends React.Component<IProps> {
public constructor(props: IProps) {
super(props);
}

public componentDidMount(): void {
const request = this.props.mxEvent.verificationRequest;
if (request) {
request.on(VerificationRequestEvent.Change, this.onRequestChanged);
}
MatrixClientPeg.safeGet().on(CryptoEvent.UserTrustStatusChanged, this.onTrustChanged);
}

public componentWillUnmount(): void {
const request = this.props.mxEvent.verificationRequest;
if (request) {
request.off(VerificationRequestEvent.Change, this.onRequestChanged);
}
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener(CryptoEvent.UserTrustStatusChanged, this.onTrustChanged);
}
}

private onRequestChanged = (): void => {
this.forceUpdate();
};
export function MKeyVerificationConclusion({ mxEvent, timestamp }: IProps): JSX.Element | null {
const client = useMatrixClientContext();
const request = mxEvent.verificationRequest;

private onTrustChanged = (userId: string): void => {
const { mxEvent } = this.props;
// key is used to trigger rerender when we received event
const [key, setKey] = useState(0);
useTypedEventEmitter(client, CryptoEvent.UserTrustStatusChanged, async (userId: string) => {
const request = mxEvent.verificationRequest;
if (!request || request.otherUserId !== userId) {
return;
}
this.forceUpdate();
};

public static shouldRender(mxEvent: MatrixEvent, request?: VerificationRequest): boolean {
// normally should not happen
if (!request) {
return false;
}
// .cancel event that was sent after the verification finished, ignore
if (mxEvent.getType() === EventType.KeyVerificationCancel && request.phase !== VerificationPhase.Cancelled) {
return false;
}
// .done event that was sent after the verification cancelled, ignore
if (mxEvent.getType() === EventType.KeyVerificationDone && request.phase !== VerificationPhase.Done) {
return false;
}

// request hasn't concluded yet
if (request.pending) {
return false;
// rerender only for the current user
if (request?.otherUserId === userId) {
setKey((key) => ++key);
}

// User isn't actually verified
if (!MatrixClientPeg.safeGet().checkUserTrust(request.otherUserId).isCrossSigningVerified()) {
return false;
});
useTypedEventEmitter(request, VerificationRequestEvent.Change, () => {
setKey((key) => ++key);
});

// check at every received request event if the verification is still ongoing
const isDisplayed = useAsyncMemo(
() => isVerificationInProgress(mxEvent, client, mxEvent.verificationRequest),
[mxEvent, client, mxEvent.verificationRequest, key],
false,
);

if (!isDisplayed || !request) return null;

const myUserId = client.getUserId();
let title: string | undefined;

if (request.phase === VerificationPhase.Done) {
title = _t("timeline|m.key.verification.done", {
name: getNameForEventRoom(client, request.otherUserId, mxEvent.getRoomId()!),
});
} else if (request.phase === VerificationPhase.Cancelled) {
const userId = request.cancellingUserId;
if (userId === myUserId) {
title = _t("timeline|m.key.verification.cancel|you_cancelled", {
name: getNameForEventRoom(client, request.otherUserId, mxEvent.getRoomId()!),
});
} else if (userId) {
title = _t("timeline|m.key.verification.cancel|user_cancelled", {
name: getNameForEventRoom(client, userId, mxEvent.getRoomId()!),
});
}

return true;
}

public render(): JSX.Element | null {
const { mxEvent } = this.props;
const request = mxEvent.verificationRequest!;

if (!MKeyVerificationConclusion.shouldRender(mxEvent, request)) {
return null;
}

const client = MatrixClientPeg.safeGet();
const myUserId = client.getUserId();
if (title) {
const classes = classNames("mx_cryptoEvent mx_cryptoEvent_icon", {
mx_cryptoEvent_icon_verified: request.phase === VerificationPhase.Done,
});
return (
<EventTileBubble
key={key}
className={classes}
title={title}
subtitle={userLabelForEventRoom(client, request.otherUserId, mxEvent.getRoomId()!)}
timestamp={timestamp}
/>
);
}

let title: string | undefined;
return null;
}

if (request.phase === VerificationPhase.Done) {
title = _t("timeline|m.key.verification.done", {
name: getNameForEventRoom(client, request.otherUserId, mxEvent.getRoomId()!),
});
} else if (request.phase === VerificationPhase.Cancelled) {
const userId = request.cancellingUserId;
if (userId === myUserId) {
title = _t("timeline|m.key.verification.cancel|you_cancelled", {
name: getNameForEventRoom(client, request.otherUserId, mxEvent.getRoomId()!),
});
} else if (userId) {
title = _t("timeline|m.key.verification.cancel|user_cancelled", {
name: getNameForEventRoom(client, userId, mxEvent.getRoomId()!),
});
}
}
/**
* Check the verification is not pending, the other user is verified,
* and we didn't receive a cancel or done event after the verification ending
* @param mxEvent matrixEvent related to the verification request
* @param matrixClient current MatrixClient
* @param request the verification request
*/
export async function isVerificationInProgress(
mxEvent: MatrixEvent,
matrixClient: MatrixClient,
request?: VerificationRequest,
): Promise<boolean> {
// normally should not happen
if (!request) {
return false;
}
// .cancel event that was sent after the verification finished, ignore
if (mxEvent.getType() === EventType.KeyVerificationCancel && request.phase !== VerificationPhase.Cancelled) {
return false;
}
// .done event that was sent after the verification cancelled, ignore
if (mxEvent.getType() === EventType.KeyVerificationDone && request.phase !== VerificationPhase.Done) {
return false;
}

if (title) {
const classes = classNames("mx_cryptoEvent mx_cryptoEvent_icon", {
mx_cryptoEvent_icon_verified: request.phase === VerificationPhase.Done,
});
return (
<EventTileBubble
className={classes}
title={title}
subtitle={userLabelForEventRoom(client, request.otherUserId, mxEvent.getRoomId()!)}
timestamp={this.props.timestamp}
/>
);
}
// request hasn't concluded yet
if (request.pending) {
return false;
}

return null;
// User isn't actually verified
const userVerificationStatus = await matrixClient.getCrypto()?.getUserVerificationStatus(request.otherUserId);
if (!userVerificationStatus?.isVerified()) {
return false;
}

return true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ limitations under the License.
*/

import React from "react";
import { render } from "@testing-library/react";
import { render, waitFor } from "@testing-library/react";
import { EventEmitter } from "events";
import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
Expand All @@ -24,20 +24,26 @@ import {
Phase as VerificationPhase,
VerificationRequest,
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import { mocked } from "jest-mock";

import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import MKeyVerificationConclusion from "../../../../src/components/views/messages/MKeyVerificationConclusion";
import { getMockClientWithEventEmitter } from "../../../test-utils";
import { MKeyVerificationConclusion } from "../../../../src/components/views/messages/MKeyVerificationConclusion";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";

const trustworthy = { isCrossSigningVerified: () => true } as unknown as UserTrustLevel;
const untrustworthy = { isCrossSigningVerified: () => false } as unknown as UserTrustLevel;
const trustworthy = { isVerified: () => true } as unknown as UserTrustLevel;
const untrustworthy = { isVerified: () => false } as unknown as UserTrustLevel;

describe("MKeyVerificationConclusion", () => {
const userId = "@user:server";

const mockCrypto = mocked({
getUserVerificationStatus: jest.fn(),
});
const mockClient = getMockClientWithEventEmitter({
getRoom: jest.fn(),
getUserId: jest.fn().mockReturnValue(userId),
checkUserTrust: jest.fn(),
getCrypto: jest.fn().mockReturnValue(mockCrypto),
});

const getMockVerificationRequest = ({
Expand Down Expand Up @@ -69,9 +75,17 @@ describe("MKeyVerificationConclusion", () => {
) as unknown as VerificationRequest;
};

function renderComponent(event: MatrixEvent) {
return render(
<MatrixClientContext.Provider value={mockClient}>
<MKeyVerificationConclusion mxEvent={event} />
</MatrixClientContext.Provider>,
);
}

beforeEach(() => {
jest.clearAllMocks();
mockClient.checkUserTrust.mockReturnValue(trustworthy);
mockCrypto.getUserVerificationStatus.mockReturnValue(trustworthy);
});

afterAll(() => {
Expand All @@ -80,77 +94,77 @@ describe("MKeyVerificationConclusion", () => {

it("shouldn't render if there's no verificationRequest", () => {
const event = new MatrixEvent({});
const { container } = render(<MKeyVerificationConclusion mxEvent={event} />);
expect(container).toBeEmpty();
const { container } = renderComponent(event);
expect(container).toBeEmptyDOMElement();
});

it("shouldn't render if the verificationRequest is pending", () => {
const event = new MatrixEvent({});
event.verificationRequest = getMockVerificationRequest({ pending: true });
const { container } = render(<MKeyVerificationConclusion mxEvent={event} />);
expect(container).toBeEmpty();
const { container } = renderComponent(event);
expect(container).toBeEmptyDOMElement();
});

it("shouldn't render if the event type is cancel but the request type isn't", () => {
const event = new MatrixEvent({ type: EventType.KeyVerificationCancel });
event.verificationRequest = getMockVerificationRequest({});
const { container } = render(<MKeyVerificationConclusion mxEvent={event} />);
expect(container).toBeEmpty();
const { container } = renderComponent(event);
expect(container).toBeEmptyDOMElement();
});

it("shouldn't render if the event type is done but the request type isn't", () => {
const event = new MatrixEvent({ type: "m.key.verification.done" });
event.verificationRequest = getMockVerificationRequest({});
const { container } = render(<MKeyVerificationConclusion mxEvent={event} />);
expect(container).toBeEmpty();
const { container } = renderComponent(event);
expect(container).toBeEmptyDOMElement();
});

it("shouldn't render if the user isn't actually trusted", () => {
mockClient.checkUserTrust.mockReturnValue(untrustworthy);
mockCrypto.getUserVerificationStatus.mockReturnValue(untrustworthy);

const event = new MatrixEvent({ type: "m.key.verification.done" });
event.verificationRequest = getMockVerificationRequest({ phase: VerificationPhase.Done });
const { container } = render(<MKeyVerificationConclusion mxEvent={event} />);
expect(container).toBeEmpty();
const { container } = renderComponent(event);
expect(container).toBeEmptyDOMElement();
});

it("should rerender appropriately if user trust status changes", () => {
mockClient.checkUserTrust.mockReturnValue(untrustworthy);
it("should rerender appropriately if user trust status changes", async () => {
mockCrypto.getUserVerificationStatus.mockReturnValue(untrustworthy);

const event = new MatrixEvent({ type: "m.key.verification.done" });
event.verificationRequest = getMockVerificationRequest({
phase: VerificationPhase.Done,
otherUserId: "@someuser:domain",
});
const { container } = render(<MKeyVerificationConclusion mxEvent={event} />);
expect(container).toBeEmpty();
const { container } = renderComponent(event);
expect(container).toBeEmptyDOMElement();

mockClient.checkUserTrust.mockReturnValue(trustworthy);
mockCrypto.getUserVerificationStatus.mockReturnValue(trustworthy);

/* Ensure we don't rerender for every trust status change of any user */
mockClient.emit(
CryptoEvent.UserTrustStatusChanged,
"@anotheruser:domain",
new UserTrustLevel(true, true, true),
);
expect(container).toBeEmpty();
expect(container).toBeEmptyDOMElement();

/* But when our user changes, we do rerender */
mockClient.emit(
CryptoEvent.UserTrustStatusChanged,
event.verificationRequest.otherUserId,
new UserTrustLevel(true, true, true),
);
expect(container).not.toBeEmpty();
await waitFor(() => expect(container).not.toBeEmptyDOMElement());
});

it("should render appropriately if we cancelled the verification", () => {
it("should render appropriately if we cancelled the verification", async () => {
const event = new MatrixEvent({ type: "m.key.verification.cancel" });
event.verificationRequest = getMockVerificationRequest({
phase: VerificationPhase.Cancelled,
cancellingUserId: userId,
});
const { container } = render(<MKeyVerificationConclusion mxEvent={event} />);
expect(container).toHaveTextContent("You cancelled verifying");
const { container } = renderComponent(event);
await waitFor(() => expect(container).toHaveTextContent("You cancelled verifying"));
});
});