You've already forked matrix-js-sdk
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:
@@ -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
|
||||||
|
@@ -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))
|
||||||
|
@@ -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;
|
||||||
|
@@ -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);
|
||||||
|
@@ -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()!,
|
||||||
|
165
src/matrixrtc/ToDeviceKeyTransport.ts
Normal file
165
src/matrixrtc/ToDeviceKeyTransport.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@@ -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 {
|
||||||
|
Reference in New Issue
Block a user