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
refactor: extract RoomKeyTransport class for key distribution
This commit is contained in:
@@ -584,7 +584,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||||
jest.advanceTimersByTime(10000);
|
await jest.runAllTimersAsync();
|
||||||
|
|
||||||
await eventSentPromise;
|
await eventSentPromise;
|
||||||
|
|
||||||
|
@@ -1,14 +1,12 @@
|
|||||||
import { type MatrixClient } from "../client.ts";
|
|
||||||
import { logger as rootLogger } from "../logger.ts";
|
import { logger as rootLogger } from "../logger.ts";
|
||||||
import { type MatrixEvent } from "../models/event.ts";
|
import { type MatrixEvent } from "../models/event.ts";
|
||||||
import { type Room } from "../models/room.ts";
|
|
||||||
import { type EncryptionConfig } from "./MatrixRTCSession.ts";
|
import { type EncryptionConfig } from "./MatrixRTCSession.ts";
|
||||||
import { secureRandomBase64Url } from "../randomstring.ts";
|
import { secureRandomBase64Url } from "../randomstring.ts";
|
||||||
import { type EncryptionKeysEventContent } from "./types.ts";
|
|
||||||
import { decodeBase64, encodeUnpaddedBase64 } from "../base64.ts";
|
import { decodeBase64, encodeUnpaddedBase64 } from "../base64.ts";
|
||||||
import { type MatrixError, safeGetRetryAfterMs } from "../http-api/errors.ts";
|
import { safeGetRetryAfterMs } from "../http-api/errors.ts";
|
||||||
import { type CallMembership } from "./CallMembership.ts";
|
import { type CallMembership } from "./CallMembership.ts";
|
||||||
import { EventType } from "../@types/event.ts";
|
import { type IKeyTransport } from "./IKeyTransport.ts";
|
||||||
|
|
||||||
const logger = rootLogger.getChild("MatrixRTCSession");
|
const logger = rootLogger.getChild("MatrixRTCSession");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,8 +38,11 @@ export type Statistics = {
|
|||||||
*/
|
*/
|
||||||
export interface IEncryptionManager {
|
export interface IEncryptionManager {
|
||||||
join(joinConfig: EncryptionConfig | undefined): void;
|
join(joinConfig: EncryptionConfig | undefined): void;
|
||||||
|
|
||||||
leave(): void;
|
leave(): void;
|
||||||
|
|
||||||
onMembershipsUpdate(oldMemberships: CallMembership[]): void;
|
onMembershipsUpdate(oldMemberships: CallMembership[]): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process `m.call.encryption_keys` events to track the encryption keys for call participants.
|
* Process `m.call.encryption_keys` events to track the encryption keys for call participants.
|
||||||
* This should be called each time the relevant event is received from a room timeline.
|
* This should be called each time the relevant event is received from a room timeline.
|
||||||
@@ -50,7 +51,9 @@ export interface IEncryptionManager {
|
|||||||
* @param event the event to process
|
* @param event the event to process
|
||||||
*/
|
*/
|
||||||
onCallEncryptionEventReceived(event: MatrixEvent): void;
|
onCallEncryptionEventReceived(event: MatrixEvent): void;
|
||||||
|
|
||||||
getEncryptionKeys(): Map<string, Array<{ key: Uint8Array; timestamp: number }>>;
|
getEncryptionKeys(): Map<string, Array<{ key: Uint8Array; timestamp: number }>>;
|
||||||
|
|
||||||
statistics: Statistics;
|
statistics: Statistics;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,9 +74,11 @@ export class EncryptionManager implements IEncryptionManager {
|
|||||||
private get updateEncryptionKeyThrottle(): number {
|
private get updateEncryptionKeyThrottle(): number {
|
||||||
return this.joinConfig?.updateEncryptionKeyThrottle ?? 3_000;
|
return this.joinConfig?.updateEncryptionKeyThrottle ?? 3_000;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get makeKeyDelay(): number {
|
private get makeKeyDelay(): number {
|
||||||
return this.joinConfig?.makeKeyDelay ?? 3_000;
|
return this.joinConfig?.makeKeyDelay ?? 3_000;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get useKeyDelay(): number {
|
private get useKeyDelay(): number {
|
||||||
return this.joinConfig?.useKeyDelay ?? 5_000;
|
return this.joinConfig?.useKeyDelay ?? 5_000;
|
||||||
}
|
}
|
||||||
@@ -99,9 +104,10 @@ export class EncryptionManager implements IEncryptionManager {
|
|||||||
private joinConfig: EncryptionConfig | undefined;
|
private joinConfig: EncryptionConfig | undefined;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private client: Pick<MatrixClient, "sendEvent" | "getDeviceId" | "getUserId" | "cancelPendingEvent">,
|
private userId: string,
|
||||||
private room: Pick<Room, "roomId">,
|
private deviceId: string,
|
||||||
private getMemberships: () => CallMembership[],
|
private getMemberships: () => CallMembership[],
|
||||||
|
private transport: IKeyTransport,
|
||||||
private onEncryptionKeysChanged: (
|
private onEncryptionKeysChanged: (
|
||||||
keyBin: Uint8Array<ArrayBufferLike>,
|
keyBin: Uint8Array<ArrayBufferLike>,
|
||||||
encryptionKeyIndex: number,
|
encryptionKeyIndex: number,
|
||||||
@@ -112,7 +118,9 @@ export class EncryptionManager implements IEncryptionManager {
|
|||||||
public getEncryptionKeys(): Map<string, Array<{ key: Uint8Array; timestamp: number }>> {
|
public getEncryptionKeys(): Map<string, Array<{ key: Uint8Array; timestamp: number }>> {
|
||||||
return this.encryptionKeys;
|
return this.encryptionKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
private joined = false;
|
private joined = false;
|
||||||
|
|
||||||
public join(joinConfig: EncryptionConfig): void {
|
public join(joinConfig: EncryptionConfig): void {
|
||||||
this.joinConfig = joinConfig;
|
this.joinConfig = joinConfig;
|
||||||
this.joined = true;
|
this.joined = true;
|
||||||
@@ -124,15 +132,10 @@ export class EncryptionManager implements IEncryptionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public leave(): void {
|
public leave(): void {
|
||||||
const userId = this.client.getUserId();
|
|
||||||
const deviceId = this.client.getDeviceId();
|
|
||||||
|
|
||||||
if (!userId) throw new Error("No userId");
|
|
||||||
if (!deviceId) throw new Error("No deviceId");
|
|
||||||
// clear our encryption keys as we're done with them now (we'll
|
// clear our encryption keys as we're done with them now (we'll
|
||||||
// make new keys if we rejoin). We leave keys for other participants
|
// make new keys if we rejoin). We leave keys for other participants
|
||||||
// as they may still be using the same ones.
|
// as they may still be using the same ones.
|
||||||
this.encryptionKeys.set(getParticipantId(userId, deviceId), []);
|
this.encryptionKeys.set(getParticipantId(this.userId, this.deviceId), []);
|
||||||
|
|
||||||
if (this.makeNewKeyTimeout !== undefined) {
|
if (this.makeNewKeyTimeout !== undefined) {
|
||||||
clearTimeout(this.makeNewKeyTimeout);
|
clearTimeout(this.makeNewKeyTimeout);
|
||||||
@@ -146,9 +149,9 @@ export class EncryptionManager implements IEncryptionManager {
|
|||||||
this.manageMediaKeys = false;
|
this.manageMediaKeys = false;
|
||||||
this.joined = false;
|
this.joined = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO deduplicate this method. It also is in MatrixRTCSession.
|
// TODO deduplicate this method. It also is in MatrixRTCSession.
|
||||||
private isMyMembership = (m: CallMembership): boolean =>
|
private isMyMembership = (m: CallMembership): boolean => m.sender === this.userId && m.deviceId === this.deviceId;
|
||||||
m.sender === this.client.getUserId() && m.deviceId === this.client.getDeviceId();
|
|
||||||
|
|
||||||
public onMembershipsUpdate(oldMemberships: CallMembership[]): void {
|
public onMembershipsUpdate(oldMemberships: CallMembership[]): void {
|
||||||
if (this.manageMediaKeys && this.joined) {
|
if (this.manageMediaKeys && this.joined) {
|
||||||
@@ -204,16 +207,17 @@ export class EncryptionManager implements IEncryptionManager {
|
|||||||
* @returns The index of the new key
|
* @returns The index of the new key
|
||||||
*/
|
*/
|
||||||
private makeNewSenderKey(delayBeforeUse = false): number {
|
private makeNewSenderKey(delayBeforeUse = false): number {
|
||||||
const userId = this.client.getUserId();
|
|
||||||
const deviceId = this.client.getDeviceId();
|
|
||||||
|
|
||||||
if (!userId) throw new Error("No userId");
|
|
||||||
if (!deviceId) throw new Error("No deviceId");
|
|
||||||
|
|
||||||
const encryptionKey = secureRandomBase64Url(16);
|
const encryptionKey = secureRandomBase64Url(16);
|
||||||
const encryptionKeyIndex = this.getNewEncryptionKeyIndex();
|
const encryptionKeyIndex = this.getNewEncryptionKeyIndex();
|
||||||
logger.info("Generated new key at index " + encryptionKeyIndex);
|
logger.info("Generated new key at index " + encryptionKeyIndex);
|
||||||
this.setEncryptionKey(userId, deviceId, encryptionKeyIndex, encryptionKey, Date.now(), delayBeforeUse);
|
this.setEncryptionKey(
|
||||||
|
this.userId,
|
||||||
|
this.deviceId,
|
||||||
|
encryptionKeyIndex,
|
||||||
|
encryptionKey,
|
||||||
|
Date.now(),
|
||||||
|
delayBeforeUse,
|
||||||
|
);
|
||||||
return encryptionKeyIndex;
|
return encryptionKeyIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,13 +270,7 @@ export class EncryptionManager implements IEncryptionManager {
|
|||||||
|
|
||||||
logger.info(`Sending encryption keys event. indexToSend=${indexToSend}`);
|
logger.info(`Sending encryption keys event. indexToSend=${indexToSend}`);
|
||||||
|
|
||||||
const userId = this.client.getUserId();
|
const myKeys = this.getKeysForParticipant(this.userId, this.deviceId);
|
||||||
const deviceId = this.client.getDeviceId();
|
|
||||||
|
|
||||||
if (!userId) throw new Error("No userId");
|
|
||||||
if (!deviceId) throw new Error("No deviceId");
|
|
||||||
|
|
||||||
const myKeys = this.getKeysForParticipant(userId, deviceId);
|
|
||||||
|
|
||||||
if (!myKeys) {
|
if (!myKeys) {
|
||||||
logger.warn("Tried to send encryption keys event but no keys found!");
|
logger.warn("Tried to send encryption keys event but no keys found!");
|
||||||
@@ -288,35 +286,15 @@ export class EncryptionManager implements IEncryptionManager {
|
|||||||
const keyToSend = myKeys[keyIndexToSend];
|
const keyToSend = myKeys[keyIndexToSend];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content: EncryptionKeysEventContent = {
|
|
||||||
keys: [
|
|
||||||
{
|
|
||||||
index: keyIndexToSend,
|
|
||||||
key: encodeUnpaddedBase64(keyToSend),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
device_id: deviceId,
|
|
||||||
call_id: "",
|
|
||||||
sent_ts: Date.now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.statistics.counters.roomEventEncryptionKeysSent += 1;
|
this.statistics.counters.roomEventEncryptionKeysSent += 1;
|
||||||
|
await this.transport.sendKey(encodeUnpaddedBase64(keyToSend), keyIndexToSend);
|
||||||
await this.client.sendEvent(this.room.roomId, EventType.CallEncryptionKeysPrefix, content);
|
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Embedded-E2EE-LOG updateEncryptionKeyEvent participantId=${userId}:${deviceId} numKeys=${myKeys.length} currentKeyIndex=${this.currentEncryptionKeyIndex} keyIndexToSend=${keyIndexToSend}`,
|
`Embedded-E2EE-LOG updateEncryptionKeyEvent participantId=${this.userId}:${this.deviceId} numKeys=${myKeys.length} currentKeyIndex=${this.currentEncryptionKeyIndex} keyIndexToSend=${keyIndexToSend}`,
|
||||||
this.encryptionKeys,
|
this.encryptionKeys,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const matrixError = error as MatrixError;
|
|
||||||
if (matrixError.event) {
|
|
||||||
// cancel the pending event: we'll just generate a new one with our latest
|
|
||||||
// keys when we resend
|
|
||||||
this.client.cancelPendingEvent(matrixError.event);
|
|
||||||
}
|
|
||||||
if (this.keysEventUpdateTimeout === undefined) {
|
if (this.keysEventUpdateTimeout === undefined) {
|
||||||
const resendDelay = safeGetRetryAfterMs(matrixError, 5000);
|
const resendDelay = safeGetRetryAfterMs(error, 5000);
|
||||||
logger.warn(`Failed to send m.call.encryption_key, retrying in ${resendDelay}`, error);
|
logger.warn(`Failed to send m.call.encryption_key, retrying in ${resendDelay}`, error);
|
||||||
this.keysEventUpdateTimeout = setTimeout(() => void this.sendEncryptionKeysEvent(), resendDelay);
|
this.keysEventUpdateTimeout = setTimeout(() => void this.sendEncryptionKeysEvent(), resendDelay);
|
||||||
} else {
|
} else {
|
||||||
@@ -326,74 +304,15 @@ export class EncryptionManager implements IEncryptionManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public onCallEncryptionEventReceived = (event: MatrixEvent): void => {
|
public onCallEncryptionEventReceived = (event: MatrixEvent): void => {
|
||||||
const userId = event.getSender();
|
this.transport.receiveRoomEvent(
|
||||||
const content = event.getContent<EncryptionKeysEventContent>();
|
event,
|
||||||
|
this.statistics,
|
||||||
const deviceId = content["device_id"];
|
(userId, deviceId, encryptionKeyIndex, encryptionKeyString, timestamp) => {
|
||||||
const callId = content["call_id"];
|
this.setEncryptionKey(userId, deviceId, encryptionKeyIndex, encryptionKeyString, timestamp);
|
||||||
|
},
|
||||||
if (!userId) {
|
);
|
||||||
logger.warn(`Received m.call.encryption_keys with no userId: callId=${callId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We currently only handle callId = "" (which is the default for room scoped calls)
|
|
||||||
if (callId !== "") {
|
|
||||||
logger.warn(
|
|
||||||
`Received m.call.encryption_keys with unsupported callId: userId=${userId}, deviceId=${deviceId}, callId=${callId}`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(content.keys)) {
|
|
||||||
logger.warn(`Received m.call.encryption_keys where keys wasn't an array: callId=${callId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userId === this.client.getUserId() && deviceId === this.client.getDeviceId()) {
|
|
||||||
// We store our own sender key in the same set along with keys from others, so it's
|
|
||||||
// important we don't allow our own keys to be set by one of these events (apart from
|
|
||||||
// the fact that we don't need it anyway because we already know our own keys).
|
|
||||||
logger.info("Ignoring our own keys event");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.statistics.counters.roomEventEncryptionKeysReceived += 1;
|
|
||||||
const age = Date.now() - (typeof content.sent_ts === "number" ? content.sent_ts : event.getTs());
|
|
||||||
this.statistics.totals.roomEventEncryptionKeysReceivedTotalAge += age;
|
|
||||||
|
|
||||||
for (const key of content.keys) {
|
|
||||||
if (!key) {
|
|
||||||
logger.info("Ignoring false-y key in keys event");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const encryptionKey = key.key;
|
|
||||||
const encryptionKeyIndex = key.index;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!encryptionKey ||
|
|
||||||
encryptionKeyIndex === undefined ||
|
|
||||||
encryptionKeyIndex === null ||
|
|
||||||
callId === undefined ||
|
|
||||||
callId === null ||
|
|
||||||
typeof deviceId !== "string" ||
|
|
||||||
typeof callId !== "string" ||
|
|
||||||
typeof encryptionKey !== "string" ||
|
|
||||||
typeof encryptionKeyIndex !== "number"
|
|
||||||
) {
|
|
||||||
logger.warn(
|
|
||||||
`Malformed call encryption_key: userId=${userId}, deviceId=${deviceId}, encryptionKeyIndex=${encryptionKeyIndex} callId=${callId}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.debug(
|
|
||||||
`Embedded-E2EE-LOG onCallEncryption userId=${userId}:${deviceId} encryptionKeyIndex=${encryptionKeyIndex} age=${age}ms`,
|
|
||||||
this.encryptionKeys,
|
|
||||||
);
|
|
||||||
this.setEncryptionKey(userId, deviceId, encryptionKeyIndex, encryptionKey, event.getTs());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private storeLastMembershipFingerprints(): void {
|
private storeLastMembershipFingerprints(): void {
|
||||||
this.lastMembershipFingerprints = new Set(
|
this.lastMembershipFingerprints = new Set(
|
||||||
this.getMemberships()
|
this.getMemberships()
|
||||||
@@ -466,14 +385,14 @@ export class EncryptionManager implements IEncryptionManager {
|
|||||||
const useKeyTimeout = setTimeout(() => {
|
const useKeyTimeout = setTimeout(() => {
|
||||||
this.setNewKeyTimeouts.delete(useKeyTimeout);
|
this.setNewKeyTimeouts.delete(useKeyTimeout);
|
||||||
logger.info(`Delayed-emitting key changed event for ${participantId} idx ${encryptionKeyIndex}`);
|
logger.info(`Delayed-emitting key changed event for ${participantId} idx ${encryptionKeyIndex}`);
|
||||||
if (userId === this.client.getUserId() && deviceId === this.client.getDeviceId()) {
|
if (userId === this.userId && deviceId === this.deviceId) {
|
||||||
this.currentEncryptionKeyIndex = encryptionKeyIndex;
|
this.currentEncryptionKeyIndex = encryptionKeyIndex;
|
||||||
}
|
}
|
||||||
this.onEncryptionKeysChanged(keyBin, encryptionKeyIndex, participantId);
|
this.onEncryptionKeysChanged(keyBin, encryptionKeyIndex, participantId);
|
||||||
}, this.useKeyDelay);
|
}, this.useKeyDelay);
|
||||||
this.setNewKeyTimeouts.add(useKeyTimeout);
|
this.setNewKeyTimeouts.add(useKeyTimeout);
|
||||||
} else {
|
} else {
|
||||||
if (userId === this.client.getUserId() && deviceId === this.client.getDeviceId()) {
|
if (userId === this.userId && deviceId === this.deviceId) {
|
||||||
this.currentEncryptionKeyIndex = encryptionKeyIndex;
|
this.currentEncryptionKeyIndex = encryptionKeyIndex;
|
||||||
}
|
}
|
||||||
this.onEncryptionKeysChanged(keyBin, encryptionKeyIndex, participantId);
|
this.onEncryptionKeysChanged(keyBin, encryptionKeyIndex, participantId);
|
||||||
@@ -493,8 +412,10 @@ export class EncryptionManager implements IEncryptionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getParticipantId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`;
|
const getParticipantId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`;
|
||||||
|
|
||||||
function keysEqual(a: Uint8Array | undefined, b: Uint8Array | undefined): boolean {
|
function keysEqual(a: Uint8Array | undefined, b: Uint8Array | undefined): boolean {
|
||||||
if (a === b) return true;
|
if (a === b) return true;
|
||||||
return !!a && !!b && a.length === b.length && a.every((x, i) => x === b[i]);
|
return !!a && !!b && a.length === b.length && a.every((x, i) => x === b[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getParticipantIdFromMembership = (m: CallMembership): string => getParticipantId(m.sender!, m.deviceId);
|
const getParticipantIdFromMembership = (m: CallMembership): string => getParticipantId(m.sender!, m.deviceId);
|
||||||
|
49
src/matrixrtc/IKeyTransport.ts
Normal file
49
src/matrixrtc/IKeyTransport.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
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 { type MatrixEvent } from "../models/event.ts";
|
||||||
|
import { type Statistics } from "./EncryptionManager.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic interface for the transport used to share room keys.
|
||||||
|
* Keys can be shared using different transports, e.g. to-device messages or room messages.
|
||||||
|
*/
|
||||||
|
export interface IKeyTransport {
|
||||||
|
/**
|
||||||
|
* Sends the current user media key.
|
||||||
|
* @param keyBase64Encoded
|
||||||
|
* @param index
|
||||||
|
*/
|
||||||
|
sendKey(keyBase64Encoded: string, index: number): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes an incoming event from the transport and extracts the key information.
|
||||||
|
* @param event
|
||||||
|
* @param statistics
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
receiveRoomEvent(
|
||||||
|
event: MatrixEvent,
|
||||||
|
statistics: Statistics,
|
||||||
|
callback: (
|
||||||
|
userId: string,
|
||||||
|
deviceId: string,
|
||||||
|
encryptionKeyIndex: number,
|
||||||
|
encryptionKeyString: string,
|
||||||
|
timestamp: number,
|
||||||
|
) => void,
|
||||||
|
): void;
|
||||||
|
}
|
@@ -30,6 +30,7 @@ import { EncryptionManager, type IEncryptionManager, type Statistics } from "./E
|
|||||||
import { LegacyMembershipManager } from "./LegacyMembershipManager.ts";
|
import { LegacyMembershipManager } from "./LegacyMembershipManager.ts";
|
||||||
import { logDurationSync } from "../utils.ts";
|
import { logDurationSync } from "../utils.ts";
|
||||||
import type { IMembershipManager } from "./types.ts";
|
import type { IMembershipManager } from "./types.ts";
|
||||||
|
import { RoomKeyTransport } from "./RoomKeyTransport.ts";
|
||||||
|
|
||||||
const logger = rootLogger.getChild("MatrixRTCSession");
|
const logger = rootLogger.getChild("MatrixRTCSession");
|
||||||
|
|
||||||
@@ -306,10 +307,13 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
// TODO: double check if this is actually needed. Should be covered by refreshRoom in MatrixRTCSessionManager
|
// TODO: double check if this is actually needed. Should be covered by refreshRoom in MatrixRTCSessionManager
|
||||||
roomState?.on(RoomStateEvent.Members, this.onRoomMemberUpdate);
|
roomState?.on(RoomStateEvent.Members, this.onRoomMemberUpdate);
|
||||||
this.setExpiryTimer();
|
this.setExpiryTimer();
|
||||||
|
|
||||||
|
const transport = new RoomKeyTransport(this.roomSubset.roomId, this.client);
|
||||||
this.encryptionManager = new EncryptionManager(
|
this.encryptionManager = new EncryptionManager(
|
||||||
this.client,
|
this.client.getUserId()!,
|
||||||
this.roomSubset,
|
this.client.getDeviceId()!,
|
||||||
() => this.memberships,
|
() => this.memberships,
|
||||||
|
transport,
|
||||||
(keyBin: Uint8Array<ArrayBufferLike>, encryptionKeyIndex: number, participantId: string) => {
|
(keyBin: Uint8Array<ArrayBufferLike>, encryptionKeyIndex: number, participantId: string) => {
|
||||||
this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, keyBin, encryptionKeyIndex, participantId);
|
this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, keyBin, encryptionKeyIndex, participantId);
|
||||||
},
|
},
|
||||||
|
141
src/matrixrtc/RoomKeyTransport.ts
Normal file
141
src/matrixrtc/RoomKeyTransport.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
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 type { MatrixClient } from "../client.ts";
|
||||||
|
import type { EncryptionKeysEventContent } from "./types.ts";
|
||||||
|
import { EventType } from "../@types/event.ts";
|
||||||
|
import { type MatrixError } from "../http-api/errors.ts";
|
||||||
|
import { logger, type Logger } from "../logger.ts";
|
||||||
|
import { type IKeyTransport } from "./IKeyTransport.ts";
|
||||||
|
import { type MatrixEvent } from "../models/event.ts";
|
||||||
|
import { type Statistics } from "./EncryptionManager.ts";
|
||||||
|
|
||||||
|
export class RoomKeyTransport implements IKeyTransport {
|
||||||
|
private readonly prefixedLogger: Logger;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private roomId: string,
|
||||||
|
private client: Pick<MatrixClient, "sendEvent" | "getDeviceId" | "getUserId" | "cancelPendingEvent">,
|
||||||
|
) {
|
||||||
|
this.prefixedLogger = logger.getChild(`[RTC: ${roomId} RoomKeyTransport]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendKey(keyBase64Encoded: string, index: number): Promise<void> {
|
||||||
|
const content: EncryptionKeysEventContent = {
|
||||||
|
keys: [
|
||||||
|
{
|
||||||
|
index: index,
|
||||||
|
key: keyBase64Encoded,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
device_id: this.client.getDeviceId()!,
|
||||||
|
call_id: "",
|
||||||
|
sent_ts: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.client.sendEvent(this.roomId, EventType.CallEncryptionKeysPrefix, content);
|
||||||
|
} catch (error) {
|
||||||
|
this.prefixedLogger.error("Failed to send call encryption keys", error);
|
||||||
|
const matrixError = error as MatrixError;
|
||||||
|
if (matrixError.event) {
|
||||||
|
// cancel the pending event: we'll just generate a new one with our latest
|
||||||
|
// keys when we resend
|
||||||
|
this.client.cancelPendingEvent(matrixError.event);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public receiveRoomEvent(
|
||||||
|
event: MatrixEvent,
|
||||||
|
statistics: Statistics,
|
||||||
|
callback: (
|
||||||
|
userId: string,
|
||||||
|
deviceId: string,
|
||||||
|
encryptionKeyIndex: number,
|
||||||
|
encryptionKeyString: string,
|
||||||
|
timestamp: number,
|
||||||
|
) => void,
|
||||||
|
): void {
|
||||||
|
const userId = event.getSender();
|
||||||
|
const content = event.getContent<EncryptionKeysEventContent>();
|
||||||
|
|
||||||
|
const deviceId = content["device_id"];
|
||||||
|
const callId = content["call_id"];
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
logger.warn(`Received m.call.encryption_keys with no userId: callId=${callId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We currently only handle callId = "" (which is the default for room scoped calls)
|
||||||
|
if (callId !== "") {
|
||||||
|
logger.warn(
|
||||||
|
`Received m.call.encryption_keys with unsupported callId: userId=${userId}, deviceId=${deviceId}, callId=${callId}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(content.keys)) {
|
||||||
|
logger.warn(`Received m.call.encryption_keys where keys wasn't an array: callId=${callId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId === this.client.getUserId() && deviceId === this.client.getDeviceId()) {
|
||||||
|
// We store our own sender key in the same set along with keys from others, so it's
|
||||||
|
// important we don't allow our own keys to be set by one of these events (apart from
|
||||||
|
// the fact that we don't need it anyway because we already know our own keys).
|
||||||
|
logger.info("Ignoring our own keys event");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
statistics.counters.roomEventEncryptionKeysReceived += 1;
|
||||||
|
const age = Date.now() - (typeof content.sent_ts === "number" ? content.sent_ts : event.getTs());
|
||||||
|
statistics.totals.roomEventEncryptionKeysReceivedTotalAge += age;
|
||||||
|
|
||||||
|
for (const key of content.keys) {
|
||||||
|
if (!key) {
|
||||||
|
logger.info("Ignoring false-y key in keys event");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptionKey = key.key;
|
||||||
|
const encryptionKeyIndex = key.index;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!encryptionKey ||
|
||||||
|
encryptionKeyIndex === undefined ||
|
||||||
|
encryptionKeyIndex === null ||
|
||||||
|
callId === undefined ||
|
||||||
|
callId === null ||
|
||||||
|
typeof deviceId !== "string" ||
|
||||||
|
typeof callId !== "string" ||
|
||||||
|
typeof encryptionKey !== "string" ||
|
||||||
|
typeof encryptionKeyIndex !== "number"
|
||||||
|
) {
|
||||||
|
logger.warn(
|
||||||
|
`Malformed call encryption_key: userId=${userId}, deviceId=${deviceId}, encryptionKeyIndex=${encryptionKeyIndex} callId=${callId}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
`Embedded-E2EE-LOG onCallEncryption userId=${userId}:${deviceId} encryptionKeyIndex=${encryptionKeyIndex} age=${age}ms`,
|
||||||
|
);
|
||||||
|
callback(userId, deviceId, encryptionKeyIndex, encryptionKey, event.getTs());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user