Skip to content

Commit a213d17

Browse files
authored
Fetch the user's device info before processing a verification request (#5030)
* Use checked way to get OlmMachine * Factor out two variables in onKeyVerificationEvent * Make sure verification test waits for the request to be processed * Fetch the user's device info before processing a verification request If we don't have the device info for a user when we receive their verification request, we ignore it. This change gives us the best possible chance of having the right device data before we try to process the verification. Fixes #30693 Fixes #27819
1 parent e885ecf commit a213d17

File tree

3 files changed

+177
-14
lines changed

3 files changed

+177
-14
lines changed

spec/integ/crypto/verification.spec.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ import {
4646
type Verifier,
4747
VerifierEvent,
4848
} from "../../../src/crypto-api/verification";
49-
import { escapeRegExp } from "../../../src/utils";
50-
import { awaitDecryption, emitPromise, getSyncResponse, syncPromise } from "../../test-utils/test-utils";
49+
import { escapeRegExp, sleep } from "../../../src/utils";
50+
import { awaitDecryption, emitPromise, getSyncResponse, syncPromise, waitFor } from "../../test-utils/test-utils";
5151
import { SyncResponder } from "../../test-utils/SyncResponder";
5252
import {
5353
BACKUP_DECRYPTION_KEY_BASE64,
@@ -79,11 +79,6 @@ import {
7979
import { type KeyBackupInfo, CryptoEvent } from "../../../src/crypto-api";
8080
import { encodeBase64 } from "../../../src/base64";
8181

82-
// The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations
83-
// to ensure that we don't end up with dangling timeouts.
84-
// But the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
85-
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
86-
8782
beforeAll(async () => {
8883
// we use the libolm primitives in the test, so init the Olm library
8984
await Olm.init();
@@ -96,6 +91,13 @@ beforeAll(async () => {
9691
await RustSdkCryptoJs.initAsync();
9792
}, 10000);
9893

94+
beforeEach(() => {
95+
// The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations
96+
// to ensure that we don't end up with dangling timeouts.
97+
// But the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
98+
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
99+
});
100+
99101
afterEach(() => {
100102
// reset fake-indexeddb after each test, to make sure we don't leak connections
101103
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
@@ -1080,6 +1082,13 @@ describe("verification", () => {
10801082
});
10811083

10821084
it("ignores old verification requests", async () => {
1085+
const debug = jest.fn();
1086+
const info = jest.fn();
1087+
const warn = jest.fn();
1088+
1089+
// @ts-ignore overriding RustCrypto's logger
1090+
aliceClient.getCrypto()!.logger = { debug, info, warn };
1091+
10831092
const eventHandler = jest.fn();
10841093
aliceClient.on(CryptoEvent.VerificationRequestReceived, eventHandler);
10851094

@@ -1094,6 +1103,16 @@ describe("verification", () => {
10941103
const matrixEvent = room.getLiveTimeline().getEvents()[0];
10951104
expect(matrixEvent.getId()).toEqual(verificationRequestEvent.event_id);
10961105

1106+
// Wait until the request has been processed. We use a real sleep()
1107+
// here to make sure any background async tasks are completed.
1108+
jest.useRealTimers();
1109+
await waitFor(async () => {
1110+
expect(info).toHaveBeenCalledWith(
1111+
expect.stringMatching(/^Ignoring just-received verification request/),
1112+
);
1113+
sleep(100);
1114+
});
1115+
10971116
// check that an event has not been raised, and that the request is not found
10981117
expect(eventHandler).not.toHaveBeenCalled();
10991118
expect(

spec/unit/rust-crypto/rust-crypto.spec.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2419,6 +2419,135 @@ describe("RustCrypto", () => {
24192419
expect(mockOlmMachine.receiveRoomKeyBundle.mock.calls[0][1]).toEqual(new TextEncoder().encode("asdfghjkl"));
24202420
});
24212421
});
2422+
2423+
describe("Verification requests", () => {
2424+
it("fetches device details before room verification requests", async () => {
2425+
// Given a RustCrypto
2426+
const olmMachine = mockedOlmMachine();
2427+
const outgoingRequestProcessor = mockedOutgoingRequestProcessor();
2428+
const rustCrypto = makeRustCrypto(olmMachine, outgoingRequestProcessor);
2429+
2430+
// When we receive a room verification request
2431+
const event = mockedEvent("!r:s.co", "@u:s.co", "m.room.message", "m.key.verification.request");
2432+
await rustCrypto.onLiveEventFromSync(event);
2433+
2434+
// Then we first fetch device details
2435+
expect(outgoingRequestProcessor.makeOutgoingRequest).toHaveBeenCalled();
2436+
2437+
// And we handle the verification event as normal
2438+
expect(olmMachine.receiveVerificationEvent).toHaveBeenCalled();
2439+
});
2440+
2441+
it("does not fetch device details before other verification events", async () => {
2442+
// Given a RustCrypto
2443+
const olmMachine = mockedOlmMachine();
2444+
const outgoingRequestProcessor = mockedOutgoingRequestProcessor();
2445+
const rustCrypto = makeRustCrypto(olmMachine, outgoingRequestProcessor);
2446+
2447+
// When we receive some verification event that is not a room request
2448+
const event = mockedEvent("!r:s.co", "@u:s.co", "m.key.verification.start");
2449+
await rustCrypto.onLiveEventFromSync(event);
2450+
2451+
// Then we do not fetch device details
2452+
expect(outgoingRequestProcessor.makeOutgoingRequest).not.toHaveBeenCalled();
2453+
2454+
// And we handle the verification event as normal
2455+
expect(olmMachine.receiveVerificationEvent).toHaveBeenCalled();
2456+
});
2457+
2458+
it("throws an error if sender is missing", async () => {
2459+
// Given a RustCrypto
2460+
const olmMachine = mockedOlmMachine();
2461+
const outgoingRequestProcessor = mockedOutgoingRequestProcessor();
2462+
const rustCrypto = makeRustCrypto(olmMachine, outgoingRequestProcessor);
2463+
2464+
// When we receive a verification event without a sender
2465+
// Then we throw
2466+
const event = mockedEvent("!r:s.co", null, "m.key.verification.start");
2467+
2468+
await expect(async () => await rustCrypto.onLiveEventFromSync(event)).rejects.toThrow(
2469+
"missing sender in the event",
2470+
);
2471+
2472+
// And we do not fetch device details or handle the event
2473+
expect(outgoingRequestProcessor.makeOutgoingRequest).not.toHaveBeenCalled();
2474+
expect(olmMachine.receiveVerificationEvent).not.toHaveBeenCalled();
2475+
});
2476+
2477+
it("throws an error if room is missing", async () => {
2478+
// Given a RustCrypto
2479+
const olmMachine = mockedOlmMachine();
2480+
const outgoingRequestProcessor = mockedOutgoingRequestProcessor();
2481+
const rustCrypto = makeRustCrypto(olmMachine, outgoingRequestProcessor);
2482+
2483+
// When we receive a verification event without a sender
2484+
// Then we throw
2485+
const event = mockedEvent(null, "@u:s.co", "m.key.verification.start");
2486+
2487+
await expect(async () => await rustCrypto.onLiveEventFromSync(event)).rejects.toThrow(
2488+
"missing roomId in the event",
2489+
);
2490+
2491+
// And we do not fetch device details or handle the event
2492+
expect(outgoingRequestProcessor.makeOutgoingRequest).not.toHaveBeenCalled();
2493+
expect(olmMachine.receiveVerificationEvent).not.toHaveBeenCalled();
2494+
});
2495+
2496+
function mockedOlmMachine(): Mocked<OlmMachine> {
2497+
return {
2498+
queryKeysForUsers: jest.fn(),
2499+
getVerificationRequest: jest.fn(),
2500+
receiveVerificationEvent: jest.fn(),
2501+
} as unknown as Mocked<OlmMachine>;
2502+
}
2503+
2504+
function makeRustCrypto(
2505+
olmMachine: OlmMachine,
2506+
outgoingRequestProcessor: OutgoingRequestProcessor,
2507+
): RustCrypto {
2508+
const rustCrypto = new RustCrypto(
2509+
new DebugLogger(debug("test Verification requests")),
2510+
olmMachine,
2511+
{} as unknown as MatrixHttpApi<IHttpOpts & { onlyData: true }>,
2512+
TEST_USER,
2513+
TEST_DEVICE_ID,
2514+
{} as ServerSideSecretStorage,
2515+
{} as CryptoCallbacks,
2516+
);
2517+
2518+
// @ts-ignore mocking outgoingRequestProcessor
2519+
rustCrypto.outgoingRequestProcessor = outgoingRequestProcessor;
2520+
2521+
return rustCrypto;
2522+
}
2523+
2524+
function mockedOutgoingRequestProcessor(): OutgoingRequestProcessor {
2525+
return {
2526+
makeOutgoingRequest: jest.fn(),
2527+
} as unknown as Mocked<OutgoingRequestProcessor>;
2528+
}
2529+
2530+
function mockedEvent(
2531+
roomId: string | null,
2532+
senderId: string | null,
2533+
eventType: string,
2534+
msgtype?: string | undefined,
2535+
): MatrixEvent {
2536+
return {
2537+
isState: jest.fn().mockReturnValue(false),
2538+
getUnsigned: jest.fn().mockReturnValue({}),
2539+
isDecryptionFailure: jest.fn(),
2540+
isEncrypted: jest.fn(),
2541+
getType: jest.fn().mockReturnValue(eventType),
2542+
getRoomId: jest.fn().mockReturnValue(roomId),
2543+
getSender: jest.fn().mockReturnValue(senderId),
2544+
getId: jest.fn(),
2545+
getStateKey: jest.fn(),
2546+
getContent: jest.fn().mockReturnValue({ msgtype: msgtype }),
2547+
getTs: jest.fn(),
2548+
} as unknown as MatrixEvent;
2549+
}
2550+
});
24222551
});
24232552

24242553
/** Build a MatrixHttpApi instance */

src/rust-crypto/rust-crypto.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2053,32 +2053,47 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
20532053
*/
20542054
private async onKeyVerificationEvent(event: MatrixEvent): Promise<void> {
20552055
const roomId = event.getRoomId();
2056+
const senderId = event.getSender();
20562057

20572058
if (!roomId) {
20582059
throw new Error("missing roomId in the event");
20592060
}
20602061

2062+
if (!senderId) {
2063+
throw new Error("missing sender in the event");
2064+
}
2065+
20612066
this.logger.debug(
20622067
`Incoming verification event ${event.getId()} type ${event.getType()} from ${event.getSender()}`,
20632068
);
20642069

2065-
await this.olmMachine.receiveVerificationEvent(
2070+
const isRoomVerificationRequest =
2071+
event.getType() === EventType.RoomMessage && event.getContent().msgtype === MsgType.KeyVerificationRequest;
2072+
2073+
if (isRoomVerificationRequest) {
2074+
// Before processing an in-room verification request, we need to
2075+
// make sure we have the sender's device information - otherwise we
2076+
// will immediately abort verification. So we explicitly fetch it
2077+
// from /keys/query and wait for that request to complete before we
2078+
// call receiveVerificationEvent.
2079+
const req = this.getOlmMachineOrThrow().queryKeysForUsers([new RustSdkCryptoJs.UserId(senderId)]);
2080+
await this.outgoingRequestProcessor.makeOutgoingRequest(req);
2081+
}
2082+
2083+
await this.getOlmMachineOrThrow().receiveVerificationEvent(
20662084
JSON.stringify({
20672085
event_id: event.getId(),
20682086
type: event.getType(),
2069-
sender: event.getSender(),
2087+
sender: senderId,
20702088
state_key: event.getStateKey(),
20712089
content: event.getContent(),
20722090
origin_server_ts: event.getTs(),
20732091
}),
20742092
new RustSdkCryptoJs.RoomId(roomId),
20752093
);
20762094

2077-
if (
2078-
event.getType() === EventType.RoomMessage &&
2079-
event.getContent().msgtype === MsgType.KeyVerificationRequest
2080-
) {
2081-
this.onIncomingKeyVerificationRequest(event.getSender()!, event.getId()!);
2095+
if (isRoomVerificationRequest) {
2096+
this.onIncomingKeyVerificationRequest(senderId, event.getId()!);
20822097
}
20832098

20842099
// that may have caused us to queue up outgoing requests, so make sure we send them.

0 commit comments

Comments
 (0)