From ca07b18c37c7db449f685baa848bc609cae80c5f Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sat, 31 Aug 2024 15:52:45 +0100 Subject: [PATCH 01/20] Add CryptoApi. encryptToDeviceMessages Deprecate Crypto. encryptAndSendToDevices and MatrixClient. encryptAndSendToDevices --- package.json | 2 +- src/client.ts | 2 + src/crypto-api/index.ts | 17 ++++ src/crypto/index.ts | 144 +++++++++++++++++++++------------ src/rust-crypto/rust-crypto.ts | 32 ++++++++ 5 files changed, 146 insertions(+), 51 deletions(-) diff --git a/package.json b/package.json index bb652a728f2..49049dbf56c 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ ], "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/matrix-sdk-crypto-wasm": "^7.0.0", + "@matrix-org/matrix-sdk-crypto-wasm": "github:matrix-org/matrix-rust-sdk-crypto-wasm#9a167f7ca220cfb7e192d5312e3159dcce5391d4", "@matrix-org/olm": "3.2.15", "another-json": "^0.2.0", "bs58": "^6.0.0", diff --git a/src/client.ts b/src/client.ts index 5bc6ab07120..82dfaaa68eb 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3217,6 +3217,8 @@ export class MatrixClient extends TypedEventEmitter[], payload: object): Promise { if (!this.crypto) { diff --git a/src/crypto-api/index.ts b/src/crypto-api/index.ts index 6b9db3811dc..0a88f70f0a9 100644 --- a/src/crypto-api/index.ts +++ b/src/crypto-api/index.ts @@ -16,6 +16,7 @@ limitations under the License. import type { SecretsBundle } from "@matrix-org/matrix-sdk-crypto-wasm"; import type { IMegolmSessionData } from "../@types/crypto.ts"; +import type { ToDeviceBatch, ToDevicePayload } from "../models/ToDeviceMessage.ts"; import { Room } from "../models/room.ts"; import { DeviceMap } from "../models/device.ts"; import { UIAuthCallback } from "../interactive-auth.ts"; @@ -550,6 +551,22 @@ export interface CryptoApi { * @param secrets - The secrets bundle received from the other device */ importSecretsBundle?(secrets: Awaited>): Promise; + + /** + * Encrypts a given payload object via Olm to-device messages to a given + * set of devices. + * + * @param eventType the type of the event to send + * @param devices an array of (user ID, device ID) pairs to encrypt the payload for + * @param payload the payload to encrypt + * + * @returns a promise which resolves to the batch of encrypted payloads which can then be sent via {@link MatrixClient#queueToDevice} + */ + encryptToDeviceMessages( + eventType: string, + devices: { userId: string; deviceId: string }[], + payload: ToDevicePayload, + ): Promise; } /** A reason code for a failure to decrypt an event. */ diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 420160c0de4..8b21c13e01f 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -55,7 +55,7 @@ import { IStore } from "../store/index.ts"; import { Room, RoomEvent } from "../models/room.ts"; import { RoomMember, RoomMemberEvent } from "../models/room-member.ts"; import { EventStatus, IContent, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event.ts"; -import { ToDeviceBatch } from "../models/ToDeviceMessage.ts"; +import { ToDeviceBatch, ToDevicePayload } from "../models/ToDeviceMessage.ts"; import { ClientEvent, IKeysUploadResponse, ISignedKey, IUploadKeySignaturesResponse, MatrixClient } from "../client.ts"; import { IRoomEncryption, RoomList } from "./RoomList.ts"; import { IKeyBackupInfo } from "./keybackup.ts"; @@ -3522,60 +3522,17 @@ export class Crypto extends TypedEventEmitter[], payload: object): Promise { - const toDeviceBatch: ToDeviceBatch = { - eventType: EventType.RoomMessageEncrypted, - batch: [], - }; - try { - await Promise.all( - userDeviceInfoArr.map(async ({ userId, deviceInfo }) => { - const deviceId = deviceInfo.deviceId; - const encryptedContent: IEncryptedContent = { - algorithm: olmlib.OLM_ALGORITHM, - sender_key: this.olmDevice.deviceCurve25519Key!, - ciphertext: {}, - [ToDeviceMessageId]: uuidv4(), - }; - - toDeviceBatch.batch.push({ - userId, - deviceId, - payload: encryptedContent, - }); - - await olmlib.ensureOlmSessionsForDevices( - this.olmDevice, - this.baseApis, - new Map([[userId, [deviceInfo]]]), - ); - await olmlib.encryptMessageForDevice( - encryptedContent.ciphertext, - this.userId, - this.deviceId, - this.olmDevice, - userId, - deviceInfo, - payload, - ); - }), + const toDeviceBatch = await this.prepareToDeviceBatch( + EventType.RoomMessageEncrypted, + userDeviceInfoArr, + payload, ); - // prune out any devices that encryptMessageForDevice could not encrypt for, - // in which case it will have just not added anything to the ciphertext object. - // There's no point sending messages to devices if we couldn't encrypt to them, - // since that's effectively a blank message. - toDeviceBatch.batch = toDeviceBatch.batch.filter((msg) => { - if (Object.keys(msg.payload.ciphertext).length > 0) { - return true; - } else { - logger.log(`No ciphertext for device ${msg.userId}:${msg.deviceId}: pruning`); - return false; - } - }); - try { await this.baseApis.queueToDevice(toDeviceBatch); } catch (e) { @@ -4305,6 +4262,93 @@ export class Crypto extends TypedEventEmitter { throw new Error("Not implemented"); } + + private async prepareToDeviceBatch( + eventType: string, + userDeviceInfoArr: IOlmDevice[], + payload: object, + ): Promise { + const toDeviceBatch: ToDeviceBatch = { + eventType, + batch: [], + }; + + await Promise.all( + userDeviceInfoArr.map(async ({ userId, deviceInfo }) => { + const deviceId = deviceInfo.deviceId; + const encryptedContent: IEncryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this.olmDevice.deviceCurve25519Key!, + ciphertext: {}, + [ToDeviceMessageId]: uuidv4(), + }; + + toDeviceBatch.batch.push({ + userId, + deviceId, + payload: encryptedContent, + }); + + await olmlib.ensureOlmSessionsForDevices( + this.olmDevice, + this.baseApis, + new Map([[userId, [deviceInfo]]]), + ); + await olmlib.encryptMessageForDevice( + encryptedContent.ciphertext, + this.userId, + this.deviceId, + this.olmDevice, + userId, + deviceInfo, + payload, + ); + }), + ); + + // prune out any devices that encryptMessageForDevice could not encrypt for, + // in which case it will have just not added anything to the ciphertext object. + // There's no point sending messages to devices if we couldn't encrypt to them, + // since that's effectively a blank message. + toDeviceBatch.batch = toDeviceBatch.batch.filter((msg) => { + if (Object.keys(msg.payload.ciphertext).length > 0) { + return true; + } else { + logger.log(`No ciphertext for device ${msg.userId}:${msg.deviceId}: pruning`); + return false; + } + }); + + return toDeviceBatch; + } + + public async encryptToDeviceMessages( + eventType: string, + devices: { userId: string; deviceId: string }[], + payload: ToDevicePayload, + ): Promise { + const userIds = new Set(devices.map(({ userId }) => userId)); + const deviceInfoMap = await this.downloadKeys(Array.from(userIds), false); + + const userDeviceInfoArr: IOlmDevice[] = []; + + devices.forEach(({ userId, deviceId }) => { + const devices = deviceInfoMap.get(userId); + if (!devices) { + logger.warn(`No devices found for user ${userId}`); + return; + } + + if (devices.has(deviceId)) { + // Send the message to a specific device + userDeviceInfoArr.push({ userId, deviceInfo: devices.get(deviceId)! }); + } else { + logger.warn(`No device found for user ${userId} with id ${deviceId}`); + } + }); + + return this.prepareToDeviceBatch(eventType, userDeviceInfoArr, payload); + } } /** diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 2a4ba09c5eb..d73fc521ba9 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -21,6 +21,7 @@ import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypt import { KnownMembership } from "../@types/membership.ts"; import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator.ts"; import type { IEncryptedEventInfo } from "../crypto/api.ts"; +import type { ToDevicePayload, ToDeviceBatch } from "../models/ToDeviceMessage.ts"; import { MatrixEvent, MatrixEventEvent } from "../models/event.ts"; import { Room } from "../models/room.ts"; import { RoomMember } from "../models/room-member.ts"; @@ -1713,6 +1714,37 @@ export class RustCrypto extends TypedEventEmitter { return await this.olmMachine.getIdentity(new RustSdkCryptoJs.UserId(this.userId)); } + + public async encryptToDeviceMessages( + eventType: string, + devices: { userId: string; deviceId: string }[], + payload: ToDevicePayload, + ): Promise { + const batch: ToDeviceBatch = { + batch: [], + eventType, + }; + + for (const { userId, deviceId } of devices) { + const device: RustSdkCryptoJs.Device | undefined = await this.olmMachine.getDevice( + new RustSdkCryptoJs.UserId(userId), + new RustSdkCryptoJs.DeviceId(deviceId), + ); + + if (device) { + const encryptedPayload = JSON.parse(await device.encryptToDeviceEvent(eventType, payload)); + batch.batch.push({ + deviceId, + userId, + payload: encryptedPayload, + }); + } else { + this.logger.warn(`encryptToDeviceMessages: unknown device ${userId}:${deviceId}`); + } + } + + return batch; + } } class EventDecryptor { From 6a0d8e26385c34d40e8c2ed1e34cb5119c12456c Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 2 Sep 2024 12:08:02 +0100 Subject: [PATCH 02/20] Overload MatrixClient. encryptAndSendToDevices instead of deprecating --- src/client.ts | 41 +++++++++++++++++++++++++++++++---------- src/embedded.ts | 28 ++++++++++++++++++++++------ 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/client.ts b/src/client.ts index 82dfaaa68eb..27c6caa2433 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3209,22 +3209,43 @@ export class MatrixClient extends TypedEventEmitter[], payload: object): Promise { - if (!this.crypto) { + public encryptAndSendToDevices(userDeviceInfoArr: IOlmDevice[], payload: object): Promise; + public encryptAndSendToDevices(devices: { userId: string; deviceId: string }[], payload: object): Promise; + public async encryptAndSendToDevices( + devices: { userId: string; deviceId: string }[] | IOlmDevice[], + payload: object, + ): Promise { + if (devices.length == 0) { + return; + } + + if ("deviceinfo" in devices[0]) { + if (!this.crypto) { + throw new Error("End-to-End encryption disabled"); + } + return this.crypto.encryptAndSendToDevices(devices as IOlmDevice[], payload); + } + + const crypto = this.getCrypto(); + if (!crypto) { throw new Error("End-to-End encryption disabled"); } - return this.crypto.encryptAndSendToDevices(userDeviceInfoArr, payload); + + const { type } = payload as { type: string }; + + const batch = await crypto.encryptToDeviceMessages( + type, + devices as { userId: string; deviceId: string }[], + payload, + ); + await this.queueToDevice(batch); } /** diff --git a/src/embedded.ts b/src/embedded.ts index 1974ea9f98e..ce4bd79d5c8 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -401,14 +401,30 @@ export class RoomWidgetClient extends MatrixClient { await this.widgetApi.sendToDevice(eventType, false, recursiveMapToObject(contentMap)); } - public async encryptAndSendToDevices(userDeviceInfoArr: IOlmDevice[], payload: object): Promise { + public async encryptAndSendToDevices( + devices: { userId: string; deviceId: string }[] | IOlmDevice[], + payload: object, + ): Promise { // map: user Id → device Id → payload const contentMap: MapWithDefault> = new MapWithDefault(() => new Map()); - for (const { - userId, - deviceInfo: { deviceId }, - } of userDeviceInfoArr) { - contentMap.getOrCreate(userId).set(deviceId, payload); + + if (devices.length == 0) { + return; + } + + if ("deviceinfo" in devices[0]) { + // pre-CryptoApi style: + for (const { + userId, + deviceInfo: { deviceId }, + } of devices as IOlmDevice[]) { + contentMap.getOrCreate(userId).set(deviceId, payload); + } + } else { + // new CryptoApi style: + for (const { userId, deviceId } of devices as { userId: string; deviceId: string }[]) { + contentMap.getOrCreate(userId).set(deviceId, payload); + } } await this.widgetApi.sendToDevice((payload as { type: string }).type, true, recursiveMapToObject(contentMap)); From 354f5b91d0a226fced0dcf84e46c3ff51026e87c Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 2 Sep 2024 16:07:37 +0100 Subject: [PATCH 03/20] Revert "Overload MatrixClient. encryptAndSendToDevices instead of deprecating" This reverts commit 6a0d8e26385c34d40e8c2ed1e34cb5119c12456c. --- src/client.ts | 41 ++++++++++------------------------------- src/embedded.ts | 28 ++++++---------------------- 2 files changed, 16 insertions(+), 53 deletions(-) diff --git a/src/client.ts b/src/client.ts index 27c6caa2433..82dfaaa68eb 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3209,43 +3209,22 @@ export class MatrixClient extends TypedEventEmitter[], payload: object): Promise; - public encryptAndSendToDevices(devices: { userId: string; deviceId: string }[], payload: object): Promise; - public async encryptAndSendToDevices( - devices: { userId: string; deviceId: string }[] | IOlmDevice[], - payload: object, - ): Promise { - if (devices.length == 0) { - return; - } - - if ("deviceinfo" in devices[0]) { - if (!this.crypto) { - throw new Error("End-to-End encryption disabled"); - } - return this.crypto.encryptAndSendToDevices(devices as IOlmDevice[], payload); - } - - const crypto = this.getCrypto(); - if (!crypto) { + public encryptAndSendToDevices(userDeviceInfoArr: IOlmDevice[], payload: object): Promise { + if (!this.crypto) { throw new Error("End-to-End encryption disabled"); } - - const { type } = payload as { type: string }; - - const batch = await crypto.encryptToDeviceMessages( - type, - devices as { userId: string; deviceId: string }[], - payload, - ); - await this.queueToDevice(batch); + return this.crypto.encryptAndSendToDevices(userDeviceInfoArr, payload); } /** diff --git a/src/embedded.ts b/src/embedded.ts index ce4bd79d5c8..1974ea9f98e 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -401,30 +401,14 @@ export class RoomWidgetClient extends MatrixClient { await this.widgetApi.sendToDevice(eventType, false, recursiveMapToObject(contentMap)); } - public async encryptAndSendToDevices( - devices: { userId: string; deviceId: string }[] | IOlmDevice[], - payload: object, - ): Promise { + public async encryptAndSendToDevices(userDeviceInfoArr: IOlmDevice[], payload: object): Promise { // map: user Id → device Id → payload const contentMap: MapWithDefault> = new MapWithDefault(() => new Map()); - - if (devices.length == 0) { - return; - } - - if ("deviceinfo" in devices[0]) { - // pre-CryptoApi style: - for (const { - userId, - deviceInfo: { deviceId }, - } of devices as IOlmDevice[]) { - contentMap.getOrCreate(userId).set(deviceId, payload); - } - } else { - // new CryptoApi style: - for (const { userId, deviceId } of devices as { userId: string; deviceId: string }[]) { - contentMap.getOrCreate(userId).set(deviceId, payload); - } + for (const { + userId, + deviceInfo: { deviceId }, + } of userDeviceInfoArr) { + contentMap.getOrCreate(userId).set(deviceId, payload); } await this.widgetApi.sendToDevice((payload as { type: string }).type, true, recursiveMapToObject(contentMap)); From 28456ce673cedf1687334aa12de73d6144ae4642 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 2 Sep 2024 16:15:56 +0100 Subject: [PATCH 04/20] Feedback from code review --- src/crypto-api/index.ts | 32 ++++++++++++++++---------------- src/crypto/index.ts | 11 +++-------- src/rust-crypto/rust-crypto.ts | 5 ++++- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/crypto-api/index.ts b/src/crypto-api/index.ts index 0a88f70f0a9..362229dc0b2 100644 --- a/src/crypto-api/index.ts +++ b/src/crypto-api/index.ts @@ -341,6 +341,22 @@ export interface CryptoApi { */ getEncryptionInfoForEvent(event: MatrixEvent): Promise; + /** + * Encrypts a given payload object via Olm to-device messages to a given + * set of devices. + * + * @param eventType the type of the event to send + * @param devices an array of (user ID, device ID) pairs to encrypt the payload for + * @param payload the payload to encrypt + * + * @returns the batch of encrypted payloads which can then be sent via {@link MatrixClient#queueToDevice} + */ + encryptToDeviceMessages( + eventType: string, + devices: { userId: string; deviceId: string }[], + payload: ToDevicePayload, + ): Promise; + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Device/User verification @@ -551,22 +567,6 @@ export interface CryptoApi { * @param secrets - The secrets bundle received from the other device */ importSecretsBundle?(secrets: Awaited>): Promise; - - /** - * Encrypts a given payload object via Olm to-device messages to a given - * set of devices. - * - * @param eventType the type of the event to send - * @param devices an array of (user ID, device ID) pairs to encrypt the payload for - * @param payload the payload to encrypt - * - * @returns a promise which resolves to the batch of encrypted payloads which can then be sent via {@link MatrixClient#queueToDevice} - */ - encryptToDeviceMessages( - eventType: string, - devices: { userId: string; deviceId: string }[], - payload: ToDevicePayload, - ): Promise; } /** A reason code for a failure to decrypt an event. */ diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 8b21c13e01f..4a40975734a 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -3527,11 +3527,7 @@ export class Crypto extends TypedEventEmitter[], payload: object): Promise { try { - const toDeviceBatch = await this.prepareToDeviceBatch( - EventType.RoomMessageEncrypted, - userDeviceInfoArr, - payload, - ); + const toDeviceBatch = await this.prepareToDeviceBatch(userDeviceInfoArr, payload); try { await this.baseApis.queueToDevice(toDeviceBatch); @@ -4264,12 +4260,11 @@ export class Crypto extends TypedEventEmitter[], payload: object, ): Promise { const toDeviceBatch: ToDeviceBatch = { - eventType, + eventType: EventType.RoomMessageEncrypted, batch: [], }; @@ -4347,7 +4342,7 @@ export class Crypto extends TypedEventEmitter { + await this.olmMachine.getMissingSessions( + devices.map(({ userId, deviceId }) => new RustSdkCryptoJs.UserId(userId)), + ); const batch: ToDeviceBatch = { batch: [], - eventType, + eventType: EventType.RoomMessageEncrypted, }; for (const { userId, deviceId } of devices) { From 94d68e8e64795d7ccca9d77ffc5744ab8ee6b9cd Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 3 Sep 2024 10:02:43 +0100 Subject: [PATCH 05/20] Use temporary pre-release build of @matrix-org/matrix-sdk-crypto-wasm --- package.json | 2 +- yarn.lock | 36 +++++------------------------------- 2 files changed, 6 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 49049dbf56c..7353887bd0b 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ ], "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/matrix-sdk-crypto-wasm": "github:matrix-org/matrix-rust-sdk-crypto-wasm#9a167f7ca220cfb7e192d5312e3159dcce5391d4", + "@matrix-org/matrix-sdk-crypto-wasm": "https://floofy.netlify.app/matrix-org-matrix-sdk-crypto-wasm-v8.0.0-pre1.tgz", "@matrix-org/olm": "3.2.15", "another-json": "^0.2.0", "bs58": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index 49efed4ad35..b8d78a3c580 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1446,10 +1446,9 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@matrix-org/matrix-sdk-crypto-wasm@^7.0.0": +"@matrix-org/matrix-sdk-crypto-wasm@https://floofy.netlify.app/matrix-org-matrix-sdk-crypto-wasm-v8.0.0-pre1.tgz": version "7.0.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-7.0.0.tgz#8d6abdb9ded8656cc9e2a7909913a34bf3fc9b3a" - integrity sha512-MOencXiW/gI5MuTtCNsuojjwT5DXCrjMqv9xOslJC9h2tPdLFFFMGr58dY5Lis4DRd9MRWcgrGowUIHOqieWTA== + resolved "https://floofy.netlify.app/matrix-org-matrix-sdk-crypto-wasm-v8.0.0-pre1.tgz#cf0df87e75d8780e3756eb13dce817ae272cc071" "@matrix-org/olm@3.2.15": version "3.2.15" @@ -5770,16 +5769,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -5834,14 +5824,7 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -6373,16 +6356,7 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From 3266a69f5f1ac3efb4019257b95a0d46f31a8dd8 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 4 Sep 2024 16:07:38 +0100 Subject: [PATCH 06/20] Deduplicate user IDs --- src/rust-crypto/rust-crypto.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 9fc6b72a71a..b3e992fa2b9 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -1720,8 +1720,9 @@ export class RustCrypto extends TypedEventEmitter { + const uniqueUsers = new Set(devices.map(({ userId }) => userId)); await this.olmMachine.getMissingSessions( - devices.map(({ userId, deviceId }) => new RustSdkCryptoJs.UserId(userId)), + Array.from(uniqueUsers).map((userId) => new RustSdkCryptoJs.UserId(userId)), ); const batch: ToDeviceBatch = { batch: [], From 8afcc05750d0cab572ed4dd1cd54019b622d3cfb Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 4 Sep 2024 16:07:55 +0100 Subject: [PATCH 07/20] Test for RustCrypto implementation --- spec/unit/rust-crypto/rust-crypto.spec.ts | 118 ++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index 2b161c778f7..9c04c7f7dd1 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -1554,6 +1554,124 @@ describe("RustCrypto", () => { await expect(rustCrypto.exportSecretsBundle()).resolves.toEqual(expect.objectContaining(bundle)); }); }); + + describe("encryptToDeviceMessages", () => { + let rustCrypto: RustCrypto; + let testOlmMachine: RustSdkCryptoJs.OlmMachine; + + beforeEach(async () => { + testOlmMachine = await OlmMachine.initialize( + new RustSdkCryptoJs.UserId(testData.TEST_USER_ID), + new RustSdkCryptoJs.DeviceId(testData.TEST_DEVICE_ID), + ); + jest.spyOn(OlmMachine, "initFromStore").mockResolvedValue(testOlmMachine); + rustCrypto = await makeTestRustCrypto(); + expect(OlmMachine.initFromStore).toHaveBeenCalled(); + }); + + afterEach(() => { + testOlmMachine?.free(); + }); + + const payload = { hello: "world" }; + + it("returns empty batch if devices not known", async () => { + const getMissingSessions = jest.spyOn(testOlmMachine, "getMissingSessions"); + const getDevice = jest.spyOn(testOlmMachine, "getDevice"); + const batch = await rustCrypto.encryptToDeviceMessages( + "m.test.type", + [ + { deviceId: "AAA", userId: "@user1:domain" }, + { deviceId: "BBB", userId: "@user1:domain" }, + { deviceId: "CCC", userId: "@user2:domain" }, + ], + payload, + ); + expect(getMissingSessions.mock.calls[0][0].length).toBe(2); + expect(getDevice).toHaveBeenCalledTimes(3); + expect(batch?.eventType).toEqual("m.room.encrypted"); + expect(batch?.batch).toEqual([]); + }, 120000); + + it("returns encrypted batch for known devices", async () => { + // Make m aware of another device, and get some OTK to be able to establish a session. + await testOlmMachine.markRequestAsSent( + "foo", + RustSdkCryptoJs.RequestType.KeysQuery, + JSON.stringify({ + device_keys: { + "@example:localhost": { + AFGUOBTZWM: { + algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"], + device_id: "AFGUOBTZWM", + keys: { + "curve25519:AFGUOBTZWM": "boYjDpaC+7NkECQEeMh5dC+I1+AfriX0VXG2UV7EUQo", + "ed25519:AFGUOBTZWM": "NayrMQ33ObqMRqz6R9GosmHdT6HQ6b/RX/3QlZ2yiec", + }, + signatures: { + "@example:localhost": { + "ed25519:AFGUOBTZWM": + "RoSWvru1jj6fs2arnTedWsyIyBmKHMdOu7r9gDi0BZ61h9SbCK2zLXzuJ9ZFLao2VvA0yEd7CASCmDHDLYpXCA", + }, + }, + user_id: "@example:localhost", + unsigned: { + device_display_name: "rust-sdk", + }, + }, + }, + }, + failures: {}, + }), + ); + + await testOlmMachine.markRequestAsSent( + "bar", + RustSdkCryptoJs.RequestType.KeysClaim, + JSON.stringify({ + one_time_keys: { + "@example:localhost": { + AFGUOBTZWM: { + "signed_curve25519:AAAABQ": { + key: "9IGouMnkB6c6HOd4xUsNv4i3Dulb4IS96TzDordzOws", + signatures: { + "@example:localhost": { + "ed25519:AFGUOBTZWM": + "2bvUbbmJegrV0eVP/vcJKuIWC3kud+V8+C0dZtg4dVovOSJdTP/iF36tQn2bh5+rb9xLlSeztXBdhy4c+LiOAg", + }, + }, + }, + }, + }, + }, + failures: {}, + }), + ); + + const batch = await rustCrypto.encryptToDeviceMessages( + "m.test.type", + [ + { deviceId: "AAA", userId: "@user1:domain" }, + { deviceId: "BBB", userId: "@user1:domain" }, + { deviceId: "CCC", userId: "@user2:domain" }, + { deviceId: "AFGUOBTZWM", userId: "@example:localhost" }, + ], + payload, + ); + expect(batch?.eventType).toEqual("m.room.encrypted"); + expect(batch?.batch.length).toEqual(1); + expect(batch?.batch[0].deviceId).toEqual("AFGUOBTZWM"); + expect(batch?.batch[0].userId).toEqual("@example:localhost"); + expect(batch?.batch[0].payload).toEqual( + expect.objectContaining({ + "algorithm": "m.olm.v1.curve25519-aes-sha2", + "ciphertext": expect.any(Object), + "org.matrix.msgid": expect.any(String), + "sender_key": expect.any(String), + }), + ); + }); + }); }); /** Build a MatrixHttpApi instance */ From 7bb6a1b9742ffea34e89f95add87dbc7e14b38cd Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 4 Sep 2024 16:55:26 +0100 Subject: [PATCH 08/20] Use ensureSessionsForUsers() --- src/rust-crypto/rust-crypto.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index b3e992fa2b9..02b786c0789 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -31,7 +31,7 @@ import { DecryptionError, OnSyncCompletedData, } from "../common-crypto/CryptoBackend.ts"; -import { logger, Logger } from "../logger.ts"; +import { logger, Logger, LogSpan } from "../logger.ts"; import { IHttpOpts, MatrixHttpApi, Method } from "../http-api/index.ts"; import { RoomEncryptor } from "./RoomEncryptor.ts"; import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor.ts"; @@ -1720,8 +1720,14 @@ export class RustCrypto extends TypedEventEmitter { + const logger = new LogSpan(this.logger, "encryptToDeviceMessages"); const uniqueUsers = new Set(devices.map(({ userId }) => userId)); - await this.olmMachine.getMissingSessions( + + // This will ensure we have Olm sessions for all of the users' devices. + // However, we only care about some of the devices. + // So, perhaps we can optimise this later on. + await this.keyClaimManager.ensureSessionsForUsers( + logger, Array.from(uniqueUsers).map((userId) => new RustSdkCryptoJs.UserId(userId)), ); const batch: ToDeviceBatch = { From 687ce0db01880b3e18efa9a854caf238b21fa982 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 6 Sep 2024 17:06:18 +0100 Subject: [PATCH 09/20] Encrypt to-device messages in parallel --- src/rust-crypto/rust-crypto.ts | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 02b786c0789..33caffdb5df 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -1735,23 +1735,25 @@ export class RustCrypto extends TypedEventEmitter { + const device: RustSdkCryptoJs.Device | undefined = await this.olmMachine.getDevice( + new RustSdkCryptoJs.UserId(userId), + new RustSdkCryptoJs.DeviceId(deviceId), + ); - if (device) { - const encryptedPayload = JSON.parse(await device.encryptToDeviceEvent(eventType, payload)); - batch.batch.push({ - deviceId, - userId, - payload: encryptedPayload, - }); - } else { - this.logger.warn(`encryptToDeviceMessages: unknown device ${userId}:${deviceId}`); - } - } + if (device) { + const encryptedPayload = JSON.parse(await device.encryptToDeviceEvent(eventType, payload)); + batch.batch.push({ + deviceId, + userId, + payload: encryptedPayload, + }); + } else { + this.logger.warn(`encryptToDeviceMessages: unknown device ${userId}:${deviceId}`); + } + }), + ); return batch; } From 5e978b95e10864cc57ace78f4e569b77ae667729 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 6 Sep 2024 17:09:10 +0100 Subject: [PATCH 10/20] Use release version of matrix-sdk-crypto-wasm --- package.json | 2 +- yarn.lock | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 7353887bd0b..8fc85d8de40 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ ], "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/matrix-sdk-crypto-wasm": "https://floofy.netlify.app/matrix-org-matrix-sdk-crypto-wasm-v8.0.0-pre1.tgz", + "@matrix-org/matrix-sdk-crypto-wasm": "^8.0.0", "@matrix-org/olm": "3.2.15", "another-json": "^0.2.0", "bs58": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index b8d78a3c580..5ae07ebf146 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1446,9 +1446,10 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@matrix-org/matrix-sdk-crypto-wasm@https://floofy.netlify.app/matrix-org-matrix-sdk-crypto-wasm-v8.0.0-pre1.tgz": - version "7.0.0" - resolved "https://floofy.netlify.app/matrix-org-matrix-sdk-crypto-wasm-v8.0.0-pre1.tgz#cf0df87e75d8780e3756eb13dce817ae272cc071" +"@matrix-org/matrix-sdk-crypto-wasm@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-8.0.0.tgz" + integrity sha512-s0q3O2dK8b6hOJ+SZFz+s/IiMabmVsNue6r17sTwbrRD8liBkCrpjYnxoMYvtC01GggJ9TZLQbeqpt8hQSPHAg== "@matrix-org/olm@3.2.15": version "3.2.15" From f8a66084100fae68d0c154ef8e3fcfec63a75866 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 9 Sep 2024 10:45:01 +0100 Subject: [PATCH 11/20] Upgrade matrix-sdk-crypto-wasm to v8 --- package.json | 2 +- yarn.lock | 39 +++++++-------------------------------- 2 files changed, 8 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 9ac783ff7e7..9ed0c0800d3 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ ], "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/matrix-sdk-crypto-wasm": "^7.0.0", + "@matrix-org/matrix-sdk-crypto-wasm": "^8.0.0", "@matrix-org/olm": "3.2.15", "another-json": "^0.2.0", "bs58": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index ef354a7116e..5a33dbb3591 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1453,10 +1453,10 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@matrix-org/matrix-sdk-crypto-wasm@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-7.0.0.tgz#8d6abdb9ded8656cc9e2a7909913a34bf3fc9b3a" - integrity sha512-MOencXiW/gI5MuTtCNsuojjwT5DXCrjMqv9xOslJC9h2tPdLFFFMGr58dY5Lis4DRd9MRWcgrGowUIHOqieWTA== +"@matrix-org/matrix-sdk-crypto-wasm@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-8.0.0.tgz#6ddc0e63538e821a2efbc5c1a2f0fa0f71d489ff" + integrity sha512-s0q3O2dK8b6hOJ+SZFz+s/IiMabmVsNue6r17sTwbrRD8liBkCrpjYnxoMYvtC01GggJ9TZLQbeqpt8hQSPHAg== "@matrix-org/olm@3.2.15": version "3.2.15" @@ -5794,16 +5794,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -5858,14 +5849,7 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -6397,16 +6381,7 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From 4326169339b1211b0d76ebd6a4945c6fe829c8d3 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 18 Sep 2024 09:59:33 +0100 Subject: [PATCH 12/20] Sync with develop --- yarn.lock | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2d828dc6672..83f6c7b31ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5831,7 +5831,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -5886,7 +5895,14 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -6418,7 +6434,16 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From 91cab8eb859cb542ed77b3051df34d9606dc2c5d Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 7 Oct 2024 14:49:03 +0100 Subject: [PATCH 13/20] Add test for olmlib CryptoApi --- spec/unit/crypto.spec.ts | 112 ++++++++++++++++++++++ spec/unit/rust-crypto/rust-crypto.spec.ts | 2 +- 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/spec/unit/crypto.spec.ts b/spec/unit/crypto.spec.ts index 680b973ad56..2080493e65b 100644 --- a/spec/unit/crypto.spec.ts +++ b/spec/unit/crypto.spec.ts @@ -26,6 +26,7 @@ import { CryptoBackend } from "../../src/common-crypto/CryptoBackend"; import { EventDecryptionResult } from "../../src/common-crypto/CryptoBackend"; import * as testData from "../test-utils/test-data"; import { KnownMembership } from "../../src/@types/membership"; +import type { DeviceInfoMap } from "../../src/crypto/DeviceList"; const Olm = global.Olm; @@ -1245,6 +1246,117 @@ describe("Crypto", function () { }); }); + describe("encryptToDeviceMessages", () => { + let client: TestClient; + let ensureOlmSessionsForDevices: jest.SpiedFunction; + let encryptMessageForDevice: jest.SpiedFunction; + const payload = { hello: "world" }; + let encryptedPayload: object; + let crypto: Crypto; + + beforeEach(async () => { + ensureOlmSessionsForDevices = jest.spyOn(olmlib, "ensureOlmSessionsForDevices"); + ensureOlmSessionsForDevices.mockResolvedValue(new Map()); + encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice"); + encryptMessageForDevice.mockImplementation(async (...[result, , , , , , payload]) => { + result.plaintext = { type: 0, body: JSON.stringify(payload) }; + }); + + client = new TestClient("@alice:example.org", "aliceweb"); + + // running initCrypto should trigger a key upload + client.httpBackend.when("POST", "/keys/upload").respond(200, {}); + await Promise.all([client.client.initCrypto(), client.httpBackend.flush("/keys/upload", 1)]); + + encryptedPayload = { + algorithm: "m.olm.v1.curve25519-aes-sha2", + sender_key: client.client.crypto!.olmDevice.deviceCurve25519Key, + ciphertext: { plaintext: { type: 0, body: JSON.stringify(payload) } }, + }; + + crypto = client.client.getCrypto() as Crypto; + }); + + afterEach(async () => { + ensureOlmSessionsForDevices.mockRestore(); + encryptMessageForDevice.mockRestore(); + await client.stop(); + }); + + it("returns encrypted batch where devices known", async () => { + const deviceInfoMap: DeviceInfoMap = new Map([ + [ + "@bob:example.org", + new Map([ + ["bobweb", new DeviceInfo("bobweb")], + ["bobmobile", new DeviceInfo("bobmobile")], + ]), + ], + ["@carol:example.org", new Map([["caroldesktop", new DeviceInfo("caroldesktop")]])], + ]); + jest.spyOn(crypto.deviceList, "downloadKeys").mockResolvedValue(deviceInfoMap); + // const deviceInfoMap = await this.downloadKeys(Array.from(userIds), false); + + const batch = await client.client.getCrypto()?.encryptToDeviceMessages( + "m.test.type", + [ + { userId: "@bob:example.org", deviceId: "bobweb" }, + { userId: "@bob:example.org", deviceId: "bobmobile" }, + { userId: "@carol:example.org", deviceId: "caroldesktop" }, + { userId: "@carol:example.org", deviceId: "carolmobile" }, // not known + ], + payload, + ); + expect(crypto.deviceList.downloadKeys).toHaveBeenCalledWith( + ["@bob:example.org", "@carol:example.org"], + false, + ); + expect(encryptMessageForDevice).toHaveBeenCalledTimes(3); + const expectedPayload = expect.objectContaining({ + ...encryptedPayload, + "org.matrix.msgid": expect.any(String), + "sender_key": expect.any(String), + }); + expect(batch?.eventType).toEqual("m.room.encrypted"); + expect(batch?.batch.length).toEqual(3); + expect(batch).toEqual({ + eventType: "m.room.encrypted", + batch: expect.arrayContaining([ + { + userId: "@bob:example.org", + deviceId: "bobweb", + payload: expectedPayload, + }, + { + userId: "@bob:example.org", + deviceId: "bobmobile", + payload: expectedPayload, + }, + { + userId: "@carol:example.org", + deviceId: "caroldesktop", + payload: expectedPayload, + }, + ]), + }); + }); + + it("returns empty batch if no devices known", async () => { + jest.spyOn(crypto.deviceList, "downloadKeys").mockResolvedValue(new Map()); + const batch = await crypto.encryptToDeviceMessages( + "m.test.type", + [ + { deviceId: "AAA", userId: "@user1:domain" }, + { deviceId: "BBB", userId: "@user1:domain" }, + { deviceId: "CCC", userId: "@user2:domain" }, + ], + payload, + ); + expect(batch?.eventType).toEqual("m.room.encrypted"); + expect(batch?.batch).toEqual([]); + }); + }); + describe("checkSecretStoragePrivateKey", () => { let client: TestClient; diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index f05ae719c24..e48aab5b0db 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -1630,7 +1630,7 @@ describe("RustCrypto", () => { expect(getDevice).toHaveBeenCalledTimes(3); expect(batch?.eventType).toEqual("m.room.encrypted"); expect(batch?.batch).toEqual([]); - }, 120000); + }); it("returns encrypted batch for known devices", async () => { // Make m aware of another device, and get some OTK to be able to establish a session. From b3647a892883e0379dcb7fda295ce28179789b36 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 7 Oct 2024 14:58:50 +0100 Subject: [PATCH 14/20] Fix link --- src/crypto-api/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto-api/index.ts b/src/crypto-api/index.ts index 350ec44dd3c..6e223e107d5 100644 --- a/src/crypto-api/index.ts +++ b/src/crypto-api/index.ts @@ -373,7 +373,7 @@ export interface CryptoApi { * @param devices an array of (user ID, device ID) pairs to encrypt the payload for * @param payload the payload to encrypt * - * @returns the batch of encrypted payloads which can then be sent via {@link MatrixClient#queueToDevice} + * @returns the batch of encrypted payloads which can then be sent via {@link matrix.MatrixClient#queueToDevice} */ encryptToDeviceMessages( eventType: string, From f29f40a01ca7defb34dd1aa928a3c42fb031cc78 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 11 Oct 2024 16:23:30 +0100 Subject: [PATCH 15/20] Feedback from review --- src/crypto-api/index.ts | 8 +-- src/rust-crypto/rust-crypto.ts | 89 ++++++++++++++++++---------------- 2 files changed, 50 insertions(+), 47 deletions(-) diff --git a/src/crypto-api/index.ts b/src/crypto-api/index.ts index 6e223e107d5..e421a8b4342 100644 --- a/src/crypto-api/index.ts +++ b/src/crypto-api/index.ts @@ -369,11 +369,11 @@ export interface CryptoApi { * Encrypts a given payload object via Olm to-device messages to a given * set of devices. * - * @param eventType the type of the event to send - * @param devices an array of (user ID, device ID) pairs to encrypt the payload for - * @param payload the payload to encrypt + * @param eventType - the type of the event to send. + * @param devices - an array of devices to encrypt the payload for. + * @param payload - the payload to encrypt. * - * @returns the batch of encrypted payloads which can then be sent via {@link matrix.MatrixClient#queueToDevice} + * @returns the batch of encrypted payloads which can then be sent via {@link matrix.MatrixClient#queueToDevice}. */ encryptToDeviceMessages( eventType: string, diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index c99f18bd249..d1221c00ed4 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -1316,6 +1316,52 @@ export class RustCrypto extends TypedEventEmitter { + const logger = new LogSpan(this.logger, "encryptToDeviceMessages"); + const uniqueUsers = new Set(devices.map(({ userId }) => userId)); + + // This will ensure we have Olm sessions for all of the users' devices. + // However, we only care about some of the devices. + // So, perhaps we can optimise this later on. + await this.keyClaimManager.ensureSessionsForUsers( + logger, + Array.from(uniqueUsers).map((userId) => new RustSdkCryptoJs.UserId(userId)), + ); + const batch: ToDeviceBatch = { + batch: [], + eventType: EventType.RoomMessageEncrypted, + }; + + await Promise.all( + devices.map(async ({ userId, deviceId }) => { + const device: RustSdkCryptoJs.Device | undefined = await this.olmMachine.getDevice( + new RustSdkCryptoJs.UserId(userId), + new RustSdkCryptoJs.DeviceId(deviceId), + ); + + if (device) { + const encryptedPayload = JSON.parse(await device.encryptToDeviceEvent(eventType, payload)); + batch.batch.push({ + deviceId, + userId, + payload: encryptedPayload, + }); + } else { + this.logger.warn(`encryptToDeviceMessages: unknown device ${userId}:${deviceId}`); + } + }), + ); + + return batch; + } + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // SyncCryptoCallbacks implementation @@ -1761,49 +1807,6 @@ export class RustCrypto extends TypedEventEmitter { return await this.olmMachine.getIdentity(new RustSdkCryptoJs.UserId(this.userId)); } - - public async encryptToDeviceMessages( - eventType: string, - devices: { userId: string; deviceId: string }[], - payload: ToDevicePayload, - ): Promise { - const logger = new LogSpan(this.logger, "encryptToDeviceMessages"); - const uniqueUsers = new Set(devices.map(({ userId }) => userId)); - - // This will ensure we have Olm sessions for all of the users' devices. - // However, we only care about some of the devices. - // So, perhaps we can optimise this later on. - await this.keyClaimManager.ensureSessionsForUsers( - logger, - Array.from(uniqueUsers).map((userId) => new RustSdkCryptoJs.UserId(userId)), - ); - const batch: ToDeviceBatch = { - batch: [], - eventType: EventType.RoomMessageEncrypted, - }; - - await Promise.all( - devices.map(async ({ userId, deviceId }) => { - const device: RustSdkCryptoJs.Device | undefined = await this.olmMachine.getDevice( - new RustSdkCryptoJs.UserId(userId), - new RustSdkCryptoJs.DeviceId(deviceId), - ); - - if (device) { - const encryptedPayload = JSON.parse(await device.encryptToDeviceEvent(eventType, payload)); - batch.batch.push({ - deviceId, - userId, - payload: encryptedPayload, - }); - } else { - this.logger.warn(`encryptToDeviceMessages: unknown device ${userId}:${deviceId}`); - } - }), - ); - - return batch; - } } class EventDecryptor { From 2be86fe6f3efd0bf5e43607888432e3d6321628b Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 11 Oct 2024 16:30:37 +0100 Subject: [PATCH 16/20] Move libolm implementation to better place in file --- src/crypto/index.ts | 175 ++++++++++++++++++++++---------------------- 1 file changed, 89 insertions(+), 86 deletions(-) diff --git a/src/crypto/index.ts b/src/crypto/index.ts index d24080bafd4..003b1807ae0 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -3568,6 +3568,95 @@ export class Crypto extends TypedEventEmitter[], + payload: object, + ): Promise { + const toDeviceBatch: ToDeviceBatch = { + eventType: EventType.RoomMessageEncrypted, + batch: [], + }; + + await Promise.all( + userDeviceInfoArr.map(async ({ userId, deviceInfo }) => { + const deviceId = deviceInfo.deviceId; + const encryptedContent: IEncryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this.olmDevice.deviceCurve25519Key!, + ciphertext: {}, + [ToDeviceMessageId]: uuidv4(), + }; + + toDeviceBatch.batch.push({ + userId, + deviceId, + payload: encryptedContent, + }); + + await olmlib.ensureOlmSessionsForDevices( + this.olmDevice, + this.baseApis, + new Map([[userId, [deviceInfo]]]), + ); + await olmlib.encryptMessageForDevice( + encryptedContent.ciphertext, + this.userId, + this.deviceId, + this.olmDevice, + userId, + deviceInfo, + payload, + ); + }), + ); + + // prune out any devices that encryptMessageForDevice could not encrypt for, + // in which case it will have just not added anything to the ciphertext object. + // There's no point sending messages to devices if we couldn't encrypt to them, + // since that's effectively a blank message. + toDeviceBatch.batch = toDeviceBatch.batch.filter((msg) => { + if (Object.keys(msg.payload.ciphertext).length > 0) { + return true; + } else { + logger.log(`No ciphertext for device ${msg.userId}:${msg.deviceId}: pruning`); + return false; + } + }); + + return toDeviceBatch; + } + + /** + * Implementation of {@link CryptoApi#encryptToDeviceMessages}. + */ + public async encryptToDeviceMessages( + eventType: string, + devices: { userId: string; deviceId: string }[], + payload: ToDevicePayload, + ): Promise { + const userIds = new Set(devices.map(({ userId }) => userId)); + const deviceInfoMap = await this.downloadKeys(Array.from(userIds), false); + + const userDeviceInfoArr: IOlmDevice[] = []; + + devices.forEach(({ userId, deviceId }) => { + const devices = deviceInfoMap.get(userId); + if (!devices) { + logger.warn(`No devices found for user ${userId}`); + return; + } + + if (devices.has(deviceId)) { + // Send the message to a specific device + userDeviceInfoArr.push({ userId, deviceInfo: devices.get(deviceId)! }); + } else { + logger.warn(`No device found for user ${userId} with id ${deviceId}`); + } + }); + + return this.prepareToDeviceBatch(userDeviceInfoArr, payload); + } + private onMembership = (event: MatrixEvent, member: RoomMember, oldMembership?: string): void => { try { this.onRoomMembership(event, member, oldMembership); @@ -4285,92 +4374,6 @@ export class Crypto extends TypedEventEmitter { throw new Error("Not implemented"); } - - private async prepareToDeviceBatch( - userDeviceInfoArr: IOlmDevice[], - payload: object, - ): Promise { - const toDeviceBatch: ToDeviceBatch = { - eventType: EventType.RoomMessageEncrypted, - batch: [], - }; - - await Promise.all( - userDeviceInfoArr.map(async ({ userId, deviceInfo }) => { - const deviceId = deviceInfo.deviceId; - const encryptedContent: IEncryptedContent = { - algorithm: olmlib.OLM_ALGORITHM, - sender_key: this.olmDevice.deviceCurve25519Key!, - ciphertext: {}, - [ToDeviceMessageId]: uuidv4(), - }; - - toDeviceBatch.batch.push({ - userId, - deviceId, - payload: encryptedContent, - }); - - await olmlib.ensureOlmSessionsForDevices( - this.olmDevice, - this.baseApis, - new Map([[userId, [deviceInfo]]]), - ); - await olmlib.encryptMessageForDevice( - encryptedContent.ciphertext, - this.userId, - this.deviceId, - this.olmDevice, - userId, - deviceInfo, - payload, - ); - }), - ); - - // prune out any devices that encryptMessageForDevice could not encrypt for, - // in which case it will have just not added anything to the ciphertext object. - // There's no point sending messages to devices if we couldn't encrypt to them, - // since that's effectively a blank message. - toDeviceBatch.batch = toDeviceBatch.batch.filter((msg) => { - if (Object.keys(msg.payload.ciphertext).length > 0) { - return true; - } else { - logger.log(`No ciphertext for device ${msg.userId}:${msg.deviceId}: pruning`); - return false; - } - }); - - return toDeviceBatch; - } - - public async encryptToDeviceMessages( - eventType: string, - devices: { userId: string; deviceId: string }[], - payload: ToDevicePayload, - ): Promise { - const userIds = new Set(devices.map(({ userId }) => userId)); - const deviceInfoMap = await this.downloadKeys(Array.from(userIds), false); - - const userDeviceInfoArr: IOlmDevice[] = []; - - devices.forEach(({ userId, deviceId }) => { - const devices = deviceInfoMap.get(userId); - if (!devices) { - logger.warn(`No devices found for user ${userId}`); - return; - } - - if (devices.has(deviceId)) { - // Send the message to a specific device - userDeviceInfoArr.push({ userId, deviceInfo: devices.get(deviceId)! }); - } else { - logger.warn(`No device found for user ${userId} with id ${deviceId}`); - } - }); - - return this.prepareToDeviceBatch(userDeviceInfoArr, payload); - } } /** From 538b39b62ac53f41b712c378acbdf156bfac265e Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 11 Oct 2024 16:47:56 +0100 Subject: [PATCH 17/20] FIx doc --- src/crypto/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 003b1807ae0..ea407d93c3b 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -3627,7 +3627,7 @@ export class Crypto extends TypedEventEmitter Date: Tue, 22 Oct 2024 12:24:26 +0100 Subject: [PATCH 18/20] Integration test --- spec/integ/crypto/to-device-messages.spec.ts | 133 +++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 spec/integ/crypto/to-device-messages.spec.ts diff --git a/spec/integ/crypto/to-device-messages.spec.ts b/spec/integ/crypto/to-device-messages.spec.ts new file mode 100644 index 00000000000..589d319beb9 --- /dev/null +++ b/spec/integ/crypto/to-device-messages.spec.ts @@ -0,0 +1,133 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import fetchMock from "fetch-mock-jest"; +import "fake-indexeddb/auto"; +import { IDBFactory } from "fake-indexeddb"; + +import { CRYPTO_BACKENDS, InitCrypto } from "../../test-utils/test-utils"; +import { createClient, MatrixClient } from "../../../src"; +import * as testData from "../../test-utils/test-data"; +import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder"; + +afterEach(() => { + // 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 + // eslint-disable-next-line no-global-assign + indexedDB = new IDBFactory(); +}); + +/** + * Integration tests for to-device messages functionality. + * + * These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as + * to provide the most effective integration tests possible. + */ +describe.each(Object.entries(CRYPTO_BACKENDS))("to-device-messages (%s)", (backend: string, initCrypto: InitCrypto) => { + let aliceClient: MatrixClient; + + /** an object which intercepts `/keys/query` requests on the test homeserver */ + let e2eKeyResponder: E2EKeyResponder; + + beforeEach( + async () => { + // anything that we don't have a specific matcher for silently returns a 404 + fetchMock.catch(404); + fetchMock.config.warnOnFallback = false; + + const homeserverUrl = "https://server.com"; + aliceClient = createClient({ + baseUrl: homeserverUrl, + userId: testData.TEST_USER_ID, + accessToken: "akjgkrgjsalice", + deviceId: testData.TEST_DEVICE_ID, + }); + + e2eKeyResponder = new E2EKeyResponder(homeserverUrl); + // /** an object which intercepts `/keys/upload` requests on the test homeserver */ + // new E2EKeyReceiver(homeserverUrl); + + // Silence warnings from the backup manager + fetchMock.getOnce(new URL("/_matrix/client/v3/room_keys/version", homeserverUrl).toString(), { + status: 404, + body: { errcode: "M_NOT_FOUND" }, + }); + + await initCrypto(aliceClient); + }, + /* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */ + 10000, + ); + + afterEach(async () => { + aliceClient.stopClient(); + fetchMock.mockReset(); + }); + + describe("encryptToDeviceMessages", () => { + it("returns empty batch for device without key", async () => { + const toDeviceBatch = await aliceClient + .getCrypto() + ?.encryptToDeviceMessages( + "m.test.event", + [{ userId: testData.BOB_TEST_USER_ID, deviceId: testData.BOB_TEST_DEVICE_ID }], + { + some: "content", + }, + ); + + expect(toDeviceBatch).toBeDefined(); + const { batch, eventType } = toDeviceBatch!; + expect(eventType).toBe("m.room.encrypted"); + expect(batch.length).toBe(0); + }); + + it("returns encrypted batch for known device", async () => { + e2eKeyResponder.addDeviceKeys(testData.BOB_SIGNED_TEST_DEVICE_DATA); + fetchMock.post("express:/_matrix/client/v3/keys/claim", () => ({ + one_time_keys: testData.BOB_ONE_TIME_KEYS, + })); + + const toDeviceBatch = await aliceClient + .getCrypto() + ?.encryptToDeviceMessages( + "m.test.event", + [{ userId: testData.BOB_TEST_USER_ID, deviceId: testData.BOB_TEST_DEVICE_ID }], + { + some: "content", + }, + ); + + expect(toDeviceBatch?.batch.length).toBe(1); + expect(toDeviceBatch?.eventType).toBe("m.room.encrypted"); + const { deviceId, payload, userId } = toDeviceBatch!.batch[0]; + expect(deviceId).toBe(testData.BOB_TEST_DEVICE_ID); + expect(userId).toBe(testData.BOB_TEST_USER_ID); + expect(payload.algorithm).toBe("m.olm.v1.curve25519-aes-sha2"); + expect(payload.sender_key).toEqual(expect.any(String)); + expect(payload.ciphertext).toEqual( + expect.objectContaining({ + [testData.BOB_SIGNED_TEST_DEVICE_DATA.keys[`curve25519:${testData.BOB_TEST_DEVICE_ID}`]]: { + body: expect.any(String), + type: 0, + }, + }), + ); + + // for future: check that bob's device can decrypt the ciphertext? + }); + }); +}); From 505d15d2afd2dd0357f7bf79e6208d46aeb9b956 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 23 Oct 2024 14:31:36 +0100 Subject: [PATCH 19/20] Make sure test device is known to client --- spec/integ/crypto/to-device-messages.spec.ts | 24 +++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/spec/integ/crypto/to-device-messages.spec.ts b/spec/integ/crypto/to-device-messages.spec.ts index 589d319beb9..fa034df6bfd 100644 --- a/spec/integ/crypto/to-device-messages.spec.ts +++ b/spec/integ/crypto/to-device-messages.spec.ts @@ -18,10 +18,12 @@ import fetchMock from "fetch-mock-jest"; import "fake-indexeddb/auto"; import { IDBFactory } from "fake-indexeddb"; -import { CRYPTO_BACKENDS, InitCrypto } from "../../test-utils/test-utils"; +import { CRYPTO_BACKENDS, getSyncResponse, InitCrypto, syncPromise } from "../../test-utils/test-utils"; import { createClient, MatrixClient } from "../../../src"; import * as testData from "../../test-utils/test-data"; import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder"; +import { SyncResponder } from "../../test-utils/SyncResponder"; +import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver"; afterEach(() => { // reset fake-indexeddb after each test, to make sure we don't leak connections @@ -41,6 +43,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("to-device-messages (%s)", (backe /** an object which intercepts `/keys/query` requests on the test homeserver */ let e2eKeyResponder: E2EKeyResponder; + let syncResponder: SyncResponder; beforeEach( async () => { @@ -57,8 +60,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("to-device-messages (%s)", (backe }); e2eKeyResponder = new E2EKeyResponder(homeserverUrl); - // /** an object which intercepts `/keys/upload` requests on the test homeserver */ - // new E2EKeyReceiver(homeserverUrl); + new E2EKeyReceiver(homeserverUrl); + syncResponder = new SyncResponder(homeserverUrl); // Silence warnings from the backup manager fetchMock.getOnce(new URL("/_matrix/client/v3/room_keys/version", homeserverUrl).toString(), { @@ -66,6 +69,16 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("to-device-messages (%s)", (backe body: { errcode: "M_NOT_FOUND" }, }); + fetchMock.get(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {}); + fetchMock.get(new URL("/_matrix/client/versions/", homeserverUrl).toString(), {}); + fetchMock.post( + new URL( + `/_matrix/client/v3/user/${encodeURIComponent(testData.TEST_USER_ID)}/filter`, + homeserverUrl, + ).toString(), + { filter_id: "fid" }, + ); + await initCrypto(aliceClient); }, /* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */ @@ -79,6 +92,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("to-device-messages (%s)", (backe describe("encryptToDeviceMessages", () => { it("returns empty batch for device without key", async () => { + await aliceClient.startClient(); + const toDeviceBatch = await aliceClient .getCrypto() ?.encryptToDeviceMessages( @@ -96,10 +111,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("to-device-messages (%s)", (backe }); it("returns encrypted batch for known device", async () => { + await aliceClient.startClient(); e2eKeyResponder.addDeviceKeys(testData.BOB_SIGNED_TEST_DEVICE_DATA); fetchMock.post("express:/_matrix/client/v3/keys/claim", () => ({ one_time_keys: testData.BOB_ONE_TIME_KEYS, })); + syncResponder.sendOrQueueSyncResponse(getSyncResponse([testData.BOB_TEST_USER_ID])); + await syncPromise(aliceClient); const toDeviceBatch = await aliceClient .getCrypto() From 9627f8cbe80cb339adeb33a8b8abe1d2c080f65d Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 25 Oct 2024 15:13:31 +0100 Subject: [PATCH 20/20] Feedback from review --- spec/integ/crypto/to-device-messages.spec.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spec/integ/crypto/to-device-messages.spec.ts b/spec/integ/crypto/to-device-messages.spec.ts index fa034df6bfd..ee15266ce98 100644 --- a/spec/integ/crypto/to-device-messages.spec.ts +++ b/spec/integ/crypto/to-device-messages.spec.ts @@ -43,7 +43,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("to-device-messages (%s)", (backe /** an object which intercepts `/keys/query` requests on the test homeserver */ let e2eKeyResponder: E2EKeyResponder; - let syncResponder: SyncResponder; beforeEach( async () => { @@ -61,7 +60,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("to-device-messages (%s)", (backe e2eKeyResponder = new E2EKeyResponder(homeserverUrl); new E2EKeyReceiver(homeserverUrl); - syncResponder = new SyncResponder(homeserverUrl); + const syncResponder = new SyncResponder(homeserverUrl); + + // add bob as known user + syncResponder.sendOrQueueSyncResponse(getSyncResponse([testData.BOB_TEST_USER_ID])); // Silence warnings from the backup manager fetchMock.getOnce(new URL("/_matrix/client/v3/room_keys/version", homeserverUrl).toString(), { @@ -91,7 +93,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("to-device-messages (%s)", (backe }); describe("encryptToDeviceMessages", () => { - it("returns empty batch for device without key", async () => { + it("returns empty batch for device that is not known", async () => { await aliceClient.startClient(); const toDeviceBatch = await aliceClient @@ -116,7 +118,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("to-device-messages (%s)", (backe fetchMock.post("express:/_matrix/client/v3/keys/claim", () => ({ one_time_keys: testData.BOB_ONE_TIME_KEYS, })); - syncResponder.sendOrQueueSyncResponse(getSyncResponse([testData.BOB_TEST_USER_ID])); await syncPromise(aliceClient); const toDeviceBatch = await aliceClient