1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-09 10:22:46 +03:00

WIP on valere/matrix_rtc_key_transport

This commit is contained in:
Valere
2025-04-02 15:33:54 +02:00
parent 8ea291aada
commit ee6747f279
7 changed files with 243 additions and 6 deletions

View File

@@ -207,7 +207,7 @@ import {
import { M_BEACON_INFO, type MBeaconInfoEventContent } from "./@types/beacon.ts"; import { M_BEACON_INFO, type MBeaconInfoEventContent } from "./@types/beacon.ts";
import { NamespacedValue, UnstableValue } from "./NamespacedValue.ts"; import { NamespacedValue, UnstableValue } from "./NamespacedValue.ts";
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue.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 { IgnoredInvites } from "./models/invites-ignorer.ts";
import { type UIARequest } from "./@types/uia.ts"; import { type UIARequest } from "./@types/uia.ts";
import { type LocalNotificationSettings } from "./@types/local_notifications.ts"; import { type LocalNotificationSettings } from "./@types/local_notifications.ts";
@@ -7942,7 +7942,23 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.http.authedRequest(Method.Put, path, undefined, body); return this.http.authedRequest(Method.Put, path, undefined, body);
} }
/** public async encryptAndSendToDevice(
eventType: string,
devices: { userId: string; deviceId: string }[],
payload: ToDevicePayload,
): Promise<void> {
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 * Sends events directly to specific devices using Matrix's to-device
* messaging system. The batch will be split up into appropriately sized * messaging system. The batch will be split up into appropriately sized
* batches for sending and stored in the store so they can be retried * batches for sending and stored in the store so they can be retried

View File

@@ -464,6 +464,22 @@ export class RoomWidgetClient extends MatrixClient {
return {}; return {};
} }
public async encryptAndSendToDevice(
eventType: string,
devices: { userId: string; deviceId: string }[],
payload: ToDevicePayload,
): Promise<void> {
// map: user Id → device Id → payload
const contentMap: MapWithDefault<string, Map<string, ToDevicePayload>> = 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<EmptyObject> { public async sendToDevice(eventType: string, contentMap: SendToDeviceContentMap): Promise<EmptyObject> {
await this.widgetApi await this.widgetApi
.sendToDevice(eventType, false, recursiveMapToObject(contentMap)) .sendToDevice(eventType, false, recursiveMapToObject(contentMap))

View File

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

View File

@@ -332,6 +332,7 @@ export class EncryptionManager implements IEncryptionManager {
timestamp: number, timestamp: number,
delayBeforeUse = false, delayBeforeUse = false,
): void { ): void {
logger.debug(`Setting encryption key for ${userId}:${deviceId} at index ${encryptionKeyIndex}`);
const keyBin = decodeBase64(encryptionKeyString); const keyBin = decodeBase64(encryptionKeyString);
const participantId = getParticipantId(userId, deviceId); const participantId = getParticipantId(userId, deviceId);

View File

@@ -28,9 +28,10 @@ import { MembershipManager } from "./NewMembershipManager.ts";
import { EncryptionManager, type IEncryptionManager } from "./EncryptionManager.ts"; import { EncryptionManager, type IEncryptionManager } from "./EncryptionManager.ts";
import { LegacyMembershipManager } from "./LegacyMembershipManager.ts"; import { LegacyMembershipManager } from "./LegacyMembershipManager.ts";
import { logDurationSync } from "../utils.ts"; import { logDurationSync } from "../utils.ts";
import { RoomKeyTransport } from "./RoomKeyTransport.ts"; import { ToDeviceKeyTransport } from "./ToDeviceKeyTransport.ts";
import { type IMembershipManager } from "./IMembershipManager.ts";
import { type Statistics } from "./types.ts"; import { type Statistics } from "./types.ts";
import { RoomKeyTransport } from "./RoomKeyTransport.ts";
import { IMembershipManager } from "./IMembershipManager.ts";
const logger = rootLogger.getChild("MatrixRTCSession"); 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. * 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; maximumNetworkErrorRetryCount?: number;
/**
* If true, use the new to-device transport for sending encryption keys.
*/
useExperimentalToDeviceTransport?: boolean;
} }
export interface EncryptionConfig { export interface EncryptionConfig {
@@ -303,6 +309,9 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
| "_unstable_updateDelayedEvent" | "_unstable_updateDelayedEvent"
| "sendEvent" | "sendEvent"
| "cancelPendingEvent" | "cancelPendingEvent"
| "encryptAndSendToDevice"
| "off"
| "on"
| "decryptEventIfNeeded" | "decryptEventIfNeeded"
>, >,
private roomSubset: Pick< private roomSubset: Pick<
@@ -370,7 +379,19 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
); );
} }
// Create Encryption manager // Create Encryption manager
const transport = new RoomKeyTransport(this.roomSubset, this.client, this.statistics); let transport;
if (joinConfig?.useExperimentalToDeviceTransport == true) {
logger.info("Using experimental to-device transport for encryption keys");
transport = new ToDeviceKeyTransport(
this.client.getUserId()!,
this.client.getDeviceId()!,
this.roomSubset.roomId,
this.client,
this.statistics,
);
} else {
transport = new RoomKeyTransport(this.roomSubset, this.client, this.statistics);
}
this.encryptionManager = new EncryptionManager( this.encryptionManager = new EncryptionManager(
this.client.getUserId()!, this.client.getUserId()!,
this.client.getDeviceId()!, this.client.getDeviceId()!,

View File

@@ -0,0 +1,165 @@
/*
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 { ClientEvent, EventType, type MatrixClient, MatrixEvent } from "../matrix.ts";
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
import { IKeyTransport, KeyTransportEvents, KeyTransportEventsHandlerMap } from "./IKeyTransport.ts";
import { type Logger, logger } from "../logger.ts";
import { CallMembership } from "./CallMembership.ts";
import { EncryptionKeysToDeviceEventContent, Statistics } from "./types.ts";
export class ToDeviceKeyTransport
extends TypedEventEmitter<KeyTransportEvents, KeyTransportEventsHandlerMap>
implements IKeyTransport
{
private readonly prefixedLogger: Logger;
public constructor(
private userId: string,
private deviceId: string,
private roomId: string,
private client: Pick<MatrixClient, "encryptAndSendToDevice" | "on" | "off">,
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<void> {
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<EncryptionKeysToDeviceEventContent>();
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;
}
}

View File

@@ -28,6 +28,24 @@ export interface EncryptionKeysEventContent {
sent_ts?: number; 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 type CallNotifyType = "ring" | "notify";
export interface ICallNotifyContent { export interface ICallNotifyContent {