From 0a587e9d097aa2dd0b118049bead7664280093fe Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 2 Apr 2025 15:33:54 +0200 Subject: [PATCH 1/9] MatrixRTC: ToDevice distribution for media stream keys --- src/client.ts | 17 ++- src/embedded.ts | 16 +++ src/matrixrtc/EncryptionManager.ts | 1 + src/matrixrtc/MatrixRTCSession.ts | 27 ++++- src/matrixrtc/ToDeviceKeyTransport.ts | 167 ++++++++++++++++++++++++++ src/matrixrtc/types.ts | 18 +++ 6 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 src/matrixrtc/ToDeviceKeyTransport.ts diff --git a/src/client.ts b/src/client.ts index 475ee77f093..b28594c303b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -207,7 +207,7 @@ import { import { M_BEACON_INFO, type MBeaconInfoEventContent } from "./@types/beacon.ts"; import { NamespacedValue, UnstableValue } from "./NamespacedValue.ts"; import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue.ts"; -import { type ToDeviceBatch } from "./models/ToDeviceMessage.ts"; +import { type ToDeviceBatch, type ToDevicePayload } from "./models/ToDeviceMessage.ts"; import { IgnoredInvites } from "./models/invites-ignorer.ts"; import { type UIARequest } from "./@types/uia.ts"; import { type LocalNotificationSettings } from "./@types/local_notifications.ts"; @@ -7942,6 +7942,21 @@ export class MatrixClient extends TypedEventEmitter { + if (!this.cryptoBackend) { + throw new Error("Cannot encrypt to device event, your client does not support encryption."); + } + const batch = await this.cryptoBackend.encryptToDeviceMessages(eventType, devices, payload); + + // TODO The batch mechanism removes all possibility to get error feedbacks.. + // We might want instead to do the API call directly and pass the errors back. + await this.queueToDevice(batch); + } + /** * Sends events directly to specific devices using Matrix's to-device * messaging system. The batch will be split up into appropriately sized diff --git a/src/embedded.ts b/src/embedded.ts index 0882872e5ad..3bd36ca7781 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -464,6 +464,22 @@ export class RoomWidgetClient extends MatrixClient { return {}; } + public async encryptAndSendToDevice( + eventType: string, + devices: { userId: string; deviceId: string }[], + payload: ToDevicePayload, + ): Promise { + // map: user Id → device Id → payload + const contentMap: MapWithDefault> = new MapWithDefault(() => new Map()); + for (const { userId, deviceId } of devices) { + contentMap.getOrCreate(userId).set(deviceId, payload); + } + + await this.widgetApi + .sendToDevice(eventType, true, recursiveMapToObject(contentMap)) + .catch(timeoutToConnectionError); + } + public async sendToDevice(eventType: string, contentMap: SendToDeviceContentMap): Promise { await this.widgetApi .sendToDevice(eventType, false, recursiveMapToObject(contentMap)) diff --git a/src/matrixrtc/EncryptionManager.ts b/src/matrixrtc/EncryptionManager.ts index b97cd9dd338..b6542348462 100644 --- a/src/matrixrtc/EncryptionManager.ts +++ b/src/matrixrtc/EncryptionManager.ts @@ -332,6 +332,7 @@ export class EncryptionManager implements IEncryptionManager { timestamp: number, delayBeforeUse = false, ): void { + logger.debug(`Setting encryption key for ${userId}:${deviceId} at index ${encryptionKeyIndex}`); const keyBin = decodeBase64(encryptionKeyString); const participantId = getParticipantId(userId, deviceId); diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 0c8cb6ee6ca..7de322bd703 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -28,9 +28,10 @@ import { MembershipManager } from "./NewMembershipManager.ts"; import { EncryptionManager, type IEncryptionManager } from "./EncryptionManager.ts"; import { LegacyMembershipManager } from "./LegacyMembershipManager.ts"; import { logDurationSync } from "../utils.ts"; -import { RoomKeyTransport } from "./RoomKeyTransport.ts"; -import { type IMembershipManager } from "./IMembershipManager.ts"; +import { ToDeviceKeyTransport } from "./ToDeviceKeyTransport.ts"; import { type Statistics } from "./types.ts"; +import { RoomKeyTransport } from "./RoomKeyTransport.ts"; +import type { IMembershipManager } from "./IMembershipManager.ts"; const logger = rootLogger.getChild("MatrixRTCSession"); @@ -125,6 +126,11 @@ export interface MembershipConfig { * The maximum number of retries that the manager will do for delayed event sending/updating and state event sending when a network error occurs. */ maximumNetworkErrorRetryCount?: number; + + /** + * If true, use the new to-device transport for sending encryption keys. + */ + useExperimentalToDeviceTransport?: boolean; } export interface EncryptionConfig { @@ -303,6 +309,9 @@ export class MatrixRTCSession extends TypedEventEmitter, private roomSubset: Pick< @@ -370,7 +379,19 @@ export class MatrixRTCSession extends TypedEventEmitter + implements IKeyTransport +{ + private readonly prefixedLogger: Logger; + + public constructor( + private userId: string, + private deviceId: string, + private roomId: string, + private client: Pick, + private statistics: Statistics, + ) { + super(); + this.prefixedLogger = logger.getChild(`[RTC: ${roomId} ToDeviceKeyTransport]`); + } + + public start(): void { + this.client.on(ClientEvent.ToDeviceEvent, (ev) => this.onToDeviceEvent(ev)); + } + + public stop(): void { + this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); + } + + public async sendKey(keyBase64Encoded: string, index: number, members: CallMembership[]): Promise { + const content: EncryptionKeysToDeviceEventContent = { + keys: { + index: index, + key: keyBase64Encoded, + }, + roomId: this.roomId, + member: { + claimed_device_id: this.deviceId, + }, + session: { + call_id: "", + application: "m.call", + scope: "m.room", + }, + }; + + const targets = members + .filter((member) => { + // filter malformed call members + if (member.sender == undefined || member.deviceId == undefined) { + logger.warn(`Malformed call member: ${member.sender}|${member.deviceId}`); + return false; + } + // Filter out me + return !(member.sender == this.userId && member.deviceId == this.deviceId); + }) + .map((member) => { + return { + userId: member.sender!, + deviceId: member.deviceId!, + }; + }); + + if (targets.length > 0) { + await this.client.encryptAndSendToDevice(EventType.CallEncryptionKeysPrefix, targets, content); + } else { + this.prefixedLogger.warn("No targets found for sending key"); + } + } + + private receiveCallKeyEvent(fromUser: string, content: EncryptionKeysToDeviceEventContent): void { + // The event has already been validated at this point. + + this.statistics.counters.roomEventEncryptionKeysReceived += 1; + + // What is this, and why is it needed? + // Also to device events do not have an origin server ts + const now = Date.now(); + const age = now - (typeof content.sent_ts === "number" ? content.sent_ts : now); + this.statistics.totals.roomEventEncryptionKeysReceivedTotalAge += age; + + this.emit( + KeyTransportEvents.ReceivedKeys, + // TODO this is claimed information + fromUser, + // TODO: This is claimed information + content.member.claimed_device_id!, + content.keys.key, + content.keys.index, + age, + ); + } + + private onToDeviceEvent = (event: MatrixEvent): void => { + if (event.getType() !== EventType.CallEncryptionKeysPrefix) { + // Ignore this is not a call encryption event + return; + } + + // TODO: Not possible to check if the event is encrypted or not + // see https://github.com/matrix-org/matrix-rust-sdk/issues/4883 + // if (evnt.getWireType() != EventType.RoomMessageEncrypted) { + // // WARN: The call keys were sent in clear. Ignore them + // logger.warn(`Call encryption keys sent in clear from: ${event.getSender()}`); + // return; + // } + + const content = this.getValidEventContent(event); + if (!content) return; + + if (!event.getSender()) return; + + this.receiveCallKeyEvent(event.getSender()!, content); + }; + + private getValidEventContent(event: MatrixEvent): EncryptionKeysToDeviceEventContent | undefined { + const content = event.getContent(); + const roomId = content.roomId; + if (!roomId) { + // Invalid event + this.prefixedLogger.warn("Malformed Event: invalid call encryption keys event, no roomId"); + return; + } + if (roomId !== this.roomId) { + this.prefixedLogger.warn("Malformed Event: Mismatch roomId"); + return; + } + + if (!content.keys || !content.keys.key || typeof content.keys.index !== "number") { + this.prefixedLogger.warn("Malformed Event: Missing keys field"); + return; + } + + if (!content.member || !content.member.claimed_device_id) { + this.prefixedLogger.warn("Malformed Event: Missing claimed_device_id"); + return; + } + + // TODO session is not used so far + // if (!content.session || !content.session.call_id || !content.session.scope || !content.session.application) { + // this.prefixedLogger.warn("Malformed Event: Missing/Malformed content.session", content.session); + // return; + // } + return content; + } +} diff --git a/src/matrixrtc/types.ts b/src/matrixrtc/types.ts index ee8c654bb9b..9d4fe4ea60c 100644 --- a/src/matrixrtc/types.ts +++ b/src/matrixrtc/types.ts @@ -28,6 +28,24 @@ export interface EncryptionKeysEventContent { sent_ts?: number; } +export interface EncryptionKeysToDeviceEventContent { + keys: { index: number; key: string }; + member: { + // id: ParticipantId, + // TODO Remove that it is claimed, need to get the sealed sender from decryption info + claimed_device_id: string; + // user_id: string + }; + roomId: string; + session: { + application: string; + call_id: string; + scope: string; + }; + // Why is this needed? + sent_ts?: number; +} + export type CallNotifyType = "ring" | "notify"; export interface ICallNotifyContent { From 233fd85338170dab10e3af5edfa5a93fb178ea75 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 7 Apr 2025 17:51:24 +0200 Subject: [PATCH 2/9] test: Add RTC to device transport test --- spec/unit/matrixrtc/RoomKeyTransport.spec.ts | 2 +- .../matrixrtc/ToDeviceKeyTransport.spec.ts | 258 ++++++++++++++++++ spec/unit/matrixrtc/mocks.ts | 2 +- src/matrixrtc/MatrixRTCSession.ts | 1 + src/matrixrtc/ToDeviceKeyTransport.ts | 16 +- src/matrixrtc/types.ts | 2 +- 6 files changed, 271 insertions(+), 10 deletions(-) create mode 100644 spec/unit/matrixrtc/ToDeviceKeyTransport.spec.ts diff --git a/spec/unit/matrixrtc/RoomKeyTransport.spec.ts b/spec/unit/matrixrtc/RoomKeyTransport.spec.ts index a5e462be826..0d0db2e4fff 100644 --- a/spec/unit/matrixrtc/RoomKeyTransport.spec.ts +++ b/spec/unit/matrixrtc/RoomKeyTransport.spec.ts @@ -20,7 +20,7 @@ import { KeyTransportEvents } from "../../../src/matrixrtc/IKeyTransport"; import { EventType, MatrixClient, RoomEvent } from "../../../src"; import type { IRoomTimelineData, MatrixEvent, Room } from "../../../src"; -describe("RoomKyTransport", () => { +describe("RoomKeyTransport", () => { let client: MatrixClient; let room: Room & { emitTimelineEvent: (event: MatrixEvent) => void; diff --git a/spec/unit/matrixrtc/ToDeviceKeyTransport.spec.ts b/spec/unit/matrixrtc/ToDeviceKeyTransport.spec.ts new file mode 100644 index 00000000000..2b813a9ac2f --- /dev/null +++ b/spec/unit/matrixrtc/ToDeviceKeyTransport.spec.ts @@ -0,0 +1,258 @@ +/* +Copyright 2025 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 { makeMockEvent, membershipTemplate, mockCallMembership } from "./mocks"; +import { ClientEvent, EventType, MatrixClient } from "../../../src"; +import { ToDeviceKeyTransport } from "../../../src/matrixrtc/ToDeviceKeyTransport.ts"; +import { Mocked } from "jest-mock"; +import { getMockClientWithEventEmitter } from "../../test-utils/client.ts"; +import { Statistics } from "../../../src/matrixrtc"; +import { KeyTransportEvents } from "../../../src/matrixrtc/IKeyTransport.ts"; +import { defer } from "../../../src/utils.ts"; +import { Logger } from "../../../src/logger.ts"; + +describe("ToDeviceKeyTransport", () => { + const roomId = "!room:id"; + + let mockClient: Mocked; + let statistics: Statistics; + let mockLogger: Mocked; + let transport: ToDeviceKeyTransport; + + function setup() { + mockClient = getMockClientWithEventEmitter({ + encryptAndSendToDevice: jest.fn(), + }); + mockLogger = { + debug: jest.fn(), + warn: jest.fn(), + } as unknown as Mocked; + statistics = { + counters: { + roomEventEncryptionKeysSent: 0, + roomEventEncryptionKeysReceived: 0, + }, + totals: { + roomEventEncryptionKeysReceivedTotalAge: 0, + }, + }; + + transport = new ToDeviceKeyTransport("@alice:example.org", "MYDEVICE", roomId, mockClient, statistics, { + getChild: jest.fn().mockReturnValue(mockLogger), + } as unknown as Mocked); + } + + it("should send my keys on via to device", async () => { + setup(); + + transport.start(); + + const keyBase64Encoded = "ABCDEDF"; + const keyIndex = 2; + await transport.sendKey(keyBase64Encoded, keyIndex, [ + mockCallMembership( + Object.assign({}, membershipTemplate, { device_id: "BOBDEVICE" }), + roomId, + "@bob:example.org", + ), + mockCallMembership( + Object.assign({}, membershipTemplate, { device_id: "CARLDEVICE" }), + roomId, + "@carl:example.org", + ), + mockCallMembership( + Object.assign({}, membershipTemplate, { device_id: "MATDEVICE" }), + roomId, + "@mat:example.org", + ), + ]); + + expect(mockClient.encryptAndSendToDevice).toHaveBeenCalledTimes(1); + expect(mockClient.encryptAndSendToDevice).toHaveBeenCalledWith( + "io.element.call.encryption_keys", + [ + { userId: "@bob:example.org", deviceId: "BOBDEVICE" }, + { userId: "@carl:example.org", deviceId: "CARLDEVICE" }, + { userId: "@mat:example.org", deviceId: "MATDEVICE" }, + ], + { + keys: { + index: keyIndex, + key: keyBase64Encoded, + }, + member: { + claimed_device_id: "MYDEVICE", + }, + room_id: roomId, + session: { + application: "m.call", + call_id: "", + scope: "m.room", + }, + }, + ); + + expect(statistics.counters.roomEventEncryptionKeysSent).toBe(1); + }); + + it("should emit when a key is received", async () => { + setup(); + + const deferred = defer<{ userId: string; deviceId: string; keyBase64Encoded: string; index: number }>(); + transport.on(KeyTransportEvents.ReceivedKeys, (userId, deviceId, keyBase64Encoded, index, timestamp) => { + deferred.resolve({ userId, deviceId, keyBase64Encoded, index }); + }); + transport.start(); + + const testEncoded = "ABCDEDF"; + const testKeyIndex = 2; + + mockClient.emit( + ClientEvent.ToDeviceEvent, + makeMockEvent(EventType.CallEncryptionKeysPrefix, "@bob:example.org", undefined, { + keys: { + index: testKeyIndex, + key: testEncoded, + }, + member: { + claimed_device_id: "BOBDEVICE", + }, + room_id: roomId, + session: { + application: "m.call", + call_id: "", + scope: "m.room", + }, + }), + ); + + const { userId, deviceId, keyBase64Encoded, index } = await deferred.promise; + expect(userId).toBe("@bob:example.org"); + expect(deviceId).toBe("BOBDEVICE"); + expect(keyBase64Encoded).toBe(testEncoded); + expect(index).toBe(testKeyIndex); + + expect(statistics.counters.roomEventEncryptionKeysReceived).toBe(1); + }); + + it("should not sent to ourself", async () => { + setup(); + + const keyBase64Encoded = "ABCDEDF"; + const keyIndex = 2; + await transport.sendKey(keyBase64Encoded, keyIndex, [ + mockCallMembership( + Object.assign({}, membershipTemplate, { device_id: "MYDEVICE" }), + roomId, + "@alice:example.org", + ), + ]); + + transport.start(); + + expect(mockClient.encryptAndSendToDevice).toHaveBeenCalledTimes(0); + }); + + it("should warn when there is a room mismatch", async () => { + setup(); + + transport.start(); + + const testEncoded = "ABCDEDF"; + const testKeyIndex = 2; + + mockClient.emit( + ClientEvent.ToDeviceEvent, + makeMockEvent(EventType.CallEncryptionKeysPrefix, "@bob:example.org", undefined, { + keys: { + index: testKeyIndex, + key: testEncoded, + }, + member: { + claimed_device_id: "BOBDEVICE", + }, + room_id: "!anotherroom:id", + session: { + application: "m.call", + call_id: "", + scope: "m.room", + }, + }), + ); + + expect(mockLogger.warn).toHaveBeenCalledWith("Malformed Event: Mismatch roomId"); + expect(statistics.counters.roomEventEncryptionKeysReceived).toBe(0); + }); + + describe("malformed events", () => { + const MALFORMED_EVENT = [ + { + keys: {}, + member: { claimed_device_id: "MYDEVICE" }, + room_id: "!room:id", + session: { application: "m.call", call_id: "", scope: "m.room" }, + }, + { + keys: { index: 0 }, + member: { claimed_device_id: "MYDEVICE" }, + room_id: "!room:id", + session: { application: "m.call", call_id: "", scope: "m.room" }, + }, + { + keys: { keys: "ABCDEF" }, + member: { claimed_device_id: "MYDEVICE" }, + room_id: "!room:id", + session: { application: "m.call", call_id: "", scope: "m.room" }, + }, + { + keys: { keys: "ABCDEF", index: 2 }, + room_id: "!room:id", + session: { application: "m.call", call_id: "", scope: "m.room" }, + }, + { + keys: { keys: "ABCDEF", index: 2 }, + member: {}, + room_id: "!room:id", + session: { application: "m.call", call_id: "", scope: "m.room" }, + }, + { + keys: { keys: "ABCDEF", index: 2 }, + member: { claimed_device_id: "MYDEVICE" }, + session: { application: "m.call", call_id: "", scope: "m.room" }, + }, + { + keys: { keys: "ABCDEF", index: 2 }, + member: { claimed_device_id: "MYDEVICE" }, + room_id: "!room:id", + session: { application: "m.call", call_id: "", scope: "m.room" }, + }, + ]; + + test.each(MALFORMED_EVENT)("should warn on malformed event %j", async (event) => { + setup(); + + transport.start(); + + mockClient.emit( + ClientEvent.ToDeviceEvent, + makeMockEvent(EventType.CallEncryptionKeysPrefix, "@bob:example.org", undefined, event), + ); + + expect(mockLogger.warn).toHaveBeenCalled(); + expect(statistics.counters.roomEventEncryptionKeysReceived).toBe(0); + }); + }); +}); diff --git a/spec/unit/matrixrtc/mocks.ts b/spec/unit/matrixrtc/mocks.ts index 5a485e7d41e..f20a9364efb 100644 --- a/spec/unit/matrixrtc/mocks.ts +++ b/spec/unit/matrixrtc/mocks.ts @@ -123,7 +123,7 @@ export function makeMockRoomState(membershipData: MembershipData, roomId: string export function makeMockEvent( type: string, sender: string, - roomId: string, + roomId: string | undefined, content: any, timestamp?: number, ): MatrixEvent { diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 7de322bd703..18b704122e8 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -388,6 +388,7 @@ export class MatrixRTCSession extends TypedEventEmitter, private statistics: Statistics, + logger: Logger, ) { super(); - this.prefixedLogger = logger.getChild(`[RTC: ${roomId} ToDeviceKeyTransport]`); + this.prefixedLogger = logger.getChild(`[${roomId} ToDeviceKeyTransport]`); } public start(): void { @@ -54,7 +55,7 @@ export class ToDeviceKeyTransport index: index, key: keyBase64Encoded, }, - roomId: this.roomId, + room_id: this.roomId, member: { claimed_device_id: this.deviceId, }, @@ -69,7 +70,7 @@ export class ToDeviceKeyTransport .filter((member) => { // filter malformed call members if (member.sender == undefined || member.deviceId == undefined) { - logger.warn(`Malformed call member: ${member.sender}|${member.deviceId}`); + this.prefixedLogger.warn(`Malformed call member: ${member.sender}|${member.deviceId}`); return false; } // Filter out me @@ -84,6 +85,7 @@ export class ToDeviceKeyTransport if (targets.length > 0) { await this.client.encryptAndSendToDevice(EventType.CallEncryptionKeysPrefix, targets, content); + this.statistics.counters.roomEventEncryptionKeysSent += 1; } else { this.prefixedLogger.warn("No targets found for sending key"); } @@ -135,8 +137,8 @@ export class ToDeviceKeyTransport }; private getValidEventContent(event: MatrixEvent): EncryptionKeysToDeviceEventContent | undefined { - const content = event.getContent(); - const roomId = content.roomId; + const content = event.getContent(); + const roomId = content.room_id; if (!roomId) { // Invalid event this.prefixedLogger.warn("Malformed Event: invalid call encryption keys event, no roomId"); @@ -162,6 +164,6 @@ export class ToDeviceKeyTransport // this.prefixedLogger.warn("Malformed Event: Missing/Malformed content.session", content.session); // return; // } - return content; + return content as EncryptionKeysToDeviceEventContent; } } diff --git a/src/matrixrtc/types.ts b/src/matrixrtc/types.ts index 9d4fe4ea60c..d408080dfa1 100644 --- a/src/matrixrtc/types.ts +++ b/src/matrixrtc/types.ts @@ -36,7 +36,7 @@ export interface EncryptionKeysToDeviceEventContent { claimed_device_id: string; // user_id: string }; - roomId: string; + room_id: string; session: { application: string; call_id: string; From b3b53474c0f586977849057688795325d58eada8 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 7 Apr 2025 17:58:10 +0200 Subject: [PATCH 3/9] lint --- spec/unit/matrixrtc/ToDeviceKeyTransport.spec.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/spec/unit/matrixrtc/ToDeviceKeyTransport.spec.ts b/spec/unit/matrixrtc/ToDeviceKeyTransport.spec.ts index 2b813a9ac2f..dcf8ed82029 100644 --- a/spec/unit/matrixrtc/ToDeviceKeyTransport.spec.ts +++ b/spec/unit/matrixrtc/ToDeviceKeyTransport.spec.ts @@ -14,15 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { type Mocked } from "jest-mock"; + import { makeMockEvent, membershipTemplate, mockCallMembership } from "./mocks"; -import { ClientEvent, EventType, MatrixClient } from "../../../src"; +import { ClientEvent, EventType, type MatrixClient } from "../../../src"; import { ToDeviceKeyTransport } from "../../../src/matrixrtc/ToDeviceKeyTransport.ts"; -import { Mocked } from "jest-mock"; import { getMockClientWithEventEmitter } from "../../test-utils/client.ts"; -import { Statistics } from "../../../src/matrixrtc"; +import { type Statistics } from "../../../src/matrixrtc"; import { KeyTransportEvents } from "../../../src/matrixrtc/IKeyTransport.ts"; import { defer } from "../../../src/utils.ts"; -import { Logger } from "../../../src/logger.ts"; +import { type Logger } from "../../../src/logger.ts"; describe("ToDeviceKeyTransport", () => { const roomId = "!room:id"; @@ -166,7 +167,7 @@ describe("ToDeviceKeyTransport", () => { expect(mockClient.encryptAndSendToDevice).toHaveBeenCalledTimes(0); }); - it("should warn when there is a room mismatch", async () => { + it("should warn when there is a room mismatch", () => { setup(); transport.start(); @@ -241,7 +242,7 @@ describe("ToDeviceKeyTransport", () => { }, ]; - test.each(MALFORMED_EVENT)("should warn on malformed event %j", async (event) => { + test.each(MALFORMED_EVENT)("should warn on malformed event %j", (event) => { setup(); transport.start(); From 598a31cf9d30580422b3528fb98cbe6d8287c261 Mon Sep 17 00:00:00 2001 From: Timo Date: Tue, 8 Apr 2025 17:36:18 +0200 Subject: [PATCH 4/9] fix key indexing --- src/matrixrtc/EncryptionManager.ts | 31 ++++++++++++++++-------------- src/matrixrtc/RoomKeyTransport.ts | 2 +- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/matrixrtc/EncryptionManager.ts b/src/matrixrtc/EncryptionManager.ts index b6542348462..821e99ef8e0 100644 --- a/src/matrixrtc/EncryptionManager.ts +++ b/src/matrixrtc/EncryptionManager.ts @@ -80,8 +80,8 @@ export class EncryptionManager implements IEncryptionManager { // if it looks like a membership has been updated. private lastMembershipFingerprints: Set | undefined; - private currentEncryptionKeyIndex = -1; - + private mediaTrailerKeyIndexInUse = -1; + private latestGeneratedKeyIndex = -1; private joinConfig: EncryptionConfig | undefined; public constructor( @@ -254,8 +254,6 @@ export class EncryptionManager implements IEncryptionManager { if (!this.joined) return; - logger.info(`Sending encryption keys event. indexToSend=${indexToSend}`); - const myKeys = this.getKeysForParticipant(this.userId, this.deviceId); if (!myKeys) { @@ -263,19 +261,22 @@ export class EncryptionManager implements IEncryptionManager { return; } - if (typeof indexToSend !== "number" && this.currentEncryptionKeyIndex === -1) { + if (typeof indexToSend !== "number" && this.latestGeneratedKeyIndex === -1) { logger.warn("Tried to send encryption keys event but no current key index found!"); return; } - const keyIndexToSend = indexToSend ?? this.currentEncryptionKeyIndex; + const keyIndexToSend = indexToSend ?? this.latestGeneratedKeyIndex; + logger.info( + `Try sending encryption keys event. keyIndexToSend=${keyIndexToSend} (method parameter: ${indexToSend})`, + ); const keyToSend = myKeys[keyIndexToSend]; try { this.statistics.counters.roomEventEncryptionKeysSent += 1; await this.transport.sendKey(encodeUnpaddedBase64(keyToSend), keyIndexToSend, this.getMemberships()); logger.debug( - `Embedded-E2EE-LOG updateEncryptionKeyEvent participantId=${this.userId}:${this.deviceId} numKeys=${myKeys.length} currentKeyIndex=${this.currentEncryptionKeyIndex} keyIndexToSend=${keyIndexToSend}`, + `sendEncryptionKeysEvent participantId=${this.userId}:${this.deviceId} numKeys=${myKeys.length} currentKeyIndex=${this.latestGeneratedKeyIndex} keyIndexToSend=${keyIndexToSend}`, this.encryptionKeys, ); } catch (error) { @@ -290,6 +291,7 @@ export class EncryptionManager implements IEncryptionManager { }; public onNewKeyReceived: KeyTransportEventListener = (userId, deviceId, keyBase64Encoded, index, timestamp) => { + logger.debug(`Received key over key transport ${userId}:${deviceId} at index ${index}`); this.setEncryptionKey(userId, deviceId, index, keyBase64Encoded, timestamp); }; @@ -302,12 +304,12 @@ export class EncryptionManager implements IEncryptionManager { } private getNewEncryptionKeyIndex(): number { - if (this.currentEncryptionKeyIndex === -1) { + if (this.latestGeneratedKeyIndex === -1) { return 0; } // maximum key index is 255 - return (this.currentEncryptionKeyIndex + 1) % 256; + return (this.latestGeneratedKeyIndex + 1) % 256; } /** @@ -357,6 +359,7 @@ export class EncryptionManager implements IEncryptionManager { } } + this.latestGeneratedKeyIndex = encryptionKeyIndex; participantKeys[encryptionKeyIndex] = { key: keyBin, timestamp, @@ -365,18 +368,18 @@ export class EncryptionManager implements IEncryptionManager { if (delayBeforeUse) { const useKeyTimeout = setTimeout(() => { this.setNewKeyTimeouts.delete(useKeyTimeout); - logger.info(`Delayed-emitting key changed event for ${participantId} idx ${encryptionKeyIndex}`); + logger.info(`Delayed-emitting key changed event for ${participantId} index ${encryptionKeyIndex}`); if (userId === this.userId && deviceId === this.deviceId) { - this.currentEncryptionKeyIndex = encryptionKeyIndex; + this.mediaTrailerKeyIndexInUse = encryptionKeyIndex; } - this.onEncryptionKeysChanged(keyBin, encryptionKeyIndex, participantId); + this.onEncryptionKeysChanged(keyBin, this.mediaTrailerKeyIndexInUse, participantId); }, this.useKeyDelay); this.setNewKeyTimeouts.add(useKeyTimeout); } else { if (userId === this.userId && deviceId === this.deviceId) { - this.currentEncryptionKeyIndex = encryptionKeyIndex; + this.mediaTrailerKeyIndexInUse = encryptionKeyIndex; } - this.onEncryptionKeysChanged(keyBin, encryptionKeyIndex, participantId); + this.onEncryptionKeysChanged(keyBin, this.mediaTrailerKeyIndexInUse, participantId); } } diff --git a/src/matrixrtc/RoomKeyTransport.ts b/src/matrixrtc/RoomKeyTransport.ts index f255eb2a14a..e98207433be 100644 --- a/src/matrixrtc/RoomKeyTransport.ts +++ b/src/matrixrtc/RoomKeyTransport.ts @@ -168,7 +168,7 @@ export class RoomKeyTransport ); } else { logger.debug( - `Embedded-E2EE-LOG onCallEncryption userId=${userId}:${deviceId} encryptionKeyIndex=${encryptionKeyIndex} age=${age}ms`, + `onCallEncryption userId=${userId}:${deviceId} encryptionKeyIndex=${encryptionKeyIndex} age=${age}ms`, ); this.emit( KeyTransportEvents.ReceivedKeys, From 3dd55e456b79e713f3d8b538202e4708a16a755e Mon Sep 17 00:00:00 2001 From: Timo Date: Tue, 8 Apr 2025 18:17:15 +0200 Subject: [PATCH 5/9] fix indexing take two - use correct value for: `onEncryptionKeysChanged` - only update `latestGeneratedKeyIndex` for "this user" key --- src/matrixrtc/EncryptionManager.ts | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/matrixrtc/EncryptionManager.ts b/src/matrixrtc/EncryptionManager.ts index 821e99ef8e0..d6219435a53 100644 --- a/src/matrixrtc/EncryptionManager.ts +++ b/src/matrixrtc/EncryptionManager.ts @@ -80,6 +80,9 @@ export class EncryptionManager implements IEncryptionManager { // if it looks like a membership has been updated. private lastMembershipFingerprints: Set | undefined; + // TODO: remove this value since I think this is not helpful to use + // it represents what index we actually sent over to ElementCall (which we do in a delayed manner) + // but it has no releavnt information for the encryption manager. private mediaTrailerKeyIndexInUse = -1; private latestGeneratedKeyIndex = -1; private joinConfig: EncryptionConfig | undefined; @@ -267,6 +270,12 @@ export class EncryptionManager implements IEncryptionManager { } const keyIndexToSend = indexToSend ?? this.latestGeneratedKeyIndex; + // TODO remove this debug log. it just shows then when sending mediaTrailerKeyIndexInUse contained the wrong index. + logger.debug( + `Compare key in use to last generated key\n`, + `latestGeneratedKeyIndex: ${this.latestGeneratedKeyIndex}\n`, + `mediaTrailerKeyIndexInUse: ${this.mediaTrailerKeyIndexInUse}`, + ); logger.info( `Try sending encryption keys event. keyIndexToSend=${keyIndexToSend} (method parameter: ${indexToSend})`, ); @@ -359,7 +368,15 @@ export class EncryptionManager implements IEncryptionManager { } } - this.latestGeneratedKeyIndex = encryptionKeyIndex; + if (userId === this.userId && deviceId === this.deviceId) { + // It is important to already update the latestGeneratedKeyIndex here + // NOT IN THE `delayBeforeUse` `setTimeout`. + // Even though this is where we call onEncryptionKeysChanged and set the key in EC (and livekit). + // It needs to happen here because we will send the key before the timeout has passed and sending + // the key will use latestGeneratedKeyIndex as the index. if we update it in the `setTimeout` callback + // it will use the wrong index (index - 1)! + this.latestGeneratedKeyIndex = encryptionKeyIndex; + } participantKeys[encryptionKeyIndex] = { key: keyBin, timestamp, @@ -372,14 +389,14 @@ export class EncryptionManager implements IEncryptionManager { if (userId === this.userId && deviceId === this.deviceId) { this.mediaTrailerKeyIndexInUse = encryptionKeyIndex; } - this.onEncryptionKeysChanged(keyBin, this.mediaTrailerKeyIndexInUse, participantId); + this.onEncryptionKeysChanged(keyBin, encryptionKeyIndex, participantId); }, this.useKeyDelay); this.setNewKeyTimeouts.add(useKeyTimeout); } else { if (userId === this.userId && deviceId === this.deviceId) { this.mediaTrailerKeyIndexInUse = encryptionKeyIndex; } - this.onEncryptionKeysChanged(keyBin, this.mediaTrailerKeyIndexInUse, participantId); + this.onEncryptionKeysChanged(keyBin, encryptionKeyIndex, participantId); } } From 96382cad18caece4ae01fb608879ffc8f37b547c Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 9 Apr 2025 11:21:55 +0200 Subject: [PATCH 6/9] test: add test for join config `useExperimentalToDeviceTransport` --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 26 ++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 9f66bdee6e8..4a418644cb5 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -486,14 +486,17 @@ describe("MatrixRTCSession", () => { let sendStateEventMock: jest.Mock; let sendDelayedStateMock: jest.Mock; let sendEventMock: jest.Mock; + let sendToDeviceMock: jest.Mock; beforeEach(() => { sendStateEventMock = jest.fn(); sendDelayedStateMock = jest.fn(); sendEventMock = jest.fn(); + sendToDeviceMock = jest.fn(); client.sendStateEvent = sendStateEventMock; client._unstable_sendDelayedStateEvent = sendDelayedStateMock; client.sendEvent = sendEventMock; + client.encryptAndSendToDevice = sendToDeviceMock; mockRoom = makeMockRoom([]); sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); @@ -965,6 +968,29 @@ describe("MatrixRTCSession", () => { jest.useRealTimers(); } }); + + it("send key as to device", async () => { + jest.useFakeTimers(); + try { + const keySentPromise = new Promise((resolve) => { + sendToDeviceMock.mockImplementation(resolve); + }); + + const mockRoom = makeMockRoom([membershipTemplate]); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + + sess!.joinRoomSession([mockFocus], mockFocus, { + manageMediaKeys: true, + useExperimentalToDeviceTransport: true, + }); + + await keySentPromise; + + expect(sendToDeviceMock).toHaveBeenCalled(); + } finally { + jest.useRealTimers(); + } + }); }); describe("receiving", () => { From 5c01a2d1e1182c70ad20c44cb617f42b5a3a07a8 Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 9 Apr 2025 12:11:25 +0200 Subject: [PATCH 7/9] update test to fail without the fixed encryption key index --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 4a418644cb5..d70252567fa 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -835,6 +835,7 @@ describe("MatrixRTCSession", () => { it("rotates key if a member leaves", async () => { jest.useFakeTimers(); try { + const KEY_DALAY = 3000; const member2 = Object.assign({}, membershipTemplate, { device_id: "BBBBBBB", }); @@ -855,7 +856,8 @@ describe("MatrixRTCSession", () => { sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload)); }); - sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true }); + sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true, makeKeyDelay: KEY_DALAY }); + const sendKeySpy = jest.spyOn((sess as unknown as any).encryptionManager.transport, "sendKey"); const firstKeysPayload = await keysSentPromise1; expect(firstKeysPayload.keys).toHaveLength(1); expect(firstKeysPayload.keys[0].index).toEqual(0); @@ -872,14 +874,24 @@ describe("MatrixRTCSession", () => { .mockReturnValue(makeMockRoomState([membershipTemplate], mockRoom.roomId)); sess.onRTCSessionMemberUpdate(); - jest.advanceTimersByTime(10000); + jest.advanceTimersByTime(3000); + expect(sendKeySpy).toHaveBeenCalledTimes(1); + // check that we send the key with index 1 even though the send gets delayed when leaving. + // this makes sure we do not use an index that is one too old. + expect(sendKeySpy).toHaveBeenLastCalledWith(expect.any(String), 1, sess.memberships); + // fake a condition in which we send another encryption key event. + // this could happen do to someone joining the call. + (sess as unknown as any).encryptionManager.sendEncryptionKeysEvent(); + expect(sendKeySpy).toHaveBeenLastCalledWith(expect.any(String), 1, sess.memberships); + jest.advanceTimersByTime(7000); const secondKeysPayload = await keysSentPromise2; expect(secondKeysPayload.keys).toHaveLength(1); expect(secondKeysPayload.keys[0].index).toEqual(1); expect(onMyEncryptionKeyChanged).toHaveBeenCalledTimes(2); - expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(2); + // initial, on leave and the fake one we do with: `(sess as unknown as any).encryptionManager.sendEncryptionKeysEvent();` + expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(3); } finally { jest.useRealTimers(); } From 778db8c787ebb819d72d49f33ab051db3c39aab5 Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 9 Apr 2025 12:58:11 +0200 Subject: [PATCH 8/9] review --- spec/unit/embedded.spec.ts | 7 ++++--- src/client.ts | 8 ++++++++ src/embedded.ts | 26 +++----------------------- src/matrixrtc/EncryptionManager.ts | 4 ---- src/matrixrtc/IKeyTransport.ts | 8 ++++++++ src/matrixrtc/MatrixRTCSession.ts | 2 +- src/webrtc/call.ts | 2 +- 7 files changed, 25 insertions(+), 32 deletions(-) diff --git a/spec/unit/embedded.spec.ts b/spec/unit/embedded.spec.ts index 0b27541db31..f23fd75e9a1 100644 --- a/spec/unit/embedded.spec.ts +++ b/spec/unit/embedded.spec.ts @@ -722,13 +722,14 @@ describe("RoomWidgetClient", () => { expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, expectedRequestData); }); - it("sends encrypted (encryptAndSendToDevices)", async () => { + it("sends encrypted (encryptAndSendToDevice)", async () => { await makeClient({ sendToDevice: ["org.example.foo"] }); expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo"); - const payload = { type: "org.example.foo", hello: "world" }; + const payload = { hello: "world" }; const embeddedClient = client as RoomWidgetClient; - await embeddedClient.encryptAndSendToDevices( + await embeddedClient.encryptAndSendToDevice( + "org.example.foo", [ { userId: "@alice:example.org", deviceId: "aliceWeb" }, { userId: "@bob:example.org", deviceId: "bobDesktop" }, diff --git a/src/client.ts b/src/client.ts index b28594c303b..2bfbcd5db3e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -7942,6 +7942,14 @@ export class MatrixClient extends TypedEventEmitter { - // map: user Id → device Id → payload - const contentMap: MapWithDefault> = new MapWithDefault(() => new Map()); - for (const { userId, deviceId } of userDeviceInfoArr) { - contentMap.getOrCreate(userId).set(deviceId, payload); - } - - await this.widgetApi - .sendToDevice((payload as { type: string }).type, true, recursiveMapToObject(contentMap)) - .catch(timeoutToConnectionError); - } - /** * Send an event to a specific list of devices via the widget API. Optionally encrypts the event. * diff --git a/src/matrixrtc/EncryptionManager.ts b/src/matrixrtc/EncryptionManager.ts index d6219435a53..efe1f84b4d1 100644 --- a/src/matrixrtc/EncryptionManager.ts +++ b/src/matrixrtc/EncryptionManager.ts @@ -13,10 +13,6 @@ const logger = rootLogger.getChild("MatrixRTCSession"); * This interface is for testing and for making it possible to interchange the encryption manager. * @internal */ -/** - * Interface representing an encryption manager for handling encryption-related - * operations in a real-time communication context. - */ export interface IEncryptionManager { /** * Joins the encryption manager with the provided configuration. diff --git a/src/matrixrtc/IKeyTransport.ts b/src/matrixrtc/IKeyTransport.ts index 4548f746a0e..48a8fe9e5d9 100644 --- a/src/matrixrtc/IKeyTransport.ts +++ b/src/matrixrtc/IKeyTransport.ts @@ -45,9 +45,17 @@ export interface IKeyTransport { */ sendKey(keyBase64Encoded: string, index: number, members: CallMembership[]): Promise; + /** Subscribe to keys from this transport. */ on(event: KeyTransportEvents.ReceivedKeys, listener: KeyTransportEventListener): this; + /** Unsubscribe to keys from this transport. */ off(event: KeyTransportEvents.ReceivedKeys, listener: KeyTransportEventListener): this; + /** Once start is called the underlying transport will subscribe to its transport system. + * Before start is called this transport will not emit any events. + */ start(): void; + /** Once stop is called the underlying transport will unsubscribe from its transport system. + * After stop is called this transport will not emit any events. + */ stop(): void; } diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 18b704122e8..35d1e2e4076 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -380,7 +380,7 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Wed, 9 Apr 2025 15:51:59 +0200 Subject: [PATCH 9/9] review (dave) --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 6 +++--- .../matrixrtc/ToDeviceKeyTransport.spec.ts | 14 ++------------ src/client.ts | 4 ++-- src/matrixrtc/EncryptionManager.ts | 18 ++---------------- src/matrixrtc/IKeyTransport.ts | 2 +- src/matrixrtc/ToDeviceKeyTransport.ts | 14 +++++++------- 6 files changed, 17 insertions(+), 41 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index d70252567fa..7dee5ccaa46 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -835,7 +835,7 @@ describe("MatrixRTCSession", () => { it("rotates key if a member leaves", async () => { jest.useFakeTimers(); try { - const KEY_DALAY = 3000; + const KEY_DELAY = 3000; const member2 = Object.assign({}, membershipTemplate, { device_id: "BBBBBBB", }); @@ -856,7 +856,7 @@ describe("MatrixRTCSession", () => { sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload)); }); - sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true, makeKeyDelay: KEY_DALAY }); + sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true, makeKeyDelay: KEY_DELAY }); const sendKeySpy = jest.spyOn((sess as unknown as any).encryptionManager.transport, "sendKey"); const firstKeysPayload = await keysSentPromise1; expect(firstKeysPayload.keys).toHaveLength(1); @@ -874,7 +874,7 @@ describe("MatrixRTCSession", () => { .mockReturnValue(makeMockRoomState([membershipTemplate], mockRoom.roomId)); sess.onRTCSessionMemberUpdate(); - jest.advanceTimersByTime(3000); + jest.advanceTimersByTime(KEY_DELAY); expect(sendKeySpy).toHaveBeenCalledTimes(1); // check that we send the key with index 1 even though the send gets delayed when leaving. // this makes sure we do not use an index that is one too old. diff --git a/spec/unit/matrixrtc/ToDeviceKeyTransport.spec.ts b/spec/unit/matrixrtc/ToDeviceKeyTransport.spec.ts index dcf8ed82029..ae165152cb1 100644 --- a/spec/unit/matrixrtc/ToDeviceKeyTransport.spec.ts +++ b/spec/unit/matrixrtc/ToDeviceKeyTransport.spec.ts @@ -33,7 +33,7 @@ describe("ToDeviceKeyTransport", () => { let mockLogger: Mocked; let transport: ToDeviceKeyTransport; - function setup() { + beforeEach(() => { mockClient = getMockClientWithEventEmitter({ encryptAndSendToDevice: jest.fn(), }); @@ -54,11 +54,9 @@ describe("ToDeviceKeyTransport", () => { transport = new ToDeviceKeyTransport("@alice:example.org", "MYDEVICE", roomId, mockClient, statistics, { getChild: jest.fn().mockReturnValue(mockLogger), } as unknown as Mocked); - } + }); it("should send my keys on via to device", async () => { - setup(); - transport.start(); const keyBase64Encoded = "ABCDEDF"; @@ -110,8 +108,6 @@ describe("ToDeviceKeyTransport", () => { }); it("should emit when a key is received", async () => { - setup(); - const deferred = defer<{ userId: string; deviceId: string; keyBase64Encoded: string; index: number }>(); transport.on(KeyTransportEvents.ReceivedKeys, (userId, deviceId, keyBase64Encoded, index, timestamp) => { deferred.resolve({ userId, deviceId, keyBase64Encoded, index }); @@ -150,8 +146,6 @@ describe("ToDeviceKeyTransport", () => { }); it("should not sent to ourself", async () => { - setup(); - const keyBase64Encoded = "ABCDEDF"; const keyIndex = 2; await transport.sendKey(keyBase64Encoded, keyIndex, [ @@ -168,8 +162,6 @@ describe("ToDeviceKeyTransport", () => { }); it("should warn when there is a room mismatch", () => { - setup(); - transport.start(); const testEncoded = "ABCDEDF"; @@ -243,8 +235,6 @@ describe("ToDeviceKeyTransport", () => { ]; test.each(MALFORMED_EVENT)("should warn on malformed event %j", (event) => { - setup(); - transport.start(); mockClient.emit( diff --git a/src/client.ts b/src/client.ts index 2bfbcd5db3e..6974f35fa4e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -7946,9 +7946,9 @@ export class MatrixClient extends TypedEventEmitter | undefined; - // TODO: remove this value since I think this is not helpful to use - // it represents what index we actually sent over to ElementCall (which we do in a delayed manner) - // but it has no releavnt information for the encryption manager. - private mediaTrailerKeyIndexInUse = -1; private latestGeneratedKeyIndex = -1; private joinConfig: EncryptionConfig | undefined; @@ -266,12 +262,7 @@ export class EncryptionManager implements IEncryptionManager { } const keyIndexToSend = indexToSend ?? this.latestGeneratedKeyIndex; - // TODO remove this debug log. it just shows then when sending mediaTrailerKeyIndexInUse contained the wrong index. - logger.debug( - `Compare key in use to last generated key\n`, - `latestGeneratedKeyIndex: ${this.latestGeneratedKeyIndex}\n`, - `mediaTrailerKeyIndexInUse: ${this.mediaTrailerKeyIndexInUse}`, - ); + logger.info( `Try sending encryption keys event. keyIndexToSend=${keyIndexToSend} (method parameter: ${indexToSend})`, ); @@ -382,16 +373,11 @@ export class EncryptionManager implements IEncryptionManager { const useKeyTimeout = setTimeout(() => { this.setNewKeyTimeouts.delete(useKeyTimeout); logger.info(`Delayed-emitting key changed event for ${participantId} index ${encryptionKeyIndex}`); - if (userId === this.userId && deviceId === this.deviceId) { - this.mediaTrailerKeyIndexInUse = encryptionKeyIndex; - } + this.onEncryptionKeysChanged(keyBin, encryptionKeyIndex, participantId); }, this.useKeyDelay); this.setNewKeyTimeouts.add(useKeyTimeout); } else { - if (userId === this.userId && deviceId === this.deviceId) { - this.mediaTrailerKeyIndexInUse = encryptionKeyIndex; - } this.onEncryptionKeysChanged(keyBin, encryptionKeyIndex, participantId); } } diff --git a/src/matrixrtc/IKeyTransport.ts b/src/matrixrtc/IKeyTransport.ts index 48a8fe9e5d9..f577d94a611 100644 --- a/src/matrixrtc/IKeyTransport.ts +++ b/src/matrixrtc/IKeyTransport.ts @@ -47,7 +47,7 @@ export interface IKeyTransport { /** Subscribe to keys from this transport. */ on(event: KeyTransportEvents.ReceivedKeys, listener: KeyTransportEventListener): this; - /** Unsubscribe to keys from this transport. */ + /** Unsubscribe from keys from this transport. */ off(event: KeyTransportEvents.ReceivedKeys, listener: KeyTransportEventListener): this; /** Once start is called the underlying transport will subscribe to its transport system. diff --git a/src/matrixrtc/ToDeviceKeyTransport.ts b/src/matrixrtc/ToDeviceKeyTransport.ts index 39c234378e5..671d41d6cfa 100644 --- a/src/matrixrtc/ToDeviceKeyTransport.ts +++ b/src/matrixrtc/ToDeviceKeyTransport.ts @@ -23,6 +23,10 @@ import { ClientEvent, type MatrixClient } from "../client.ts"; import type { MatrixEvent } from "../models/event.ts"; import { EventType } from "../@types/event.ts"; +/** + * ToDeviceKeyTransport is used to send MatrixRTC keys to other devices using the + * to-device CS-API. + */ export class ToDeviceKeyTransport extends TypedEventEmitter implements IKeyTransport @@ -42,7 +46,7 @@ export class ToDeviceKeyTransport } public start(): void { - this.client.on(ClientEvent.ToDeviceEvent, (ev) => this.onToDeviceEvent(ev)); + this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); } public stop(): void { @@ -110,7 +114,7 @@ export class ToDeviceKeyTransport content.member.claimed_device_id!, content.keys.key, content.keys.index, - age, + now, ); } @@ -159,11 +163,7 @@ export class ToDeviceKeyTransport return; } - // TODO session is not used so far - // if (!content.session || !content.session.call_id || !content.session.scope || !content.session.application) { - // this.prefixedLogger.warn("Malformed Event: Missing/Malformed content.session", content.session); - // return; - // } + // TODO check for session related fields once the to-device encryption uses the new format. return content as EncryptionKeysToDeviceEventContent; } }