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..ee15266ce98 --- /dev/null +++ b/spec/integ/crypto/to-device-messages.spec.ts @@ -0,0 +1,152 @@ +/* +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, 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 + // 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); + new E2EKeyReceiver(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(), { + status: 404, + 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. */ + 10000, + ); + + afterEach(async () => { + aliceClient.stopClient(); + fetchMock.mockReset(); + }); + + describe("encryptToDeviceMessages", () => { + it("returns empty batch for device that is not known", async () => { + await aliceClient.startClient(); + + 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 () => { + 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, + })); + await syncPromise(aliceClient); + + 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? + }); + }); +}); diff --git a/spec/unit/crypto.spec.ts b/spec/unit/crypto.spec.ts index fa287a3a9e2..616c8f95255 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 de93bc9771d..117112dd09b 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -1593,6 +1593,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([]); + }); + + 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 */ diff --git a/src/client.ts b/src/client.ts index 39cfa8e9629..023231532e1 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3260,7 +3260,7 @@ 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 7e130b190da..0b8a870aefe 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"; @@ -364,6 +365,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 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}. + */ + encryptToDeviceMessages( + eventType: string, + devices: { userId: string; deviceId: string }[], + payload: ToDevicePayload, + ): Promise; + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Device/User verification diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 829000bf98b..c252ba15a4b 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -53,7 +53,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"; @@ -3483,59 +3483,12 @@ 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, - ); - }), - ); - - // 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; - } - }); + const toDeviceBatch = await this.prepareToDeviceBatch(userDeviceInfoArr, payload); try { await this.baseApis.queueToDevice(toDeviceBatch); @@ -3549,6 +3502,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 Crypto.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); diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 6f5e0af6c64..2d65e7f5c58 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"; @@ -30,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"; @@ -1316,6 +1317,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