You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-04 13:31:41 +03:00
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
This commit is contained in:
@@ -46,8 +46,8 @@ import {
|
|||||||
type Verifier,
|
type Verifier,
|
||||||
VerifierEvent,
|
VerifierEvent,
|
||||||
} from "../../../src/crypto-api/verification";
|
} from "../../../src/crypto-api/verification";
|
||||||
import { escapeRegExp } from "../../../src/utils";
|
import { escapeRegExp, sleep } from "../../../src/utils";
|
||||||
import { awaitDecryption, emitPromise, getSyncResponse, syncPromise } from "../../test-utils/test-utils";
|
import { awaitDecryption, emitPromise, getSyncResponse, syncPromise, waitFor } from "../../test-utils/test-utils";
|
||||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||||
import {
|
import {
|
||||||
BACKUP_DECRYPTION_KEY_BASE64,
|
BACKUP_DECRYPTION_KEY_BASE64,
|
||||||
@@ -79,11 +79,6 @@ import {
|
|||||||
import { type KeyBackupInfo, CryptoEvent } from "../../../src/crypto-api";
|
import { type KeyBackupInfo, CryptoEvent } from "../../../src/crypto-api";
|
||||||
import { encodeBase64 } from "../../../src/base64";
|
import { encodeBase64 } from "../../../src/base64";
|
||||||
|
|
||||||
// The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations
|
|
||||||
// to ensure that we don't end up with dangling timeouts.
|
|
||||||
// But the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
|
||||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
// we use the libolm primitives in the test, so init the Olm library
|
// we use the libolm primitives in the test, so init the Olm library
|
||||||
await Olm.init();
|
await Olm.init();
|
||||||
@@ -96,6 +91,13 @@ beforeAll(async () => {
|
|||||||
await RustSdkCryptoJs.initAsync();
|
await RustSdkCryptoJs.initAsync();
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations
|
||||||
|
// to ensure that we don't end up with dangling timeouts.
|
||||||
|
// But the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||||
|
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||||
@@ -1080,6 +1082,13 @@ describe("verification", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("ignores old verification requests", async () => {
|
it("ignores old verification requests", async () => {
|
||||||
|
const debug = jest.fn();
|
||||||
|
const info = jest.fn();
|
||||||
|
const warn = jest.fn();
|
||||||
|
|
||||||
|
// @ts-ignore overriding RustCrypto's logger
|
||||||
|
aliceClient.getCrypto()!.logger = { debug, info, warn };
|
||||||
|
|
||||||
const eventHandler = jest.fn();
|
const eventHandler = jest.fn();
|
||||||
aliceClient.on(CryptoEvent.VerificationRequestReceived, eventHandler);
|
aliceClient.on(CryptoEvent.VerificationRequestReceived, eventHandler);
|
||||||
|
|
||||||
@@ -1094,6 +1103,16 @@ describe("verification", () => {
|
|||||||
const matrixEvent = room.getLiveTimeline().getEvents()[0];
|
const matrixEvent = room.getLiveTimeline().getEvents()[0];
|
||||||
expect(matrixEvent.getId()).toEqual(verificationRequestEvent.event_id);
|
expect(matrixEvent.getId()).toEqual(verificationRequestEvent.event_id);
|
||||||
|
|
||||||
|
// Wait until the request has been processed. We use a real sleep()
|
||||||
|
// here to make sure any background async tasks are completed.
|
||||||
|
jest.useRealTimers();
|
||||||
|
await waitFor(async () => {
|
||||||
|
expect(info).toHaveBeenCalledWith(
|
||||||
|
expect.stringMatching(/^Ignoring just-received verification request/),
|
||||||
|
);
|
||||||
|
sleep(100);
|
||||||
|
});
|
||||||
|
|
||||||
// check that an event has not been raised, and that the request is not found
|
// check that an event has not been raised, and that the request is not found
|
||||||
expect(eventHandler).not.toHaveBeenCalled();
|
expect(eventHandler).not.toHaveBeenCalled();
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -2419,6 +2419,135 @@ describe("RustCrypto", () => {
|
|||||||
expect(mockOlmMachine.receiveRoomKeyBundle.mock.calls[0][1]).toEqual(new TextEncoder().encode("asdfghjkl"));
|
expect(mockOlmMachine.receiveRoomKeyBundle.mock.calls[0][1]).toEqual(new TextEncoder().encode("asdfghjkl"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Verification requests", () => {
|
||||||
|
it("fetches device details before room verification requests", async () => {
|
||||||
|
// Given a RustCrypto
|
||||||
|
const olmMachine = mockedOlmMachine();
|
||||||
|
const outgoingRequestProcessor = mockedOutgoingRequestProcessor();
|
||||||
|
const rustCrypto = makeRustCrypto(olmMachine, outgoingRequestProcessor);
|
||||||
|
|
||||||
|
// When we receive a room verification request
|
||||||
|
const event = mockedEvent("!r:s.co", "@u:s.co", "m.room.message", "m.key.verification.request");
|
||||||
|
await rustCrypto.onLiveEventFromSync(event);
|
||||||
|
|
||||||
|
// Then we first fetch device details
|
||||||
|
expect(outgoingRequestProcessor.makeOutgoingRequest).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// And we handle the verification event as normal
|
||||||
|
expect(olmMachine.receiveVerificationEvent).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not fetch device details before other verification events", async () => {
|
||||||
|
// Given a RustCrypto
|
||||||
|
const olmMachine = mockedOlmMachine();
|
||||||
|
const outgoingRequestProcessor = mockedOutgoingRequestProcessor();
|
||||||
|
const rustCrypto = makeRustCrypto(olmMachine, outgoingRequestProcessor);
|
||||||
|
|
||||||
|
// When we receive some verification event that is not a room request
|
||||||
|
const event = mockedEvent("!r:s.co", "@u:s.co", "m.key.verification.start");
|
||||||
|
await rustCrypto.onLiveEventFromSync(event);
|
||||||
|
|
||||||
|
// Then we do not fetch device details
|
||||||
|
expect(outgoingRequestProcessor.makeOutgoingRequest).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// And we handle the verification event as normal
|
||||||
|
expect(olmMachine.receiveVerificationEvent).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws an error if sender is missing", async () => {
|
||||||
|
// Given a RustCrypto
|
||||||
|
const olmMachine = mockedOlmMachine();
|
||||||
|
const outgoingRequestProcessor = mockedOutgoingRequestProcessor();
|
||||||
|
const rustCrypto = makeRustCrypto(olmMachine, outgoingRequestProcessor);
|
||||||
|
|
||||||
|
// When we receive a verification event without a sender
|
||||||
|
// Then we throw
|
||||||
|
const event = mockedEvent("!r:s.co", null, "m.key.verification.start");
|
||||||
|
|
||||||
|
await expect(async () => await rustCrypto.onLiveEventFromSync(event)).rejects.toThrow(
|
||||||
|
"missing sender in the event",
|
||||||
|
);
|
||||||
|
|
||||||
|
// And we do not fetch device details or handle the event
|
||||||
|
expect(outgoingRequestProcessor.makeOutgoingRequest).not.toHaveBeenCalled();
|
||||||
|
expect(olmMachine.receiveVerificationEvent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws an error if room is missing", async () => {
|
||||||
|
// Given a RustCrypto
|
||||||
|
const olmMachine = mockedOlmMachine();
|
||||||
|
const outgoingRequestProcessor = mockedOutgoingRequestProcessor();
|
||||||
|
const rustCrypto = makeRustCrypto(olmMachine, outgoingRequestProcessor);
|
||||||
|
|
||||||
|
// When we receive a verification event without a sender
|
||||||
|
// Then we throw
|
||||||
|
const event = mockedEvent(null, "@u:s.co", "m.key.verification.start");
|
||||||
|
|
||||||
|
await expect(async () => await rustCrypto.onLiveEventFromSync(event)).rejects.toThrow(
|
||||||
|
"missing roomId in the event",
|
||||||
|
);
|
||||||
|
|
||||||
|
// And we do not fetch device details or handle the event
|
||||||
|
expect(outgoingRequestProcessor.makeOutgoingRequest).not.toHaveBeenCalled();
|
||||||
|
expect(olmMachine.receiveVerificationEvent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
function mockedOlmMachine(): Mocked<OlmMachine> {
|
||||||
|
return {
|
||||||
|
queryKeysForUsers: jest.fn(),
|
||||||
|
getVerificationRequest: jest.fn(),
|
||||||
|
receiveVerificationEvent: jest.fn(),
|
||||||
|
} as unknown as Mocked<OlmMachine>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeRustCrypto(
|
||||||
|
olmMachine: OlmMachine,
|
||||||
|
outgoingRequestProcessor: OutgoingRequestProcessor,
|
||||||
|
): RustCrypto {
|
||||||
|
const rustCrypto = new RustCrypto(
|
||||||
|
new DebugLogger(debug("test Verification requests")),
|
||||||
|
olmMachine,
|
||||||
|
{} as unknown as MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||||
|
TEST_USER,
|
||||||
|
TEST_DEVICE_ID,
|
||||||
|
{} as ServerSideSecretStorage,
|
||||||
|
{} as CryptoCallbacks,
|
||||||
|
);
|
||||||
|
|
||||||
|
// @ts-ignore mocking outgoingRequestProcessor
|
||||||
|
rustCrypto.outgoingRequestProcessor = outgoingRequestProcessor;
|
||||||
|
|
||||||
|
return rustCrypto;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockedOutgoingRequestProcessor(): OutgoingRequestProcessor {
|
||||||
|
return {
|
||||||
|
makeOutgoingRequest: jest.fn(),
|
||||||
|
} as unknown as Mocked<OutgoingRequestProcessor>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockedEvent(
|
||||||
|
roomId: string | null,
|
||||||
|
senderId: string | null,
|
||||||
|
eventType: string,
|
||||||
|
msgtype?: string | undefined,
|
||||||
|
): MatrixEvent {
|
||||||
|
return {
|
||||||
|
isState: jest.fn().mockReturnValue(false),
|
||||||
|
getUnsigned: jest.fn().mockReturnValue({}),
|
||||||
|
isDecryptionFailure: jest.fn(),
|
||||||
|
isEncrypted: jest.fn(),
|
||||||
|
getType: jest.fn().mockReturnValue(eventType),
|
||||||
|
getRoomId: jest.fn().mockReturnValue(roomId),
|
||||||
|
getSender: jest.fn().mockReturnValue(senderId),
|
||||||
|
getId: jest.fn(),
|
||||||
|
getStateKey: jest.fn(),
|
||||||
|
getContent: jest.fn().mockReturnValue({ msgtype: msgtype }),
|
||||||
|
getTs: jest.fn(),
|
||||||
|
} as unknown as MatrixEvent;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Build a MatrixHttpApi instance */
|
/** Build a MatrixHttpApi instance */
|
||||||
|
|||||||
@@ -2053,20 +2053,38 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
*/
|
*/
|
||||||
private async onKeyVerificationEvent(event: MatrixEvent): Promise<void> {
|
private async onKeyVerificationEvent(event: MatrixEvent): Promise<void> {
|
||||||
const roomId = event.getRoomId();
|
const roomId = event.getRoomId();
|
||||||
|
const senderId = event.getSender();
|
||||||
|
|
||||||
if (!roomId) {
|
if (!roomId) {
|
||||||
throw new Error("missing roomId in the event");
|
throw new Error("missing roomId in the event");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!senderId) {
|
||||||
|
throw new Error("missing sender in the event");
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Incoming verification event ${event.getId()} type ${event.getType()} from ${event.getSender()}`,
|
`Incoming verification event ${event.getId()} type ${event.getType()} from ${event.getSender()}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.olmMachine.receiveVerificationEvent(
|
const isRoomVerificationRequest =
|
||||||
|
event.getType() === EventType.RoomMessage && event.getContent().msgtype === MsgType.KeyVerificationRequest;
|
||||||
|
|
||||||
|
if (isRoomVerificationRequest) {
|
||||||
|
// Before processing an in-room verification request, we need to
|
||||||
|
// make sure we have the sender's device information - otherwise we
|
||||||
|
// will immediately abort verification. So we explicitly fetch it
|
||||||
|
// from /keys/query and wait for that request to complete before we
|
||||||
|
// call receiveVerificationEvent.
|
||||||
|
const req = this.getOlmMachineOrThrow().queryKeysForUsers([new RustSdkCryptoJs.UserId(senderId)]);
|
||||||
|
await this.outgoingRequestProcessor.makeOutgoingRequest(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.getOlmMachineOrThrow().receiveVerificationEvent(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
event_id: event.getId(),
|
event_id: event.getId(),
|
||||||
type: event.getType(),
|
type: event.getType(),
|
||||||
sender: event.getSender(),
|
sender: senderId,
|
||||||
state_key: event.getStateKey(),
|
state_key: event.getStateKey(),
|
||||||
content: event.getContent(),
|
content: event.getContent(),
|
||||||
origin_server_ts: event.getTs(),
|
origin_server_ts: event.getTs(),
|
||||||
@@ -2074,11 +2092,8 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
new RustSdkCryptoJs.RoomId(roomId),
|
new RustSdkCryptoJs.RoomId(roomId),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (isRoomVerificationRequest) {
|
||||||
event.getType() === EventType.RoomMessage &&
|
this.onIncomingKeyVerificationRequest(senderId, event.getId()!);
|
||||||
event.getContent().msgtype === MsgType.KeyVerificationRequest
|
|
||||||
) {
|
|
||||||
this.onIncomingKeyVerificationRequest(event.getSender()!, event.getId()!);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// that may have caused us to queue up outgoing requests, so make sure we send them.
|
// that may have caused us to queue up outgoing requests, so make sure we send them.
|
||||||
|
|||||||
Reference in New Issue
Block a user