Skip to content

Commit d3733d7

Browse files
committed
WIP on valere/matrix_rtc_key_transport
1 parent 3a9d03c commit d3733d7

File tree

6 files changed

+210
-6
lines changed

6 files changed

+210
-6
lines changed

src/client.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ import {
207207
import { M_BEACON_INFO, type MBeaconInfoEventContent } from "./@types/beacon.ts";
208208
import { NamespacedValue, UnstableValue } from "./NamespacedValue.ts";
209209
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue.ts";
210-
import { type ToDeviceBatch } from "./models/ToDeviceMessage.ts";
210+
import {type ToDeviceBatch, ToDevicePayload} from "./models/ToDeviceMessage.ts";
211211
import { IgnoredInvites } from "./models/invites-ignorer.ts";
212212
import { type UIARequest } from "./@types/uia.ts";
213213
import { type LocalNotificationSettings } from "./@types/local_notifications.ts";
@@ -7942,7 +7942,23 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
79427942
return this.http.authedRequest(Method.Put, path, undefined, body);
79437943
}
79447944

7945-
/**
7945+
public async encryptAndSendToDevice(
7946+
eventType: string,
7947+
devices: { userId: string; deviceId: string }[],
7948+
payload: ToDevicePayload,
7949+
): Promise<void> {
7950+
if (!this.cryptoBackend) {
7951+
throw new Error("Cannot encrypt to device event, your client does not support encryption.");
7952+
}
7953+
const batch = await this.cryptoBackend.encryptToDeviceMessages(eventType, devices, payload);
7954+
7955+
// TODO The batch mechanism removes all possibility to get error feedbacks..
7956+
// We might want instead to do the API call directly and pass the errors back.
7957+
await this.queueToDevice(batch);
7958+
}
7959+
7960+
7961+
/**
79467962
* Sends events directly to specific devices using Matrix's to-device
79477963
* messaging system. The batch will be split up into appropriately sized
79487964
* batches for sending and stored in the store so they can be retried

src/embedded.ts

+16
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,22 @@ export class RoomWidgetClient extends MatrixClient {
464464
return {};
465465
}
466466

467+
public async encryptAndSendToDevice(
468+
eventType: string,
469+
devices: { userId: string; deviceId: string }[],
470+
payload: ToDevicePayload,
471+
): Promise<void> {
472+
// map: user Id → device Id → payload
473+
const contentMap: MapWithDefault<string, Map<string, ToDevicePayload>> = new MapWithDefault(() => new Map());
474+
for (const { userId, deviceId } of devices) {
475+
contentMap.getOrCreate(userId).set(deviceId, payload);
476+
}
477+
478+
await this.widgetApi
479+
.sendToDevice(eventType, true, recursiveMapToObject(contentMap))
480+
.catch(timeoutToConnectionError);
481+
}
482+
467483
public async sendToDevice(eventType: string, contentMap: SendToDeviceContentMap): Promise<EmptyObject> {
468484
await this.widgetApi
469485
.sendToDevice(eventType, false, recursiveMapToObject(contentMap))

src/http-api/fetch.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
218218
* On success, sets new access and refresh tokens in opts.
219219
* @returns Promise that resolves to a boolean - true when token was refreshed successfully
220220
*/
221-
@singleAsyncExecution
221+
// @singleAsyncExecution
222222
private async tryRefreshToken(): Promise<TokenRefreshOutcome> {
223223
if (!this.opts.refreshToken || !this.opts.tokenRefreshFunction) {
224224
return TokenRefreshOutcome.Logout;

src/matrixrtc/EncryptionManager.ts

+1
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ export class EncryptionManager implements IEncryptionManager {
339339
timestamp: number,
340340
delayBeforeUse = false,
341341
): void {
342+
logger.debug(`Setting encryption key for ${userId}:${deviceId} at index ${encryptionKeyIndex}`);
342343
const keyBin = decodeBase64(encryptionKeyString);
343344

344345
const participantId = getParticipantId(userId, deviceId);

src/matrixrtc/MatrixRTCSession.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { EncryptionManager, type IEncryptionManager, type Statistics } from "./E
2929
import { LegacyMembershipManager } from "./LegacyMembershipManager.ts";
3030
import { logDurationSync } from "../utils.ts";
3131
import type { IMembershipManager } from "./types.ts";
32-
import { RoomKeyTransport } from "./RoomKeyTransport.ts";
32+
import { ToDeviceKeyTransport } from "./ToDeviceKeyTransport.ts";
3333

3434
const logger = rootLogger.getChild("MatrixRTCSession");
3535

@@ -296,7 +296,9 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
296296
| "_unstable_updateDelayedEvent"
297297
| "sendEvent"
298298
| "cancelPendingEvent"
299-
| "decryptEventIfNeeded"
299+
| "encryptAndSendToDevice"
300+
| "off"
301+
| "on"
300302
>,
301303
private roomSubset: Pick<
302304
Room,
@@ -311,7 +313,14 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
311313
roomState?.on(RoomStateEvent.Members, this.onRoomMemberUpdate);
312314
this.setExpiryTimer();
313315

314-
const transport = new RoomKeyTransport(this.roomSubset, this.client, this.statistics);
316+
// const transport = new RoomKeyTransport(this.roomSubset.roomId, this.client);
317+
const transport = new ToDeviceKeyTransport(
318+
this.client.getUserId()!,
319+
this.client.getDeviceId()!,
320+
this.roomSubset.roomId,
321+
this.client,
322+
);
323+
// const transport = new RoomKeyTransport(this.roomSubset, this.client, this.statistics);
315324
this.encryptionManager = new EncryptionManager(
316325
this.client.getUserId()!,
317326
this.client.getDeviceId()!,
@@ -544,4 +553,5 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
544553

545554
this.setExpiryTimer();
546555
};
556+
547557
}

src/matrixrtc/ToDeviceKeyTransport.ts

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
Copyright 2025 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import {ClientEvent, EventType, type MatrixClient, MatrixEvent } from "../matrix.ts";
18+
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
19+
import {Statistics} from "./EncryptionManager.ts";
20+
import {IKeyTransport, KeyTransportEvents, KeyTransportEventsHandlerMap} from "./IKeyTransport.ts";
21+
import {type Logger, logger} from "../logger.ts";
22+
import {CallMembership} from "./CallMembership.ts";
23+
24+
export type ParticipantId = string
25+
26+
export interface EncryptionKeysToDeviceEventContent {
27+
keys: { index: number; key: string };
28+
// member: {
29+
// id: ParticipantId,
30+
// device_id: string,
31+
// user_id: string
32+
// };
33+
roomId: string;
34+
session: {
35+
application: string,
36+
call_id: string,
37+
scope: string
38+
},
39+
}
40+
41+
export class ToDeviceKeyTransport extends TypedEventEmitter<KeyTransportEvents, KeyTransportEventsHandlerMap>
42+
implements IKeyTransport {
43+
private readonly prefixedLogger: Logger;
44+
45+
public constructor(
46+
private userId: string,
47+
private deviceId: string,
48+
private roomId: string,
49+
private client: Pick<MatrixClient, "encryptAndSendToDevice" | "on" | "off">,
50+
// private statistics: Statistics,
51+
) {
52+
super();
53+
this.prefixedLogger = logger.getChild(`[RTC: ${roomId} ToDeviceKeyTransport]`);
54+
}
55+
56+
start(): void {
57+
this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
58+
}
59+
60+
stop(): void {
61+
this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
62+
}
63+
64+
public async sendKey(keyBase64Encoded: string, index: number, members: CallMembership[]): Promise<void> {
65+
const content: EncryptionKeysToDeviceEventContent = {
66+
keys: {
67+
index: index,
68+
key: keyBase64Encoded,
69+
},
70+
roomId: this.roomId,
71+
// member: {
72+
// id: getParticipantId(this.userId, this.deviceId),
73+
// // device_id: this.deviceId,
74+
// // user_id: this.userId
75+
// },
76+
session: {
77+
call_id: "",
78+
application: "m.call",
79+
scope: "m.room",
80+
},
81+
};
82+
83+
const targets = members.filter(member => {
84+
// filter malformed call members
85+
if (member.sender == undefined || member.deviceId == undefined) {
86+
logger.warn(`Malformed call member: ${member.sender}|${member.deviceId}`);
87+
return false;
88+
}
89+
// Filter out me
90+
return !(member.sender == this.userId && member.deviceId == this.deviceId);
91+
})
92+
.map(member => {
93+
return {
94+
userId: member.sender!,
95+
deviceId: member.deviceId!,
96+
}
97+
});
98+
99+
if (targets.length > 0) {
100+
await this.client.encryptAndSendToDevice(EventType.CallEncryptionKeysPrefix, targets, content);
101+
} else {
102+
this.prefixedLogger.warn("No targets found for sending key");
103+
}
104+
}
105+
106+
receiveRoomEvent(event: MatrixEvent /**, statistics: Statistics*/): void {
107+
// The event has already been validated at this point.
108+
const content = event.getContent<EncryptionKeysToDeviceEventContent>();
109+
110+
// statistics.counters.roomEventEncryptionKeysReceived += 1;
111+
112+
// WTF is this?
113+
// const age = Date.now() - (typeof content.sent_ts === "number" ? content.sent_ts : event.getTs());
114+
// statistics.totals.roomEventEncryptionKeysReceivedTotalAge += age;
115+
116+
this.emit(
117+
KeyTransportEvents.ReceivedKeys,
118+
event.getSender()!,
119+
// TODO: Check that this is correctly passed from the to device and is not claimed info
120+
(event.event as any)["sender_device"]!,
121+
content.keys.key,
122+
content.keys.index,
123+
event.getTs(),
124+
)
125+
}
126+
127+
128+
private onToDeviceEvent = (event: MatrixEvent): void => {
129+
if (event.getType() !== EventType.CallEncryptionKeysPrefix) {
130+
// Ignore this is not a call encryption event
131+
return;
132+
}
133+
134+
// if (event.getWireType() != EventType.RoomMessageEncrypted) {
135+
// // WARN: The call keys were sent in clear. Ignore them
136+
// logger.warn(`Call encryption keys sent in clear from: ${event.getSender()}`);
137+
// return;
138+
// }
139+
140+
const content = event.getContent<EncryptionKeysToDeviceEventContent>();
141+
const roomId = content.roomId;
142+
if (!roomId) {
143+
// Invalid event
144+
logger.warn("onToDeviceEvent: invalid call encryption keys event, no roomId");
145+
return;
146+
}
147+
148+
if (roomId !== this.roomId) {
149+
return;
150+
}
151+
// const rtcSession = this.roomSessions.get(roomId);
152+
// if (!rtcSession) {
153+
// logger.warn(`onToDeviceEvent: call encryption keys event for unknown session for room ${roomId}`);
154+
// return;
155+
// }
156+
157+
this.receiveRoomEvent(event
158+
// , this.statistics
159+
);
160+
};
161+
}

0 commit comments

Comments
 (0)