1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

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
This commit is contained in:
Valere Fedronic
2025-07-02 10:02:23 +02:00
committed by GitHub
parent de659d6431
commit 161c12f5d5
9 changed files with 304 additions and 61 deletions

View File

@ -17,13 +17,22 @@ limitations under the License.
import fetchMock from "fetch-mock-jest"; import fetchMock from "fetch-mock-jest";
import "fake-indexeddb/auto"; import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb"; import { IDBFactory } from "fake-indexeddb";
import Olm from "@matrix-org/olm";
import { getSyncResponse, syncPromise } from "../../test-utils/test-utils"; import { getSyncResponse, syncPromise } from "../../test-utils/test-utils";
import { createClient, type MatrixClient } from "../../../src"; import {
ClientEvent,
createClient,
type IToDeviceEvent,
type MatrixClient,
type MatrixEvent,
type ReceivedToDeviceMessage,
} from "../../../src";
import * as testData from "../../test-utils/test-data"; import * as testData from "../../test-utils/test-data";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder"; import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { SyncResponder } from "../../test-utils/SyncResponder"; import { SyncResponder } from "../../test-utils/SyncResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver"; import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { encryptOlmEvent, establishOlmSession, getTestOlmAccountKeys } from "./olm-utils.ts";
afterEach(() => { afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections // reset fake-indexeddb after each test, to make sure we don't leak connections
@ -43,6 +52,8 @@ describe("to-device-messages", () => {
/** an object which intercepts `/keys/query` requests on the test homeserver */ /** an object which intercepts `/keys/query` requests on the test homeserver */
let e2eKeyResponder: E2EKeyResponder; let e2eKeyResponder: E2EKeyResponder;
let e2eKeyReceiver: E2EKeyReceiver;
let syncResponder: SyncResponder;
beforeEach( beforeEach(
async () => { async () => {
@ -59,8 +70,8 @@ describe("to-device-messages", () => {
}); });
e2eKeyResponder = new E2EKeyResponder(homeserverUrl); e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
new E2EKeyReceiver(homeserverUrl); e2eKeyReceiver = new E2EKeyReceiver(homeserverUrl);
const syncResponder = new SyncResponder(homeserverUrl); syncResponder = new SyncResponder(homeserverUrl);
// add bob as known user // add bob as known user
syncResponder.sendOrQueueSyncResponse(getSyncResponse([testData.BOB_TEST_USER_ID])); syncResponder.sendOrQueueSyncResponse(getSyncResponse([testData.BOB_TEST_USER_ID]));
@ -149,4 +160,111 @@ describe("to-device-messages", () => {
// for future: check that bob's device can decrypt the ciphertext? // for future: check that bob's device can decrypt the ciphertext?
}); });
}); });
describe("receive to-device-messages", () => {
it("Should receive decrypted to-device message via ClientEvent", async () => {
// create a test olm device which we will use to communicate with alice. We use libolm to implement this.
await Olm.init();
const testOlmAccount = new Olm.Account();
testOlmAccount.create();
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
e2eKeyResponder.addDeviceKeys(testDeviceKeys);
await aliceClient.startClient();
await syncPromise(aliceClient);
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"]));
await syncPromise(aliceClient);
const p2pSession = await establishOlmSession(aliceClient, e2eKeyReceiver, syncResponder, testOlmAccount);
const toDeviceEvent = encryptOlmEvent({
sender: "@bob:xyz",
senderKey: testDeviceKeys.keys[`curve25519:DEVICE_ID`],
senderSigningKey: testDeviceKeys.keys[`ed25519:DEVICE_ID`],
p2pSession: p2pSession,
recipient: aliceClient.getUserId()!,
recipientCurve25519Key: e2eKeyReceiver.getDeviceKey(),
recipientEd25519Key: e2eKeyReceiver.getSigningKey(),
plaincontent: {
body: "foo",
},
plaintype: "m.test.type",
});
const processedToDeviceResolver: PromiseWithResolvers<ReceivedToDeviceMessage> = Promise.withResolvers();
aliceClient.on(ClientEvent.ReceivedToDeviceMessage, (payload) => {
processedToDeviceResolver.resolve(payload);
});
const oldToDeviceResolver: PromiseWithResolvers<MatrixEvent> = Promise.withResolvers();
aliceClient.on(ClientEvent.ToDeviceEvent, (event) => {
oldToDeviceResolver.resolve(event);
});
expect(toDeviceEvent.type).toBe("m.room.encrypted");
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [toDeviceEvent] } });
await syncPromise(aliceClient);
const { message, encryptionInfo } = await processedToDeviceResolver.promise;
expect(message.type).toBe("m.test.type");
expect(message.content["body"]).toBe("foo");
expect(encryptionInfo).not.toBeNull();
expect(encryptionInfo!.senderVerified).toBe(false);
expect(encryptionInfo!.sender).toBe("@bob:xyz");
expect(encryptionInfo!.senderDevice).toBe("DEVICE_ID");
const oldFormat = await oldToDeviceResolver.promise;
expect(oldFormat.isEncrypted()).toBe(true);
expect(oldFormat.getType()).toBe("m.test.type");
expect(oldFormat.getContent()["body"]).toBe("foo");
});
it("Should receive clear to-device message via ClientEvent", async () => {
await aliceClient.startClient();
await syncPromise(aliceClient);
const toDeviceEvent: IToDeviceEvent = {
sender: "@bob:xyz",
type: "m.test.type",
content: {
body: "foo",
},
};
const processedToDeviceResolver: PromiseWithResolvers<ReceivedToDeviceMessage> = Promise.withResolvers();
aliceClient.on(ClientEvent.ReceivedToDeviceMessage, (payload) => {
processedToDeviceResolver.resolve(payload);
});
const oldToDeviceResolver: PromiseWithResolvers<MatrixEvent> = Promise.withResolvers();
aliceClient.on(ClientEvent.ToDeviceEvent, (event) => {
oldToDeviceResolver.resolve(event);
});
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [toDeviceEvent] } });
await syncPromise(aliceClient);
const { message, encryptionInfo } = await processedToDeviceResolver.promise;
expect(message.type).toBe("m.test.type");
expect(message.content["body"]).toBe("foo");
// When the message is not encrypted, we don't have the encryptionInfo.
expect(encryptionInfo).toBeNull();
const oldFormat = await oldToDeviceResolver.promise;
expect(oldFormat.isEncrypted()).toBe(false);
expect(oldFormat.getType()).toBe("m.test.type");
expect(oldFormat.getContent()["body"]).toBe("foo");
});
});
}); });

View File

@ -551,11 +551,11 @@ describe("RustCrypto", () => {
const inputs: IToDeviceEvent[] = [ const inputs: IToDeviceEvent[] = [
{ content: { key: "value" }, type: "org.matrix.test", sender: "@alice:example.com" }, { content: { key: "value" }, type: "org.matrix.test", sender: "@alice:example.com" },
]; ];
const res = await rustCrypto.preprocessToDeviceMessages(inputs); const res = (await rustCrypto.preprocessToDeviceMessages(inputs)).map((p) => p.message);
expect(res).toEqual(inputs); expect(res).toEqual(inputs);
}); });
it("should pass through bad encrypted messages", async () => { it("should fail to process bad encrypted messages", async () => {
const olmMachine: OlmMachine = rustCrypto["olmMachine"]; const olmMachine: OlmMachine = rustCrypto["olmMachine"];
const keys = olmMachine.identityKeys; const keys = olmMachine.identityKeys;
const inputs: IToDeviceEvent[] = [ const inputs: IToDeviceEvent[] = [
@ -576,7 +576,7 @@ describe("RustCrypto", () => {
]; ];
const res = await rustCrypto.preprocessToDeviceMessages(inputs); const res = await rustCrypto.preprocessToDeviceMessages(inputs);
expect(res).toEqual(inputs); expect(res.length).toEqual(0);
}); });
it("emits VerificationRequestReceived on incoming m.key.verification.request", async () => { it("emits VerificationRequestReceived on incoming m.key.verification.request", async () => {

View File

@ -87,7 +87,12 @@ import { type IIdentityServerProvider } from "./@types/IIdentityServerProvider.t
import { type MatrixScheduler } from "./scheduler.ts"; import { type MatrixScheduler } from "./scheduler.ts";
import { type BeaconEvent, type BeaconEventHandlerMap } from "./models/beacon.ts"; import { type BeaconEvent, type BeaconEventHandlerMap } from "./models/beacon.ts";
import { type AuthDict } from "./interactive-auth.ts"; import { type AuthDict } from "./interactive-auth.ts";
import { type IMinimalEvent, type IRoomEvent, type IStateEvent } from "./sync-accumulator.ts"; import {
type IMinimalEvent,
type IRoomEvent,
type IStateEvent,
type ReceivedToDeviceMessage,
} from "./sync-accumulator.ts";
import type { EventTimelineSet } from "./models/event-timeline-set.ts"; import type { EventTimelineSet } from "./models/event-timeline-set.ts";
import * as ContentHelpers from "./content-helpers.ts"; import * as ContentHelpers from "./content-helpers.ts";
import { import {
@ -885,7 +890,9 @@ const EVENT_ID_PREFIX = "$";
export enum ClientEvent { export enum ClientEvent {
Sync = "sync", Sync = "sync",
Event = "event", Event = "event",
/** @deprecated Use {@link ReceivedToDeviceMessage}. */
ToDeviceEvent = "toDeviceEvent", ToDeviceEvent = "toDeviceEvent",
ReceivedToDeviceMessage = "receivedToDeviceMessage",
AccountData = "accountData", AccountData = "accountData",
Room = "Room", Room = "Room",
DeleteRoom = "deleteRoom", DeleteRoom = "deleteRoom",
@ -1088,6 +1095,20 @@ export type ClientEventHandlerMap = {
* ``` * ```
*/ */
[ClientEvent.ToDeviceEvent]: (event: MatrixEvent) => void; [ClientEvent.ToDeviceEvent]: (event: MatrixEvent) => void;
/**
* Fires whenever the SDK receives a new to-device message.
* @param payload - The message and encryptionInfo for this message (See {@link ReceivedToDeviceMessage}) which caused this event to fire.
* @example
* ```
* matrixClient.on("receivedToDeviceMessage", function(payload){
* const { message, encryptionInfo } = payload;
* var claimed_sender = encryptionInfo ? encryptionInfo.sender : message.sender;
* var isVerified = encryptionInfo ? encryptionInfo.verified : false;
* var type = message.type;
* });
* ```
*/
[ClientEvent.ReceivedToDeviceMessage]: (payload: ReceivedToDeviceMessage) => void;
/** /**
* Fires if a to-device event is received that cannot be decrypted. * Fires if a to-device event is received that cannot be decrypted.
* Encrypted to-device events will (generally) use plain Olm encryption, * Encrypted to-device events will (generally) use plain Olm encryption,

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator.ts"; import type { IDeviceLists, IToDeviceEvent, ReceivedToDeviceMessage } from "../sync-accumulator.ts";
import { type IClearEvent, type MatrixEvent } from "../models/event.ts"; import { type IClearEvent, type MatrixEvent } from "../models/event.ts";
import { type Room } from "../models/room.ts"; import { type Room } from "../models/room.ts";
import { type CryptoApi, type DecryptionFailureCode, type ImportRoomKeysOpts } from "../crypto-api/index.ts"; import { type CryptoApi, type DecryptionFailureCode, type ImportRoomKeysOpts } from "../crypto-api/index.ts";
@ -96,9 +96,11 @@ export interface SyncCryptoCallbacks {
* messages, rather than the results of any decryption attempts. * messages, rather than the results of any decryption attempts.
* *
* @param events - the received to-device messages * @param events - the received to-device messages
* @returns A list of preprocessed to-device messages. * @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
* failed to decrypt, and so will be omitted from the output list.
*
*/ */
preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]>; preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<ReceivedToDeviceMessage[]>;
/** /**
* Called by the /sync loop when one time key counts and unused fallback key details are received. * Called by the /sync loop when one time key counts and unused fallback key details are received.

View File

@ -1365,6 +1365,27 @@ export interface OwnDeviceKeys {
curve25519: string; curve25519: string;
} }
/**
* Information about the encryption of a successfully decrypted to-device message.
*/
export interface OlmEncryptionInfo {
/** The user ID of the event sender, note this is untrusted data unless `isVerified` is true **/
sender: string;
/**
* The device ID of the device that sent us the event.
* Note this is untrusted data unless {@link senderVerified} is true.
* If the device ID is not known, this will be `null`.
**/
senderDevice?: string;
/** The sender device's public Curve25519 key, base64 encoded **/
senderCurve25519KeyBase64: string;
/**
* If true, this message is guaranteed to be authentic as it is coming from a device belonging to a user that we have verified.
* This is the state at the time of decryption (the user could be verified later).
*/
senderVerified: boolean;
}
export * from "./verification.ts"; export * from "./verification.ts";
export type * from "./keybackup.ts"; export type * from "./keybackup.ts";
export * from "./recovery-key.ts"; export * from "./recovery-key.ts";

View File

@ -19,7 +19,7 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto.ts"; import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto.ts";
import { KnownMembership } from "../@types/membership.ts"; import { KnownMembership } from "../@types/membership.ts";
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator.ts"; import { type IDeviceLists, type IToDeviceEvent, type ReceivedToDeviceMessage } from "../sync-accumulator.ts";
import type { ToDevicePayload, ToDeviceBatch } from "../models/ToDeviceMessage.ts"; import type { ToDevicePayload, ToDeviceBatch } from "../models/ToDeviceMessage.ts";
import { type MatrixEvent, MatrixEventEvent } from "../models/event.ts"; import { type MatrixEvent, MatrixEventEvent } from "../models/event.ts";
import { type Room } from "../models/room.ts"; import { type Room } from "../models/room.ts";
@ -1481,7 +1481,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
* @param oneTimeKeysCounts - the received one time key counts * @param oneTimeKeysCounts - the received one time key counts
* @param unusedFallbackKeys - the received unused fallback keys * @param unusedFallbackKeys - the received unused fallback keys
* @param devices - the received device list updates * @param devices - the received device list updates
* @returns A list of preprocessed to-device messages. * @returns A list of processed to-device messages.
*/ */
private async receiveSyncChanges({ private async receiveSyncChanges({
events, events,
@ -1493,15 +1493,13 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
oneTimeKeysCounts?: Map<string, number>; oneTimeKeysCounts?: Map<string, number>;
unusedFallbackKeys?: Set<string>; unusedFallbackKeys?: Set<string>;
devices?: RustSdkCryptoJs.DeviceLists; devices?: RustSdkCryptoJs.DeviceLists;
}): Promise<IToDeviceEvent[]> { }): Promise<RustSdkCryptoJs.ProcessedToDeviceEvent[]> {
const result = await this.olmMachine.receiveSyncChanges( return await this.olmMachine.receiveSyncChanges(
events ? JSON.stringify(events) : "[]", events ? JSON.stringify(events) : "[]",
devices, devices,
oneTimeKeysCounts, oneTimeKeysCounts,
unusedFallbackKeys, unusedFallbackKeys,
); );
return result.map((processed) => JSON.parse(processed.rawEvent));
} }
/** called by the sync loop to preprocess incoming to-device messages /** called by the sync loop to preprocess incoming to-device messages
@ -1509,22 +1507,56 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
* @param events - the received to-device messages * @param events - the received to-device messages
* @returns A list of preprocessed to-device messages. * @returns A list of preprocessed to-device messages.
*/ */
public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]> { public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<ReceivedToDeviceMessage[]> {
// send the received to-device messages into receiveSyncChanges. We have no info on device-list changes, // send the received to-device messages into receiveSyncChanges. We have no info on device-list changes,
// one-time-keys, or fallback keys, so just pass empty data. // one-time-keys, or fallback keys, so just pass empty data.
const processed = await this.receiveSyncChanges({ events }); const processed = await this.receiveSyncChanges({ events });
// look for interesting to-device messages const received: ReceivedToDeviceMessage[] = [];
for (const message of processed) { for (const message of processed) {
if (message.type === EventType.KeyVerificationRequest) { const parsedMessage: IToDeviceEvent = JSON.parse(message.rawEvent);
const sender = message.sender;
const transactionId = message.content.transaction_id; // look for interesting to-device messages
if (parsedMessage.type === EventType.KeyVerificationRequest) {
const sender = parsedMessage.sender;
const transactionId = parsedMessage.content.transaction_id;
if (transactionId && sender) { if (transactionId && sender) {
this.onIncomingKeyVerificationRequest(sender, transactionId); this.onIncomingKeyVerificationRequest(sender, transactionId);
} }
} }
switch (message.type) {
case RustSdkCryptoJs.ProcessedToDeviceEventType.Decrypted: {
const encryptionInfo = (message as RustSdkCryptoJs.DecryptedToDeviceEvent).encryptionInfo;
received.push({
message: parsedMessage,
encryptionInfo: {
sender: encryptionInfo.sender.toString(),
senderDevice: encryptionInfo.senderDevice?.toString(),
senderCurve25519KeyBase64: encryptionInfo.senderCurve25519Key,
senderVerified: encryptionInfo.isSenderVerified(),
},
});
break;
}
case RustSdkCryptoJs.ProcessedToDeviceEventType.PlainText: {
received.push({
message: parsedMessage,
encryptionInfo: null,
});
break;
}
case RustSdkCryptoJs.ProcessedToDeviceEventType.UnableToDecrypt:
// ignore messages we cannot decrypt
break;
case RustSdkCryptoJs.ProcessedToDeviceEventType.Invalid:
// ignore invalid messages
break;
}
} }
return processed;
return received;
} }
/** called by the sync loop to process one time key counts and unused fallback keys /** called by the sync loop to process one time key counts and unused fallback keys

View File

@ -37,6 +37,7 @@ import {
type IStateEvent, type IStateEvent,
type IStrippedState, type IStrippedState,
type ISyncResponse, type ISyncResponse,
type ReceivedToDeviceMessage,
} from "./sync-accumulator.ts"; } from "./sync-accumulator.ts";
import { MatrixError } from "./http-api/index.ts"; import { MatrixError } from "./http-api/index.ts";
import { import {
@ -150,11 +151,20 @@ class ExtensionToDevice implements Extension<ExtensionToDeviceRequest, Extension
} }
public async onResponse(data: ExtensionToDeviceResponse): Promise<void> { public async onResponse(data: ExtensionToDeviceResponse): Promise<void> {
let events = data["events"] || []; const events = data["events"] || [];
if (events.length > 0 && this.cryptoCallbacks) { let receivedToDeviceMessages: ReceivedToDeviceMessage[];
events = await this.cryptoCallbacks.preprocessToDeviceMessages(events); if (this.cryptoCallbacks) {
receivedToDeviceMessages = await this.cryptoCallbacks.preprocessToDeviceMessages(events);
} else {
receivedToDeviceMessages = events.map((rawEvent) =>
// Crypto is not enabled, so we just return the events.
({
message: rawEvent,
encryptionInfo: null,
}),
);
} }
processToDeviceMessages(events, this.client); processToDeviceMessages(receivedToDeviceMessages, this.client);
this.nextBatch = data.next_batch; this.nextBatch = data.next_batch;
} }

View File

@ -25,6 +25,7 @@ import { type IRoomSummary } from "./models/room-summary.ts";
import { type EventType } from "./@types/event.ts"; import { type EventType } from "./@types/event.ts";
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync.ts"; import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync.ts";
import { ReceiptAccumulator } from "./receipt-accumulator.ts"; import { ReceiptAccumulator } from "./receipt-accumulator.ts";
import { type OlmEncryptionInfo } from "./crypto-api/index.ts";
interface IOpts { interface IOpts {
/** /**
@ -134,12 +135,31 @@ interface IAccountData {
events: IMinimalEvent[]; events: IMinimalEvent[];
} }
/** A to-device message as received from the sync. */
export interface IToDeviceEvent { export interface IToDeviceEvent {
content: IContent; content: IContent;
sender: string; sender: string;
type: string; type: string;
} }
/**
* A (possibly decrypted) to-device message after it has been successfully processed by the sdk.
*
* If the message was encrypted, the `encryptionInfo` field will contain the encryption information.
* If the message was sent in clear, this field will be null.
*
* The `message` field contains the message `type`, `content`, and `sender` as if the message was sent in clear.
*/
export interface ReceivedToDeviceMessage {
/** The message type, content, and sender as if the message was sent in clear. */
message: IToDeviceEvent;
/**
* Information about the encryption of the message.
* Will be null if the message was sent in clear
*/
encryptionInfo: OlmEncryptionInfo | null;
}
interface IToDevice { interface IToDevice {
events: IToDeviceEvent[]; events: IToDeviceEvent[];
} }

View File

@ -44,8 +44,8 @@ import {
type IInvitedRoom, type IInvitedRoom,
type IInviteState, type IInviteState,
type IJoinedRoom, type IJoinedRoom,
type ILeftRoom,
type IKnockedRoom, type IKnockedRoom,
type ILeftRoom,
type IMinimalEvent, type IMinimalEvent,
type IRoomEvent, type IRoomEvent,
type IStateEvent, type IStateEvent,
@ -53,13 +53,14 @@ import {
type ISyncResponse, type ISyncResponse,
type ITimeline, type ITimeline,
type IToDeviceEvent, type IToDeviceEvent,
type ReceivedToDeviceMessage,
} from "./sync-accumulator.ts"; } from "./sync-accumulator.ts";
import { MatrixEvent, type IEvent } from "./models/event.ts"; import { MatrixEvent } from "./models/event.ts";
import { type MatrixError, Method } from "./http-api/index.ts"; import { type MatrixError, Method } from "./http-api/index.ts";
import { type ISavedSync } from "./store/index.ts"; import { type ISavedSync } from "./store/index.ts";
import { EventType } from "./@types/event.ts"; import { EventType } from "./@types/event.ts";
import { type IPushRules } from "./@types/PushRules.ts"; import { type IPushRules } from "./@types/PushRules.ts";
import { RoomStateEvent, type IMarkerFoundOptions } from "./models/room-state.ts"; import { type IMarkerFoundOptions, RoomStateEvent } from "./models/room-state.ts";
import { RoomMemberEvent } from "./models/room-member.ts"; import { RoomMemberEvent } from "./models/room-member.ts";
import { BeaconEvent } from "./models/beacon.ts"; import { BeaconEvent } from "./models/beacon.ts";
import { type IEventsResponse } from "./@types/requests.ts"; import { type IEventsResponse } from "./@types/requests.ts";
@ -1144,13 +1145,23 @@ export class SyncApi {
// handle to-device events // handle to-device events
if (data.to_device && Array.isArray(data.to_device.events) && data.to_device.events.length > 0) { if (data.to_device && Array.isArray(data.to_device.events) && data.to_device.events.length > 0) {
let toDeviceMessages: IToDeviceEvent[] = data.to_device.events.filter(noUnsafeEventProps); const toDeviceMessages: IToDeviceEvent[] = data.to_device.events.filter(noUnsafeEventProps);
let receivedToDeviceMessages: ReceivedToDeviceMessage[];
if (this.syncOpts.cryptoCallbacks) { if (this.syncOpts.cryptoCallbacks) {
toDeviceMessages = await this.syncOpts.cryptoCallbacks.preprocessToDeviceMessages(toDeviceMessages); receivedToDeviceMessages =
await this.syncOpts.cryptoCallbacks.preprocessToDeviceMessages(toDeviceMessages);
} else {
receivedToDeviceMessages = toDeviceMessages.map((rawEvent) =>
// Crypto is not enabled, so we just return the events.
({
message: rawEvent,
encryptionInfo: null,
}),
);
} }
processToDeviceMessages(toDeviceMessages, client); processToDeviceMessages(receivedToDeviceMessages, client);
} else { } else {
// no more to-device events: we can stop polling with a short timeout. // no more to-device events: we can stop polling with a short timeout.
this.catchingUp = false; this.catchingUp = false;
@ -1909,21 +1920,21 @@ export function _createAndReEmitRoom(client: MatrixClient, roomId: string, opts:
/** /**
* Process a list of (decrypted, where possible) received to-device events. * Process a list of (decrypted, where possible) received to-device events.
* *
* Converts the events into `MatrixEvent`s, and emits appropriate {@link ClientEvent.ToDeviceEvent} events. * Emits the appropriate {@link ClientEvent.ReceivedToDeviceMessage} event.
* Also converts the events into `MatrixEvent`s, and emits the now deprecated {@link ClientEvent.ToDeviceEvent} events for compatibility.
* */ * */
export function processToDeviceMessages(toDeviceMessages: IToDeviceEvent[], client: MatrixClient): void { export function processToDeviceMessages(toDeviceMessages: ReceivedToDeviceMessage[], client: MatrixClient): void {
const cancelledKeyVerificationTxns: string[] = []; const cancelledKeyVerificationTxns: string[] = [];
toDeviceMessages toDeviceMessages
.map(mapToDeviceEvent) .map((processedMessage) => {
.map((toDeviceEvent) => {
// map is a cheap inline forEach // map is a cheap inline forEach
// We want to flag m.key.verification.start events as cancelled // We want to flag m.key.verification.start events as cancelled
// if there's an accompanying m.key.verification.cancel event, so // if there's an accompanying m.key.verification.cancel event, so
// we pull out the transaction IDs from the cancellation events // we pull out the transaction IDs from the cancellation events
// so we can flag the verification events as cancelled in the loop // so we can flag the verification events as cancelled in the loop
// below. // below.
if (toDeviceEvent.getType() === "m.key.verification.cancel") { if (processedMessage.message.type === "m.key.verification.cancel") {
const txnId: string = toDeviceEvent.getContent()["transaction_id"]; const txnId: string = processedMessage.message.content["transaction_id"];
if (txnId) { if (txnId) {
cancelledKeyVerificationTxns.push(txnId); cancelledKeyVerificationTxns.push(txnId);
} }
@ -1931,32 +1942,40 @@ export function processToDeviceMessages(toDeviceMessages: IToDeviceEvent[], clie
// as mentioned above, .map is a cheap inline forEach, so return // as mentioned above, .map is a cheap inline forEach, so return
// the unmodified event. // the unmodified event.
return toDeviceEvent; return processedMessage;
}) })
.forEach(function (toDeviceEvent) { .forEach(function (processedEvent) {
const content = toDeviceEvent.getContent(); // For backwards compatibility, we also emit the event as a `MatrixEvent` using `ClientEvent.ToDeviceEvent`.
if (toDeviceEvent.getType() == "m.room.message" && content.msgtype == "m.bad.encrypted") { {
// the mapper already logged a warning. const toDeviceEvent = processedEvent.message;
logger.log("Ignoring undecryptable to-device event from " + toDeviceEvent.getSender()); const content = toDeviceEvent.content;
return; // The message is cloned before being passed to the MatrixEvent constructor, because
} // the `makeEncrypted` method will mutate the type and content properties of the original message and will interfere
// with the emitted event for `ReceivedToDeviceMessage`.
if ( const deprecatedCompatibilityEvent = new MatrixEvent(Object.assign({}, toDeviceEvent));
toDeviceEvent.getType() === "m.key.verification.start" || if (
toDeviceEvent.getType() === "m.key.verification.request" toDeviceEvent.type === "m.key.verification.start" ||
) { toDeviceEvent.type === "m.key.verification.request"
const txnId = content["transaction_id"]; ) {
if (cancelledKeyVerificationTxns.includes(txnId)) { const txnId = content["transaction_id"];
toDeviceEvent.flagCancelled(); if (cancelledKeyVerificationTxns.includes(txnId)) {
deprecatedCompatibilityEvent.flagCancelled();
}
} }
if (processedEvent.encryptionInfo) {
// Restore partially the legacy behavior to detect encrypted messages.
// Now `event.isEncrypted()` will return true.
deprecatedCompatibilityEvent.makeEncrypted(
EventType.RoomMessageEncrypted,
{ ciphertext: "" },
processedEvent.encryptionInfo.senderCurve25519KeyBase64,
"",
);
}
client.emit(ClientEvent.ToDeviceEvent, deprecatedCompatibilityEvent);
} }
client.emit(ClientEvent.ToDeviceEvent, toDeviceEvent); client.emit(ClientEvent.ReceivedToDeviceMessage, processedEvent);
}); });
} }
function mapToDeviceEvent(plainOldJsObject: Partial<IEvent>): MatrixEvent {
// to-device events should not have a `room_id` property, but let's be sure
delete plainOldJsObject.room_id;
return new MatrixEvent(plainOldJsObject);
}