From ee6747f27988c58dead89e9b95eb3457b709a752 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 2 Apr 2025 15:33:54 +0200 Subject: [PATCH] WIP on valere/matrix_rtc_key_transport --- src/client.ts | 20 +++- src/embedded.ts | 16 +++ src/http-api/fetch.ts | 2 +- src/matrixrtc/EncryptionManager.ts | 1 + src/matrixrtc/MatrixRTCSession.ts | 27 ++++- src/matrixrtc/ToDeviceKeyTransport.ts | 165 ++++++++++++++++++++++++++ src/matrixrtc/types.ts | 18 +++ 7 files changed, 243 insertions(+), 6 deletions(-) create mode 100644 src/matrixrtc/ToDeviceKeyTransport.ts diff --git a/src/client.ts b/src/client.ts index 475ee77f0..08a243d02 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, 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,7 +7942,23 @@ 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 * batches for sending and stored in the store so they can be retried diff --git a/src/embedded.ts b/src/embedded.ts index 0882872e5..3bd36ca77 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/http-api/fetch.ts b/src/http-api/fetch.ts index 9a54b7360..7e9988429 100644 --- a/src/http-api/fetch.ts +++ b/src/http-api/fetch.ts @@ -218,7 +218,7 @@ export class FetchHttpApi { * On success, sets new access and refresh tokens in opts. * @returns Promise that resolves to a boolean - true when token was refreshed successfully */ - @singleAsyncExecution + // @singleAsyncExecution private async tryRefreshToken(): Promise { if (!this.opts.refreshToken || !this.opts.tokenRefreshFunction) { return TokenRefreshOutcome.Logout; diff --git a/src/matrixrtc/EncryptionManager.ts b/src/matrixrtc/EncryptionManager.ts index b97cd9dd3..b65423484 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 0c8cb6ee6..6772a6892 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 { 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]`); + } + + start(): void { + this.client.on(ClientEvent.ToDeviceEvent, (ev) => this.onToDeviceEvent(ev)); + } + + 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"); + } + } + + 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 || !content.keys.index) { + 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 ee8c654bb..9d4fe4ea6 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 {