Skip to content

Commit 7ef7ea7

Browse files
BillCarsonFrtoger5
authored andcommitted
crypto: Add new ClientEvent.ReceivedToDeviceMessage with proper OlmEncryptionInfo support (#4891)
* crypto: Add new ClientEvent.ReceivedToDeviceMessage refactor rename ProcessedToDeviceEvent to ReceivedToDeviceEvent * fix: Restore legacy isEncrypted() for to-device messages * Update test for new preprocessToDeviceMessages API * quick fix on doc * quick update docs and renaming * review: Better doc and names for OlmEncryptionInfo * review: Remove IToDeviceMessage alias and only keep IToDeviceEvent * review: improve comments of processToDeviceMessages * review: pass up encrypted event when no crypto callbacks * review: use single payload for ReceivedToDeviceMessage * fix linter * review: minor comment update
1 parent f9757e4 commit 7ef7ea7

File tree

9 files changed

+304
-61
lines changed

9 files changed

+304
-61
lines changed

spec/integ/crypto/to-device-messages.spec.ts

Lines changed: 121 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,22 @@ limitations under the License.
1717
import fetchMock from "fetch-mock-jest";
1818
import "fake-indexeddb/auto";
1919
import { IDBFactory } from "fake-indexeddb";
20+
import Olm from "@matrix-org/olm";
2021

2122
import { getSyncResponse, syncPromise } from "../../test-utils/test-utils";
22-
import { createClient, type MatrixClient } from "../../../src";
23+
import {
24+
ClientEvent,
25+
createClient,
26+
type IToDeviceEvent,
27+
type MatrixClient,
28+
type MatrixEvent,
29+
type ReceivedToDeviceMessage,
30+
} from "../../../src";
2331
import * as testData from "../../test-utils/test-data";
2432
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
2533
import { SyncResponder } from "../../test-utils/SyncResponder";
2634
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
35+
import { encryptOlmEvent, establishOlmSession, getTestOlmAccountKeys } from "./olm-utils.ts";
2736

2837
afterEach(() => {
2938
// reset fake-indexeddb after each test, to make sure we don't leak connections
@@ -43,6 +52,8 @@ describe("to-device-messages", () => {
4352

4453
/** an object which intercepts `/keys/query` requests on the test homeserver */
4554
let e2eKeyResponder: E2EKeyResponder;
55+
let e2eKeyReceiver: E2EKeyReceiver;
56+
let syncResponder: SyncResponder;
4657

4758
beforeEach(
4859
async () => {
@@ -59,8 +70,8 @@ describe("to-device-messages", () => {
5970
});
6071

6172
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
62-
new E2EKeyReceiver(homeserverUrl);
63-
const syncResponder = new SyncResponder(homeserverUrl);
73+
e2eKeyReceiver = new E2EKeyReceiver(homeserverUrl);
74+
syncResponder = new SyncResponder(homeserverUrl);
6475

6576
// add bob as known user
6677
syncResponder.sendOrQueueSyncResponse(getSyncResponse([testData.BOB_TEST_USER_ID]));
@@ -149,4 +160,111 @@ describe("to-device-messages", () => {
149160
// for future: check that bob's device can decrypt the ciphertext?
150161
});
151162
});
163+
164+
describe("receive to-device-messages", () => {
165+
it("Should receive decrypted to-device message via ClientEvent", async () => {
166+
// create a test olm device which we will use to communicate with alice. We use libolm to implement this.
167+
await Olm.init();
168+
const testOlmAccount = new Olm.Account();
169+
testOlmAccount.create();
170+
171+
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
172+
e2eKeyResponder.addDeviceKeys(testDeviceKeys);
173+
174+
await aliceClient.startClient();
175+
await syncPromise(aliceClient);
176+
177+
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"]));
178+
await syncPromise(aliceClient);
179+
180+
const p2pSession = await establishOlmSession(aliceClient, e2eKeyReceiver, syncResponder, testOlmAccount);
181+
182+
const toDeviceEvent = encryptOlmEvent({
183+
sender: "@bob:xyz",
184+
senderKey: testDeviceKeys.keys[`curve25519:DEVICE_ID`],
185+
senderSigningKey: testDeviceKeys.keys[`ed25519:DEVICE_ID`],
186+
p2pSession: p2pSession,
187+
recipient: aliceClient.getUserId()!,
188+
recipientCurve25519Key: e2eKeyReceiver.getDeviceKey(),
189+
recipientEd25519Key: e2eKeyReceiver.getSigningKey(),
190+
plaincontent: {
191+
body: "foo",
192+
},
193+
plaintype: "m.test.type",
194+
});
195+
196+
const processedToDeviceResolver: PromiseWithResolvers<ReceivedToDeviceMessage> = Promise.withResolvers();
197+
198+
aliceClient.on(ClientEvent.ReceivedToDeviceMessage, (payload) => {
199+
processedToDeviceResolver.resolve(payload);
200+
});
201+
202+
const oldToDeviceResolver: PromiseWithResolvers<MatrixEvent> = Promise.withResolvers();
203+
204+
aliceClient.on(ClientEvent.ToDeviceEvent, (event) => {
205+
oldToDeviceResolver.resolve(event);
206+
});
207+
208+
expect(toDeviceEvent.type).toBe("m.room.encrypted");
209+
210+
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [toDeviceEvent] } });
211+
await syncPromise(aliceClient);
212+
213+
const { message, encryptionInfo } = await processedToDeviceResolver.promise;
214+
215+
expect(message.type).toBe("m.test.type");
216+
expect(message.content["body"]).toBe("foo");
217+
218+
expect(encryptionInfo).not.toBeNull();
219+
expect(encryptionInfo!.senderVerified).toBe(false);
220+
expect(encryptionInfo!.sender).toBe("@bob:xyz");
221+
expect(encryptionInfo!.senderDevice).toBe("DEVICE_ID");
222+
223+
const oldFormat = await oldToDeviceResolver.promise;
224+
expect(oldFormat.isEncrypted()).toBe(true);
225+
expect(oldFormat.getType()).toBe("m.test.type");
226+
expect(oldFormat.getContent()["body"]).toBe("foo");
227+
});
228+
229+
it("Should receive clear to-device message via ClientEvent", async () => {
230+
await aliceClient.startClient();
231+
await syncPromise(aliceClient);
232+
233+
const toDeviceEvent: IToDeviceEvent = {
234+
sender: "@bob:xyz",
235+
type: "m.test.type",
236+
content: {
237+
body: "foo",
238+
},
239+
};
240+
241+
const processedToDeviceResolver: PromiseWithResolvers<ReceivedToDeviceMessage> = Promise.withResolvers();
242+
243+
aliceClient.on(ClientEvent.ReceivedToDeviceMessage, (payload) => {
244+
processedToDeviceResolver.resolve(payload);
245+
});
246+
247+
const oldToDeviceResolver: PromiseWithResolvers<MatrixEvent> = Promise.withResolvers();
248+
249+
aliceClient.on(ClientEvent.ToDeviceEvent, (event) => {
250+
oldToDeviceResolver.resolve(event);
251+
});
252+
253+
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [toDeviceEvent] } });
254+
await syncPromise(aliceClient);
255+
256+
const { message, encryptionInfo } = await processedToDeviceResolver.promise;
257+
258+
expect(message.type).toBe("m.test.type");
259+
expect(message.content["body"]).toBe("foo");
260+
261+
// When the message is not encrypted, we don't have the encryptionInfo.
262+
expect(encryptionInfo).toBeNull();
263+
264+
const oldFormat = await oldToDeviceResolver.promise;
265+
expect(oldFormat.isEncrypted()).toBe(false);
266+
expect(oldFormat.getType()).toBe("m.test.type");
267+
expect(oldFormat.getContent()["body"]).toBe("foo");
268+
});
269+
});
152270
});

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -551,11 +551,11 @@ describe("RustCrypto", () => {
551551
const inputs: IToDeviceEvent[] = [
552552
{ content: { key: "value" }, type: "org.matrix.test", sender: "@alice:example.com" },
553553
];
554-
const res = await rustCrypto.preprocessToDeviceMessages(inputs);
554+
const res = (await rustCrypto.preprocessToDeviceMessages(inputs)).map((p) => p.message);
555555
expect(res).toEqual(inputs);
556556
});
557557

558-
it("should pass through bad encrypted messages", async () => {
558+
it("should fail to process bad encrypted messages", async () => {
559559
const olmMachine: OlmMachine = rustCrypto["olmMachine"];
560560
const keys = olmMachine.identityKeys;
561561
const inputs: IToDeviceEvent[] = [
@@ -576,7 +576,7 @@ describe("RustCrypto", () => {
576576
];
577577

578578
const res = await rustCrypto.preprocessToDeviceMessages(inputs);
579-
expect(res).toEqual(inputs);
579+
expect(res.length).toEqual(0);
580580
});
581581

582582
it("emits VerificationRequestReceived on incoming m.key.verification.request", async () => {

src/client.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,12 @@ import { type IIdentityServerProvider } from "./@types/IIdentityServerProvider.t
8787
import { type MatrixScheduler } from "./scheduler.ts";
8888
import { type BeaconEvent, type BeaconEventHandlerMap } from "./models/beacon.ts";
8989
import { type AuthDict } from "./interactive-auth.ts";
90-
import { type IMinimalEvent, type IRoomEvent, type IStateEvent } from "./sync-accumulator.ts";
90+
import {
91+
type IMinimalEvent,
92+
type IRoomEvent,
93+
type IStateEvent,
94+
type ReceivedToDeviceMessage,
95+
} from "./sync-accumulator.ts";
9196
import type { EventTimelineSet } from "./models/event-timeline-set.ts";
9297
import * as ContentHelpers from "./content-helpers.ts";
9398
import {
@@ -885,7 +890,9 @@ const EVENT_ID_PREFIX = "$";
885890
export enum ClientEvent {
886891
Sync = "sync",
887892
Event = "event",
893+
/** @deprecated Use {@link ReceivedToDeviceMessage}. */
888894
ToDeviceEvent = "toDeviceEvent",
895+
ReceivedToDeviceMessage = "receivedToDeviceMessage",
889896
AccountData = "accountData",
890897
Room = "Room",
891898
DeleteRoom = "deleteRoom",
@@ -1088,6 +1095,20 @@ export type ClientEventHandlerMap = {
10881095
* ```
10891096
*/
10901097
[ClientEvent.ToDeviceEvent]: (event: MatrixEvent) => void;
1098+
/**
1099+
* Fires whenever the SDK receives a new to-device message.
1100+
* @param payload - The message and encryptionInfo for this message (See {@link ReceivedToDeviceMessage}) which caused this event to fire.
1101+
* @example
1102+
* ```
1103+
* matrixClient.on("receivedToDeviceMessage", function(payload){
1104+
* const { message, encryptionInfo } = payload;
1105+
* var claimed_sender = encryptionInfo ? encryptionInfo.sender : message.sender;
1106+
* var isVerified = encryptionInfo ? encryptionInfo.verified : false;
1107+
* var type = message.type;
1108+
* });
1109+
* ```
1110+
*/
1111+
[ClientEvent.ReceivedToDeviceMessage]: (payload: ReceivedToDeviceMessage) => void;
10911112
/**
10921113
* Fires if a to-device event is received that cannot be decrypted.
10931114
* Encrypted to-device events will (generally) use plain Olm encryption,

src/common-crypto/CryptoBackend.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator.ts";
17+
import type { IDeviceLists, IToDeviceEvent, ReceivedToDeviceMessage } from "../sync-accumulator.ts";
1818
import { type IClearEvent, type MatrixEvent } from "../models/event.ts";
1919
import { type Room } from "../models/room.ts";
2020
import { type CryptoApi, type DecryptionFailureCode, type ImportRoomKeysOpts } from "../crypto-api/index.ts";
@@ -96,9 +96,11 @@ export interface SyncCryptoCallbacks {
9696
* messages, rather than the results of any decryption attempts.
9797
*
9898
* @param events - the received to-device messages
99-
* @returns A list of preprocessed to-device messages.
99+
* @returns A list of preprocessed to-device messages. This will not map 1:1 to the input list, as some messages may be invalid or
100+
* failed to decrypt, and so will be omitted from the output list.
101+
*
100102
*/
101-
preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]>;
103+
preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<ReceivedToDeviceMessage[]>;
102104

103105
/**
104106
* Called by the /sync loop when one time key counts and unused fallback key details are received.

src/crypto-api/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,6 +1365,27 @@ export interface OwnDeviceKeys {
13651365
curve25519: string;
13661366
}
13671367

1368+
/**
1369+
* Information about the encryption of a successfully decrypted to-device message.
1370+
*/
1371+
export interface OlmEncryptionInfo {
1372+
/** The user ID of the event sender, note this is untrusted data unless `isVerified` is true **/
1373+
sender: string;
1374+
/**
1375+
* The device ID of the device that sent us the event.
1376+
* Note this is untrusted data unless {@link senderVerified} is true.
1377+
* If the device ID is not known, this will be `null`.
1378+
**/
1379+
senderDevice?: string;
1380+
/** The sender device's public Curve25519 key, base64 encoded **/
1381+
senderCurve25519KeyBase64: string;
1382+
/**
1383+
* If true, this message is guaranteed to be authentic as it is coming from a device belonging to a user that we have verified.
1384+
* This is the state at the time of decryption (the user could be verified later).
1385+
*/
1386+
senderVerified: boolean;
1387+
}
1388+
13681389
export * from "./verification.ts";
13691390
export type * from "./keybackup.ts";
13701391
export * from "./recovery-key.ts";

src/rust-crypto/rust-crypto.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
1919

2020
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto.ts";
2121
import { KnownMembership } from "../@types/membership.ts";
22-
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator.ts";
22+
import { type IDeviceLists, type IToDeviceEvent, type ReceivedToDeviceMessage } from "../sync-accumulator.ts";
2323
import type { ToDevicePayload, ToDeviceBatch } from "../models/ToDeviceMessage.ts";
2424
import { type MatrixEvent, MatrixEventEvent } from "../models/event.ts";
2525
import { type Room } from "../models/room.ts";
@@ -1481,7 +1481,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
14811481
* @param oneTimeKeysCounts - the received one time key counts
14821482
* @param unusedFallbackKeys - the received unused fallback keys
14831483
* @param devices - the received device list updates
1484-
* @returns A list of preprocessed to-device messages.
1484+
* @returns A list of processed to-device messages.
14851485
*/
14861486
private async receiveSyncChanges({
14871487
events,
@@ -1493,38 +1493,70 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
14931493
oneTimeKeysCounts?: Map<string, number>;
14941494
unusedFallbackKeys?: Set<string>;
14951495
devices?: RustSdkCryptoJs.DeviceLists;
1496-
}): Promise<IToDeviceEvent[]> {
1497-
const result = await this.olmMachine.receiveSyncChanges(
1496+
}): Promise<RustSdkCryptoJs.ProcessedToDeviceEvent[]> {
1497+
return await this.olmMachine.receiveSyncChanges(
14981498
events ? JSON.stringify(events) : "[]",
14991499
devices,
15001500
oneTimeKeysCounts,
15011501
unusedFallbackKeys,
15021502
);
1503-
1504-
return result.map((processed) => JSON.parse(processed.rawEvent));
15051503
}
15061504

15071505
/** called by the sync loop to preprocess incoming to-device messages
15081506
*
15091507
* @param events - the received to-device messages
15101508
* @returns A list of preprocessed to-device messages.
15111509
*/
1512-
public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]> {
1510+
public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<ReceivedToDeviceMessage[]> {
15131511
// send the received to-device messages into receiveSyncChanges. We have no info on device-list changes,
15141512
// one-time-keys, or fallback keys, so just pass empty data.
15151513
const processed = await this.receiveSyncChanges({ events });
15161514

1517-
// look for interesting to-device messages
1515+
const received: ReceivedToDeviceMessage[] = [];
1516+
15181517
for (const message of processed) {
1519-
if (message.type === EventType.KeyVerificationRequest) {
1520-
const sender = message.sender;
1521-
const transactionId = message.content.transaction_id;
1518+
const parsedMessage: IToDeviceEvent = JSON.parse(message.rawEvent);
1519+
1520+
// look for interesting to-device messages
1521+
if (parsedMessage.type === EventType.KeyVerificationRequest) {
1522+
const sender = parsedMessage.sender;
1523+
const transactionId = parsedMessage.content.transaction_id;
15221524
if (transactionId && sender) {
15231525
this.onIncomingKeyVerificationRequest(sender, transactionId);
15241526
}
15251527
}
1528+
1529+
switch (message.type) {
1530+
case RustSdkCryptoJs.ProcessedToDeviceEventType.Decrypted: {
1531+
const encryptionInfo = (message as RustSdkCryptoJs.DecryptedToDeviceEvent).encryptionInfo;
1532+
received.push({
1533+
message: parsedMessage,
1534+
encryptionInfo: {
1535+
sender: encryptionInfo.sender.toString(),
1536+
senderDevice: encryptionInfo.senderDevice?.toString(),
1537+
senderCurve25519KeyBase64: encryptionInfo.senderCurve25519Key,
1538+
senderVerified: encryptionInfo.isSenderVerified(),
1539+
},
1540+
});
1541+
break;
1542+
}
1543+
case RustSdkCryptoJs.ProcessedToDeviceEventType.PlainText: {
1544+
received.push({
1545+
message: parsedMessage,
1546+
encryptionInfo: null,
1547+
});
1548+
break;
1549+
}
1550+
case RustSdkCryptoJs.ProcessedToDeviceEventType.UnableToDecrypt:
1551+
// ignore messages we cannot decrypt
1552+
break;
1553+
case RustSdkCryptoJs.ProcessedToDeviceEventType.Invalid:
1554+
// ignore invalid messages
1555+
break;
1556+
}
15261557
}
1527-
return processed;
1558+
1559+
return received;
15281560
}
15291561

15301562
/** called by the sync loop to process one time key counts and unused fallback keys

0 commit comments

Comments
 (0)