Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MatrixRTC key distribution using to-device messaging #4485

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/matrixrtc/CallMembership.ts
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ import { Focus } from "./focus.ts";
import { isLivekitFocusActive } from "./LivekitFocus.ts";

type CallScope = "m.room" | "m.user";

// Represents an entry in the memberships section of an m.call.member event as it is on the wire

// There are two different data interfaces. One for the Legacy types and one compliant with MSC4143
@@ -39,6 +40,8 @@ export type SessionMembershipData = {

// Application specific data
scope?: CallScope;

key_distribution?: KeyDistributionMechanism;
};

export const isSessionMembershipData = (data: CallMembershipData): data is SessionMembershipData =>
@@ -69,6 +72,7 @@ export type CallMembershipDataLegacy = {
membershipID: string;
created_ts?: number;
foci_active?: Focus[];
key_distribution?: KeyDistributionMechanism;
} & EitherAnd<{ expires: number }, { expires_ts: number }>;

export const isLegacyCallMembershipData = (data: CallMembershipData): data is CallMembershipDataLegacy =>
@@ -103,6 +107,8 @@ const checkCallMembershipDataLegacy = (data: any, errors: string[]): data is Cal

export type CallMembershipData = CallMembershipDataLegacy | SessionMembershipData;

type KeyDistributionMechanism = "room_event" | "to_device";

export class CallMembership {
public static equal(a: CallMembership, b: CallMembership): boolean {
return deepCompare(a.membershipData, b.membershipData);
@@ -244,4 +250,8 @@ export class CallMembership {
}
}
}

public get keyDistributionMethod(): KeyDistributionMechanism {
return this.membershipData.key_distribution ?? "room_event";
}
}
149 changes: 126 additions & 23 deletions src/matrixrtc/MatrixRTCSession.ts
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
import { EventTimeline } from "../models/event-timeline.ts";
import { Room } from "../models/room.ts";
import { MatrixClient } from "../client.ts";
import { MatrixClient, SendToDeviceContentMap } from "../client.ts";
import { EventType } from "../@types/event.ts";
import { UpdateDelayedEventAction } from "../@types/requests.ts";
import {
@@ -31,14 +31,15 @@
import { RoomStateEvent } from "../models/room-state.ts";
import { Focus } from "./focus.ts";
import { randomString, secureRandomBase64Url } from "../randomstring.ts";
import { EncryptionKeysEventContent } from "./types.ts";
import { EncryptionKeysEventContent, EncryptionKeysToDeviceContent } from "./types.ts";
import { decodeBase64, encodeUnpaddedBase64 } from "../base64.ts";
import { KnownMembership } from "../@types/membership.ts";
import { MatrixError } from "../http-api/errors.ts";
import { MatrixEvent } from "../models/event.ts";
import { isLivekitFocusActive } from "./LivekitFocus.ts";
import { ExperimentalGroupCallRoomMemberState } from "../webrtc/groupCall.ts";
import { sleep } from "../utils.ts";
import type { RoomWidgetClient } from "../embedded.ts";

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

@@ -162,8 +163,21 @@
* The number of times we have received a room event containing encryption keys.
*/
roomEventEncryptionKeysReceived: 0,
/**
* The number of times we have sent a to-device event containing encryption keys.
*/
toDeviceEncryptionKeysSent: 0,
/**
* The number of times we have received a to-device event containing encryption keys.
*/
toDeviceEncryptionKeysReceived: 0,
},
totals: {
/**
* The total age (in milliseconds) of all to-device events containing encryption keys that we have received.
* We track the total age so that we can later calculate the average age of all keys received.
*/
toDeviceEncryptionKeysReceivedTotalAge: 0,
/**
* The total age (in milliseconds) of all room events containing encryption keys that we have received.
* We track the total age so that we can later calculate the average age of all keys received.
@@ -546,7 +560,7 @@
}

/**
* Requests that we resend our current keys to the room. May send a keys event immediately
* Requests that we (re)-send our current keys to the room. May send a keys event immediately
* or queue for alter if one has already been sent recently.
*/
private requestSendCurrentKey(): void {
@@ -602,21 +616,10 @@
const keyToSend = myKeys[keyIndexToSend];

try {
const content: EncryptionKeysEventContent = {
keys: [
{
index: keyIndexToSend,
key: encodeUnpaddedBase64(keyToSend),
},
],
device_id: deviceId,
call_id: "",
sent_ts: Date.now(),
};

this.statistics.counters.roomEventEncryptionKeysSent += 1;

await this.client.sendEvent(this.room.roomId, EventType.CallEncryptionKeysPrefix, content);
await Promise.all([
this.sendKeysViaRoomEvent(deviceId, keyToSend, keyIndexToSend),
this.sendKeysViaToDevice(deviceId, keyToSend, keyIndexToSend),
]);

logger.debug(
`Embedded-E2EE-LOG updateEncryptionKeyEvent participantId=${userId}:${deviceId} numKeys=${myKeys.length} currentKeyIndex=${this.currentEncryptionKeyIndex} keyIndexToSend=${keyIndexToSend}`,
@@ -639,6 +642,96 @@
}
};

private async sendKeysViaRoomEvent(deviceId: string, key: Uint8Array, index: number): Promise<void> {
const membersRequiringRoomEvent = this.memberships.filter(
(m) => !this.isMyMembership(m) && m.keyDistributionMethod === "room_event",
);

if (membersRequiringRoomEvent.length === 0) {
logger.info("No members require keys via room event");
return;
}

logger.info(
`Sending encryption keys event for: ${membersRequiringRoomEvent.map((m) => `${m.sender}:${m.deviceId}`).join(", ")}`,
);

const content: EncryptionKeysEventContent = {
keys: [
{
index,
key: encodeUnpaddedBase64(key),
},
],
device_id: deviceId,
call_id: "",
sent_ts: Date.now(),
};

this.statistics.counters.roomEventEncryptionKeysSent += 1;

await this.client.sendEvent(this.room.roomId, EventType.CallEncryptionKeysPrefix, content);
}

private async sendKeysViaToDevice(deviceId: string, key: Uint8Array, index: number): Promise<void> {
const membershipsRequiringToDevice = this.memberships.filter(
(m) => !this.isMyMembership(m) && m.sender && m.keyDistributionMethod === "to_device",
);

if (membershipsRequiringToDevice.length === 0) {
logger.info("No members require keys via to-device event");
return;
}

const content: EncryptionKeysToDeviceContent = {
keys: [{ index, key: encodeUnpaddedBase64(key) }],
device_id: deviceId,
call_id: "",
room_id: this.room.roomId,
sent_ts: Date.now(),
};

logger.info(
`Sending encryption keys to-device batch for: ${membershipsRequiringToDevice.map(({ sender, deviceId }) => `${sender}:${deviceId}`).join(", ")}`,
);

this.statistics.counters.toDeviceEncryptionKeysSent += membershipsRequiringToDevice.length;

// we don't do an instanceof due to circular dependency issues
if ("widgetApi" in this.client) {
logger.info("Sending keys via widgetApi");
// embedded mode, getCrypto() returns null and so we make some assumptions about the underlying implementation

const contentMap: SendToDeviceContentMap = new Map();

membershipsRequiringToDevice.forEach(({ sender, deviceId }) => {
if (!contentMap.has(sender!)) {
contentMap.set(sender!, new Map());
}

contentMap.get(sender!)!.set(deviceId, content);
});

await (this.client as unknown as RoomWidgetClient).sendToDeviceViaWidgetApi(
EventType.CallEncryptionKeysPrefix,
true,
contentMap,
);
} else {
const crypto = this.client.getCrypto();
if (!crypto) {
logger.error("No crypto instance available to send keys via to-device event");
return;
}

const devices = membershipsRequiringToDevice.map(({ deviceId, sender }) => ({ userId: sender!, deviceId }));

const batch = await crypto.encryptToDeviceMessages(EventType.CallEncryptionKeysPrefix, devices, content);

await this.client.queueToDevice(batch);
}
}

/**
* Sets a timer for the soonest membership expiry
*/
@@ -714,9 +807,17 @@
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;
let age: number;

if (event.getRoomId()) {

Check failure on line 812 in src/matrixrtc/MatrixRTCSession.ts

GitHub Actions / Jest [unit] (Node lts/*)

MatrixRTCSession › key management › receiving › collects keys from encryption events

TypeError: event.getRoomId is not a function at MatrixRTCSession.getRoomId [as onCallEncryption] (src/matrixrtc/MatrixRTCSession.ts:812:19) at Object.onCallEncryption (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:1179:22)

Check failure on line 812 in src/matrixrtc/MatrixRTCSession.ts

GitHub Actions / Jest [unit] (Node 22)

MatrixRTCSession › key management › receiving › collects keys from encryption events

TypeError: event.getRoomId is not a function at MatrixRTCSession.getRoomId [as onCallEncryption] (src/matrixrtc/MatrixRTCSession.ts:812:19) at Object.onCallEncryption (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:1179:22)
this.statistics.counters.roomEventEncryptionKeysReceived += 1;
age = Date.now() - (typeof content.sent_ts === "number" ? content.sent_ts : event.getTs());
this.statistics.totals.roomEventEncryptionKeysReceivedTotalAge += age;
} else {
this.statistics.counters.toDeviceEncryptionKeysReceived += 1;
age = Date.now() - (content as EncryptionKeysToDeviceContent).sent_ts;
this.statistics.totals.toDeviceEncryptionKeysReceivedTotalAge += age;
}

for (const key of content.keys) {
if (!key) {
@@ -795,8 +896,8 @@
logger.debug(`Member(s) have left: queueing sender key rotation`);
this.makeNewKeyTimeout = setTimeout(this.onRotateKeyTimeout, MAKE_KEY_DELAY);
} else if (anyJoined) {
logger.debug(`New member(s) have joined: re-sending keys`);
this.requestSendCurrentKey();
logger.debug(`New member(s) have joined: rotating keys`);
this.makeNewKeyTimeout = setTimeout(this.onRotateKeyTimeout, MAKE_KEY_DELAY);
} else if (oldFingerprints) {
// does it look like any of the members have updated their memberships?
const newFingerprints = this.lastMembershipFingerprints!;
@@ -849,6 +950,7 @@
foci_active: this.ownFociPreferred,
membershipID: this.membershipId,
...(createdTs ? { created_ts: createdTs } : {}),
key_distribution: "to_device",
};
}
/**
@@ -862,6 +964,7 @@
device_id: deviceId,
focus_active: { type: "livekit", focus_selection: "oldest_membership" },
foci_preferred: this.ownFociPreferred ?? [],
key_distribution: "to_device",
};
}

52 changes: 41 additions & 11 deletions src/matrixrtc/MatrixRTCSessionManager.ts
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ import { RoomState, RoomStateEvent } from "../models/room-state.ts";
import { MatrixEvent } from "../models/event.ts";
import { MatrixRTCSession } from "./MatrixRTCSession.ts";
import { EventType } from "../@types/event.ts";
import { EncryptionKeysToDeviceContent } from "./types.ts";

const logger = rootLogger.getChild("MatrixRTCSessionManager");

@@ -56,7 +57,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM

public start(): void {
// We shouldn't need to null-check here, but matrix-client.spec.ts mocks getRooms
// returing nothing, and breaks tests if you change it to return an empty array :'(
// returning nothing, and breaks tests if you change it to return an empty array :'(
for (const room of this.client.getRooms() ?? []) {
const session = MatrixRTCSession.roomSessionForRoom(this.client, room);
if (session.memberships.length > 0) {
@@ -67,6 +68,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
this.client.on(ClientEvent.Room, this.onRoom);
this.client.on(RoomEvent.Timeline, this.onTimeline);
this.client.on(RoomStateEvent.Events, this.onRoomState);
this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
}

public stop(): void {
@@ -78,6 +80,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
this.client.off(ClientEvent.Room, this.onRoom);
this.client.off(RoomEvent.Timeline, this.onTimeline);
this.client.off(RoomStateEvent.Events, this.onRoomState);
this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
}

/**
@@ -100,15 +103,40 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
return this.roomSessions.get(room.roomId)!;
}

private async consumeCallEncryptionEvent(event: MatrixEvent, isRetry = false): Promise<void> {
private onTimeline = (event: MatrixEvent): void => {
this.consumeCallEncryptionEvent(event, (event) => event.getRoomId(), false);
};

private onToDeviceEvent = (event: MatrixEvent): void => {
if (!event.isEncrypted()) {
logger.warn("Ignoring unencrypted to-device call encryption event", event);
return;
}
this.consumeCallEncryptionEvent(
event,
(event) => event.getContent<EncryptionKeysToDeviceContent>().room_id,
false,
);
};

/**
* @param event - the event to consume
* @param roomIdExtractor - the function to extract the room id from the event
* @param isRetry - whether this is a retry. If false we will retry decryption failures once
*/
private consumeCallEncryptionEvent = async (
event: MatrixEvent,
roomIdExtractor: (event: MatrixEvent) => string | undefined,
isRetry: boolean,
): Promise<void> => {
await this.client.decryptEventIfNeeded(event);
if (event.isDecryptionFailure()) {
if (!isRetry) {
logger.warn(
`Decryption failed for event ${event.getId()}: ${event.decryptionFailureReason} will retry once only`,
);
// retry after 1 second. After this we give up.
setTimeout(() => this.consumeCallEncryptionEvent(event, true), 1000);
setTimeout(() => this.consumeCallEncryptionEvent(event, roomIdExtractor, true), 1000);
} else {
logger.warn(`Decryption failed for event ${event.getId()}: ${event.decryptionFailureReason}`);
}
@@ -117,18 +145,20 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
logger.info(`Decryption succeeded for event ${event.getId()} after retry`);
}

if (event.getType() !== EventType.CallEncryptionKeysPrefix) return Promise.resolve();
if (event.getType() !== EventType.CallEncryptionKeysPrefix) return;
const roomId = roomIdExtractor(event);
if (!roomId) {
logger.error("Received call encryption event with no room_id!");
return;
}

const room = this.client.getRoom(roomId);

const room = this.client.getRoom(event.getRoomId());
if (!room) {
logger.error(`Got room state event for unknown room ${event.getRoomId()}!`);
return Promise.resolve();
logger.error(`Got encryption event for unknown room ${roomId}!`);
return;
}

this.getRoomSession(room).onCallEncryption(event);
}
private onTimeline = (event: MatrixEvent): void => {
this.consumeCallEncryptionEvent(event);
};

private onRoom = (room: Room): void => {
5 changes: 5 additions & 0 deletions src/matrixrtc/types.ts
Original file line number Diff line number Diff line change
@@ -26,6 +26,11 @@ export interface EncryptionKeysEventContent {
sent_ts?: number;
}

export interface EncryptionKeysToDeviceContent extends EncryptionKeysEventContent {
room_id?: string;
sent_ts: number;
}

export type CallNotifyType = "ring" | "notify";

export interface ICallNotifyContent {

Unchanged files with check annotations Beta

jest.useFakeTimers();
sess!.joinRoomSession([mockFocus], mockFocus);
await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client.sendStateEvent).toHaveBeenCalledWith(

Check failure on line 445 in spec/unit/matrixrtc/MatrixRTCSession.spec.ts

GitHub Actions / Jest [unit] (Node lts/*)

MatrixRTCSession › joining › sends a membership event when joining a call

expect(jest.fn()).toHaveBeenCalledWith(...expected) - Expected + Received "mNH20QJF", "org.matrix.msc3401.call.member", @@ -9,10 +9,11 @@ "foci_active": Array [ Object { "type": "mock", }, ], - "membershipID": StringMatching /.*/, + "key_distribution": "to_device", + "membershipID": "s0e0O", "scope": "m.room", }, ], }, "@alice:example.org", Number of calls: 1 at Object.toHaveBeenCalledWith (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:445:43) at asyncGeneratorStep (node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:17) at _next (node_modules/@babel/runtime/helpers/asyncToGenerator.js:17:9)

Check failure on line 445 in spec/unit/matrixrtc/MatrixRTCSession.spec.ts

GitHub Actions / Jest [unit] (Node 22)

MatrixRTCSession › joining › sends a membership event when joining a call

expect(jest.fn()).toHaveBeenCalledWith(...expected) - Expected + Received "1LtcTZdo", "org.matrix.msc3401.call.member", @@ -9,10 +9,11 @@ "foci_active": Array [ Object { "type": "mock", }, ], - "membershipID": StringMatching /.*/, + "key_distribution": "to_device", + "membershipID": "HhMqL", "scope": "m.room", }, ], }, "@alice:example.org", Number of calls: 1 at Object.toHaveBeenCalledWith (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:445:43) at asyncGeneratorStep (node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:17) at _next (node_modules/@babel/runtime/helpers/asyncToGenerator.js:17:9)
mockRoom!.roomId,
EventType.GroupCallMemberPrefix,
{
jest.useFakeTimers();
sess!.joinRoomSession([activeFocusConfig], activeFocus, { useLegacyMemberEvents: false });
await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client.sendStateEvent).toHaveBeenCalledWith(

Check failure on line 483 in spec/unit/matrixrtc/MatrixRTCSession.spec.ts

GitHub Actions / Jest [unit] (Node lts/*)

MatrixRTCSession › joining › non-legacy calls › sends a membership event with session payload when joining a non-legacy call

expect(jest.fn()).toHaveBeenCalledWith(...expected) - Expected + Received "yzzE08Cq", "org.matrix.msc3401.call.member", @@ -10,7 +10,8 @@ ], "focus_active": Object { "focus_selection": "oldest_membership", "type": "livekit", }, + "key_distribution": "to_device", "scope": "m.room", }, "_@alice:example.org_AAAAAAA", Number of calls: 1 at toHaveBeenCalledWith (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:483:47) at asyncGeneratorStep (node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:17) at _next (node_modules/@babel/runtime/helpers/asyncToGenerator.js:17:9)

Check failure on line 483 in spec/unit/matrixrtc/MatrixRTCSession.spec.ts

GitHub Actions / Jest [unit] (Node lts/*)

MatrixRTCSession › joining › non-legacy calls › does not prefix the state key with _ for rooms that support user-owned state events

expect(jest.fn()).toHaveBeenCalledWith(...expected) - Expected + Received "OKsFiFNx", "org.matrix.msc3401.call.member", @@ -10,7 +10,8 @@ ], "focus_active": Object { "focus_selection": "oldest_membership", "type": "livekit", }, + "key_distribution": "to_device", "scope": "m.room", }, "@alice:example.org_AAAAAAA", Number of calls: 1 at toHaveBeenCalledWith (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:483:47) at asyncGeneratorStep (node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:17) at _next (node_modules/@babel/runtime/helpers/asyncToGenerator.js:17:9)

Check failure on line 483 in spec/unit/matrixrtc/MatrixRTCSession.spec.ts

GitHub Actions / Jest [unit] (Node 22)

MatrixRTCSession › joining › non-legacy calls › sends a membership event with session payload when joining a non-legacy call

expect(jest.fn()).toHaveBeenCalledWith(...expected) - Expected + Received "bJsvCjA9", "org.matrix.msc3401.call.member", @@ -10,7 +10,8 @@ ], "focus_active": Object { "focus_selection": "oldest_membership", "type": "livekit", }, + "key_distribution": "to_device", "scope": "m.room", }, "_@alice:example.org_AAAAAAA", Number of calls: 1 at toHaveBeenCalledWith (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:483:47) at asyncGeneratorStep (node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:17) at _next (node_modules/@babel/runtime/helpers/asyncToGenerator.js:17:9)

Check failure on line 483 in spec/unit/matrixrtc/MatrixRTCSession.spec.ts

GitHub Actions / Jest [unit] (Node 22)

MatrixRTCSession › joining › non-legacy calls › does not prefix the state key with _ for rooms that support user-owned state events

expect(jest.fn()).toHaveBeenCalledWith(...expected) - Expected + Received "hp94LSEg", "org.matrix.msc3401.call.member", @@ -10,7 +10,8 @@ ], "focus_active": Object { "focus_selection": "oldest_membership", "type": "livekit", }, + "key_distribution": "to_device", "scope": "m.room", }, "@alice:example.org_AAAAAAA", Number of calls: 1 at toHaveBeenCalledWith (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:483:47) at asyncGeneratorStep (node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:17) at _next (node_modules/@babel/runtime/helpers/asyncToGenerator.js:17:9)
mockRoom!.roomId,
EventType.GroupCallMemberPrefix,
{
jest.advanceTimersByTime(timeElapsed);
await eventReSentPromise;
expect(sendStateEventMock).toHaveBeenCalledWith(

Check failure on line 573 in spec/unit/matrixrtc/MatrixRTCSession.spec.ts

GitHub Actions / Jest [unit] (Node lts/*)

MatrixRTCSession › joining › renews membership event before expiry time

expect(jest.fn()).toHaveBeenCalledWith(...expected) - Expected + Received "KE0SCpjI", "org.matrix.msc3401.call.member", @@ -10,10 +10,11 @@ "foci_active": Array [ Object { "type": "mock", }, ], - "membershipID": StringMatching /.*/, + "key_distribution": "to_device", + "membershipID": "LNprs", "scope": "m.room", }, ], }, "@alice:example.org", Number of calls: 1 at Object.toHaveBeenCalledWith (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:573:44) at asyncGeneratorStep (node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:17) at _next (node_modules/@babel/runtime/helpers/asyncToGenerator.js:17:9)

Check failure on line 573 in spec/unit/matrixrtc/MatrixRTCSession.spec.ts

GitHub Actions / Jest [unit] (Node 22)

MatrixRTCSession › joining › renews membership event before expiry time

expect(jest.fn()).toHaveBeenCalledWith(...expected) - Expected + Received "4ByXzKPD", "org.matrix.msc3401.call.member", @@ -10,10 +10,11 @@ "foci_active": Array [ Object { "type": "mock", }, ], - "membershipID": StringMatching /.*/, + "key_distribution": "to_device", + "membershipID": "u76Op", "scope": "m.room", }, ], }, "@alice:example.org", Number of calls: 1 at Object.toHaveBeenCalledWith (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:573:44) at asyncGeneratorStep (node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:17) at _next (node_modules/@babel/runtime/helpers/asyncToGenerator.js:17:9)
mockRoom.roomId,
EventType.GroupCallMemberPrefix,
{
);
});
it("sends keys when joining", async () => {

Check failure on line 683 in spec/unit/matrixrtc/MatrixRTCSession.spec.ts

GitHub Actions / Jest [unit] (Node lts/*)

MatrixRTCSession › key management › sending › sends keys when joining

thrown: "Exceeded timeout of 5000 ms for a test. Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout." at it (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:683:13) at describe (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:647:9) at describe (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:646:5) at Object.describe (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:41:1)

Check failure on line 683 in spec/unit/matrixrtc/MatrixRTCSession.spec.ts

GitHub Actions / Jest [unit] (Node 22)

MatrixRTCSession › key management › sending › sends keys when joining

thrown: "Exceeded timeout of 5000 ms for a test. Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout." at it (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:683:13) at describe (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:647:9) at describe (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:646:5) at Object.describe (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:41:1)
jest.useFakeTimers();
try {
const eventSentPromise = new Promise((resolve) => {
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
expect(client.sendEvent).toHaveBeenCalledTimes(1);

Check failure on line 719 in spec/unit/matrixrtc/MatrixRTCSession.spec.ts

GitHub Actions / Jest [unit] (Node lts/*)

MatrixRTCSession › key management › sending › does not send key if join called when already joined

expect(jest.fn()).toHaveBeenCalledTimes(expected) Expected number of calls: 1 Received number of calls: 0 at Object.toHaveBeenCalledTimes (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:719:42)

Check failure on line 719 in spec/unit/matrixrtc/MatrixRTCSession.spec.ts

GitHub Actions / Jest [unit] (Node 22)

MatrixRTCSession › key management › sending › does not send key if join called when already joined

expect(jest.fn()).toHaveBeenCalledTimes(expected) Expected number of calls: 1 Received number of calls: 0 at Object.toHaveBeenCalledTimes (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:719:42)
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
});
it("retries key sends", async () => {

Check failure on line 728 in spec/unit/matrixrtc/MatrixRTCSession.spec.ts

GitHub Actions / Jest [unit] (Node lts/*)

MatrixRTCSession › key management › sending › retries key sends

thrown: "Exceeded timeout of 5000 ms for a test. Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout." at it (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:728:13) at describe (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:647:9) at describe (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:646:5) at Object.describe (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:41:1)

Check failure on line 728 in spec/unit/matrixrtc/MatrixRTCSession.spec.ts

GitHub Actions / Jest [unit] (Node 22)

MatrixRTCSession › key management › sending › retries key sends

thrown: "Exceeded timeout of 5000 ms for a test. Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout." at it (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:728:13) at describe (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:647:9) at describe (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:646:5) at Object.describe (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:41:1)
jest.useFakeTimers();
let firstEventSent = false;
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
expect(client.cancelPendingEvent).toHaveBeenCalledWith(eventSentinel);

Check failure on line 773 in spec/unit/matrixrtc/MatrixRTCSession.spec.ts

GitHub Actions / Jest [unit] (Node lts/*)

MatrixRTCSession › key management › sending › cancels key send event that fail

expect(jest.fn()).toHaveBeenCalledWith(...expected) Expected: {} Number of calls: 0 at Object.toHaveBeenCalledWith (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:773:51) at asyncGeneratorStep (node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:17) at _next (node_modules/@babel/runtime/helpers/asyncToGenerator.js:17:9) at node_modules/@babel/runtime/helpers/asyncToGenerator.js:22:7 at Object.<anonymous> (node_modules/@babel/runtime/helpers/asyncToGenerator.js:14:12)

Check failure on line 773 in spec/unit/matrixrtc/MatrixRTCSession.spec.ts

GitHub Actions / Jest [unit] (Node 22)

MatrixRTCSession › key management › sending › cancels key send event that fail

expect(jest.fn()).toHaveBeenCalledWith(...expected) Expected: {} Number of calls: 0 at Object.toHaveBeenCalledWith (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:773:51) at asyncGeneratorStep (node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:17) at _next (node_modules/@babel/runtime/helpers/asyncToGenerator.js:17:9) at node_modules/@babel/runtime/helpers/asyncToGenerator.js:22:7 at Object.<anonymous> (node_modules/@babel/runtime/helpers/asyncToGenerator.js:14:12)
});
it("Re-sends key if a new member joins", async () => {

Check failure on line 776 in spec/unit/matrixrtc/MatrixRTCSession.spec.ts

GitHub Actions / Jest [unit] (Node lts/*)

MatrixRTCSession › key management › sending › Re-sends key if a new member joins

thrown: "Exceeded timeout of 5000 ms for a test. Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout." at it (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:776:13) at describe (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:647:9) at describe (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:646:5) at Object.describe (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:41:1)

Check failure on line 776 in spec/unit/matrixrtc/MatrixRTCSession.spec.ts

GitHub Actions / Jest [unit] (Node 22)

MatrixRTCSession › key management › sending › Re-sends key if a new member joins

thrown: "Exceeded timeout of 5000 ms for a test. Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout." at it (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:776:13) at describe (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:647:9) at describe (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:646:5) at Object.describe (spec/unit/matrixrtc/MatrixRTCSession.spec.ts:41:1)
jest.useFakeTimers();
try {
const mockRoom = makeMockRoom([membershipTemplate]);