Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ElementR: Add CryptoApi.findVerificationRequestDMInProgress #3601

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5c5dacb
Add `CryptoApi.findVerificationRequestDMInProgress`
florianduros Jul 13, 2023
b548bd1
Merge branch 'develop' into florianduros/element-r/findVerificationRe…
florianduros Jul 13, 2023
eabe332
Fix linting and missing parameters
florianduros Jul 13, 2023
9023af8
Merge branch 'develop' into florianduros/element-r/findVerificationRe…
florianduros Jul 13, 2023
708a88f
Move `ROOM_ID` into `test-data`
florianduros Jul 13, 2023
2fd336f
Remove verification request from `EventDecryptor` pending list
florianduros Jul 13, 2023
79a4ab6
Merge branch 'develop' into florianduros/element-r/findVerificationRe…
florianduros Jul 13, 2023
3c38952
Merge branch 'develop' into florianduros/element-r/findVerificationRe…
florianduros Jul 17, 2023
034a74b
Fix duplicate timeline event processing
florianduros Jul 17, 2023
be60c05
Add extra documentation
florianduros Jul 17, 2023
b8045e2
Try to fix sonar error
florianduros Jul 17, 2023
60328c2
Use `roomId`
florianduros Jul 18, 2023
0be3dc3
Fix typo
florianduros Jul 18, 2023
3368fec
Review changes
florianduros Jul 18, 2023
6c6726b
Review changes
florianduros Jul 19, 2023
da5eaf0
Fix `initRustCrypto` jsdoc
florianduros Jul 20, 2023
4d3725d
Merge branch 'develop' into florianduros/element-r/findVerificationRe…
florianduros Jul 20, 2023
1ad1555
Listen to `ClientEvent.Event` instead of `RoomEvent.Timeline`
florianduros Jul 20, 2023
2e947d1
Merge branch 'develop' into florianduros/element-r/findVerificationRe…
florianduros Jul 21, 2023
8624350
Fix missing room id in `generate-test-data.py`
florianduros Jul 25, 2023
19d7ee4
Merge branch 'develop' into florianduros/element-r/findVerificationRe…
florianduros Jul 26, 2023
16adc86
Review changes
florianduros Jul 26, 2023
caee179
Merge branch 'develop' into florianduros/element-r/findVerificationRe…
florianduros Jul 27, 2023
1d0a7b6
Review changes
florianduros Jul 27, 2023
06dcec8
Handle encrypted event
florianduros Jul 28, 2023
265c870
Merge branch 'develop' into florianduros/element-r/findVerificationRe…
florianduros Jul 28, 2023
b638e0b
Fix linting
florianduros Jul 28, 2023
e2dc71a
Comments and run timers
florianduros Jul 28, 2023
8840d4b
Ignore 404
florianduros Jul 28, 2023
4b32853
Merge branch 'develop' into florianduros/element-r/findVerificationRe…
florianduros Jul 28, 2023
ca8779a
Fix test
florianduros Jul 31, 2023
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
270 changes: 212 additions & 58 deletions spec/integ/crypto/crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,31 @@ import fetchMock from "fetch-mock-jest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import { MockResponse, MockResponseFunction } from "fetch-mock";
import Olm from "@matrix-org/olm";

import type { IDeviceKeys } from "../../../src/@types/crypto";
import * as testUtils from "../../test-utils/test-utils";
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import { CRYPTO_BACKENDS, getSyncResponse, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import { TEST_ROOM_ID, TEST_ROOM_ID as ROOM_ID, TEST_USER_ID } from "../../test-utils/test-data";
import { TestClient } from "../../TestClient";
import { logger } from "../../../src/logger";
import {
Category,
createClient,
IClaimOTKsResult,
IContent,
IDownloadKeyResult,
IEvent,
IJoinedRoom,
IndexedDBCryptoStore,
IStartClientOpts,
ISyncResponse,
MatrixClient,
MatrixEvent,
MatrixEventEvent,
PendingEventOrdering,
Room,
RoomMember,
RoomStateEvent,
IRoomEvent,
} from "../../../src/matrix";
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
import { E2EKeyReceiver, IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
Expand All @@ -53,8 +55,7 @@ import { flushPromises } from "../../test-utils/flushPromises";
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { CryptoCallbacks } from "../../../src/crypto-api";

const ROOM_ID = "!room:id";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";

afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
Expand Down Expand Up @@ -156,18 +157,23 @@ function encryptMegolmEvent(opts: {
expect(opts.room_id).toBeTruthy();
plaintext.room_id = opts.room_id;
}
return encryptMegolmEventRawPlainText({ senderKey: opts.senderKey, groupSession: opts.groupSession, plaintext });
return encryptMegolmEventRawPlainText({
senderKey: opts.senderKey,
groupSession: opts.groupSession,
plaintext,
});
}

function encryptMegolmEventRawPlainText(opts: {
senderKey: string;
groupSession: Olm.OutboundGroupSession;
plaintext: Partial<IEvent>;
origin_server_ts?: number;
}): IEvent {
return {
event_id: "$test_megolm_event_" + Math.random(),
sender: "@not_the_real_sender:example.com",
origin_server_ts: 1672944778000,
sender: opts.plaintext.sender ?? "@not_the_real_sender:example.com",
origin_server_ts: opts.plaintext.origin_server_ts ?? 1672944778000,
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: opts.groupSession.encrypt(JSON.stringify(opts.plaintext)),
Expand Down Expand Up @@ -213,55 +219,6 @@ function encryptGroupSessionKey(opts: {
});
}

// get a /sync response which contains a single room (ROOM_ID), with the members given
function getSyncResponse(roomMembers: string[]): ISyncResponse {
const roomResponse: IJoinedRoom = {
summary: {
"m.heroes": [],
"m.joined_member_count": roomMembers.length,
"m.invited_member_count": roomMembers.length,
},
state: {
events: [
testUtils.mkEventCustom({
sender: roomMembers[0],
type: "m.room.encryption",
state_key: "",
content: {
algorithm: "m.megolm.v1.aes-sha2",
},
}),
],
},
timeline: {
events: [],
prev_batch: "",
},
ephemeral: { events: [] },
account_data: { events: [] },
unread_notifications: {},
};

for (let i = 0; i < roomMembers.length; i++) {
roomResponse.state.events.push(
testUtils.mkMembershipCustom({
membership: "join",
sender: roomMembers[i],
}),
);
}

return {
next_batch: "1",
rooms: {
join: { [ROOM_ID]: roomResponse },
invite: {},
leave: {},
},
account_data: { events: [] },
};
}

/**
* Establish an Olm Session with the test user
*
Expand Down Expand Up @@ -415,7 +372,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
let aliceClient: MatrixClient;

/** an object which intercepts `/keys/upload` requests from {@link #aliceClient} to catch the uploaded keys */
let keyReceiver: IE2EKeyReceiver;
let keyReceiver: E2EKeyReceiver;

/** an object which intercepts `/keys/query` requests on the test homeserver */
let keyResponder: E2EKeyResponder;

/** an object which intercepts `/sync` requests from {@link #aliceClient} */
let syncResponder: ISyncResponder;
Expand Down Expand Up @@ -586,6 +546,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,

afterEach(async () => {
await aliceClient.stopClient();

// Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync();

fetchMock.mockReset();
});

Expand Down Expand Up @@ -708,6 +672,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
await syncPromise(aliceClient);

await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true });
expect(event.isDecryptionFailure()).toBeFalsy();
expect(event.getContent().body).toEqual("42");
});

Expand Down Expand Up @@ -2411,4 +2376,193 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
expect(selfSigningKey[secretStorageKey]).toBeDefined();
});
});

describe("Incoming verification in a DM", () => {
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);

keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
keyResponder.addKeyReceiver(TEST_USER_ID, keyReceiver);

expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
});

afterEach(() => {
jest.useRealTimers();
});

/**
* Return a verification request event from Bob
* @see https://spec.matrix.org/v1.7/client-server-api/#mkeyverificationrequest
*/
function createVerificationRequestEvent(): IRoomEvent {
return {
content: {
body: "Verification request from Bob to Alice",
from_device: "BobDevice",
methods: ["m.sas.v1"],
msgtype: "m.key.verification.request",
to: aliceClient.getUserId()!,
},
event_id: "$143273582443PhrSn:example.org",
origin_server_ts: Date.now(),
room_id: TEST_ROOM_ID,
sender: "@bob:xyz",
type: "m.room.message",
unsigned: {
age: 1234,
},
};
}

/**
* Create a to-device event
* @param groupSession
* @param p2pSession
*/
function createToDeviceEvent(groupSession: Olm.OutboundGroupSession, p2pSession: Olm.Session): Partial<IEvent> {
return encryptGroupSessionKey({
recipient: aliceClient.getUserId()!,
recipientCurve25519Key: keyReceiver.getDeviceKey(),
recipientEd25519Key: keyReceiver.getSigningKey(),
olmAccount: testOlmAccount,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
}

/**
* Create and encrypt a verification request event
* @param groupSession
*/
function createEncryptedMessage(groupSession: Olm.OutboundGroupSession): IEvent {
return encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
plaintext: createVerificationRequestEvent(),
});
}

newBackendOnly("Verification request from Bob to Alice", async () => {
// Tell alice she is sharing a room with bob
const syncResponse = getSyncResponse(["@bob:xyz"]);

// Add verification request from Bob to Alice in the DM between them
syncResponse.rooms[Category.Join][TEST_ROOM_ID].timeline.events.push(createVerificationRequestEvent());
syncResponder.sendOrQueueSyncResponse(syncResponse);
// Wait for the sync response to be processed
await syncPromise(aliceClient);

const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
// Expect to find the verification request received during the sync
expect(request?.roomId).toBe(TEST_ROOM_ID);
expect(request?.isSelfVerification).toBe(false);
expect(request?.otherUserId).toBe("@bob:xyz");
});

newBackendOnly("Verification request not found", async () => {
// Tell alice she is sharing a room with bob
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"]));
// Wait for the sync response to be processed
await syncPromise(aliceClient);

// Expect to not find any verification request
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
expect(request).not.toBeDefined();
});

newBackendOnly("Process encrypted verification request", async () => {
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();

// make the room_key event, but don't send it yet
const toDeviceEvent = createToDeviceEvent(groupSession, p2pSession);

// Add verification request from Bob to Alice in the DM between them
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
rooms: { join: { [ROOM_ID]: { timeline: { events: [createEncryptedMessage(groupSession)] } } } },
});
// Wait for the sync response to be processed
await syncPromise(aliceClient);

const room = aliceClient.getRoom(ROOM_ID)!;
const matrixEvent = room.getLiveTimeline().getEvents()[0];

// wait for a first attempt at decryption: should fail
await testUtils.awaitDecryption(matrixEvent);
expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted");

// Send the Bob's keys
syncResponder.sendOrQueueSyncResponse({
next_batch: 2,
to_device: {
events: [toDeviceEvent],
},
});
await syncPromise(aliceClient);

// Wait for the message to be decrypted
await testUtils.awaitDecryption(matrixEvent, { waitOnDecryptionFailure: true });

const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
// Expect to find the verification request received during the sync
expect(request?.roomId).toBe(TEST_ROOM_ID);
expect(request?.isSelfVerification).toBe(false);
expect(request?.otherUserId).toBe("@bob:xyz");
});

newBackendOnly(
"If Bob keys are not received in the 5mins after the verification request, the request is ignored",
async () => {
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();

// make the room_key event, but don't send it yet
const toDeviceEvent = createToDeviceEvent(groupSession, p2pSession);

jest.useFakeTimers();

// Add verification request from Bob to Alice in the DM between them
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
rooms: { join: { [ROOM_ID]: { timeline: { events: [createEncryptedMessage(groupSession)] } } } },
});
// Wait for the sync response to be processed
await syncPromise(aliceClient);

const room = aliceClient.getRoom(ROOM_ID)!;
const matrixEvent = room.getLiveTimeline().getEvents()[0];

// wait for a first attempt at decryption: should fail
await testUtils.awaitDecryption(matrixEvent);
expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted");

// Advance time by 5mins, the verification request should be ignored after that
jest.advanceTimersByTime(5 * 60 * 1000);

// Send the Bob's keys
syncResponder.sendOrQueueSyncResponse({
next_batch: 2,
to_device: {
events: [toDeviceEvent],
},
});
await syncPromise(aliceClient);

// Wait for the message to be decrypted
await testUtils.awaitDecryption(matrixEvent, { waitOnDecryptionFailure: true });

const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
// the request should not be present
expect(request).not.toBeDefined();
},
);
});
});
2 changes: 2 additions & 0 deletions spec/test-utils/test-data/generate-test-data.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
# input data
TEST_USER_ID = "@alice:localhost"
TEST_DEVICE_ID = "test_device"
TEST_ROOM_ID = "!room:id"
# any 32-byte string can be an ed25519 private key.
TEST_DEVICE_PRIVATE_KEY_BYTES = b"deadbeefdeadbeefdeadbeefdeadbeef"

Expand Down Expand Up @@ -130,6 +131,7 @@ def main() -> None:

export const TEST_USER_ID = "{TEST_USER_ID}";
export const TEST_DEVICE_ID = "{TEST_DEVICE_ID}";
export const TEST_ROOM_ID = "{TEST_ROOM_ID}";

/** The base64-encoded public ed25519 key for this device */
export const TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "{b64_public_key}";
Expand Down
1 change: 1 addition & 0 deletions spec/test-utils/test-data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { KeyBackupInfo } from "../../../src/crypto-api";

export const TEST_USER_ID = "@alice:localhost";
export const TEST_DEVICE_ID = "test_device";
export const TEST_ROOM_ID = "!room:id";

/** The base64-encoded public ed25519 key for this device */
export const TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "YI/7vbGVLpGdYtuceQR8MSsKB/QjgfMXM1xqnn+0NWU";
Expand Down
Loading
Loading