Skip to content

Commit f477099

Browse files
committed
rtc: E2E ratchet key on new joiners
1 parent 07af3d9 commit f477099

File tree

3 files changed

+76
-9
lines changed

3 files changed

+76
-9
lines changed

src/matrixrtc/EncryptionManager.ts

+11
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ export interface IEncryptionManager {
4545
* objects containing encryption keys and their associated timestamps.
4646
*/
4747
getEncryptionKeys(): Map<string, Array<{ key: Uint8Array; timestamp: number }>>;
48+
49+
/**
50+
* The ratcheting is done on the decoding layer, the encryption manager asks for a key to be ratcheted, then
51+
* the lower layer will emit the ratcheted key to the encryption manager.
52+
* This is called after the key a ratchet request has been performed.
53+
*/
54+
onOwnKeyRatcheted(key: ArrayBuffer, keyIndex: number | undefined): void;
4855
}
4956

5057
/**
@@ -100,6 +107,10 @@ export class EncryptionManager implements IEncryptionManager {
100107
this.logger = (parentLogger ?? rootLogger).getChild(`[EncryptionManager]`);
101108
}
102109

110+
public onOwnKeyRatcheted(key: ArrayBuffer, keyIndex: number | undefined): void {
111+
this.logger.warn("Ratcheting key is not implemented in EncryptionManager");
112+
}
113+
103114
public getEncryptionKeys(): Map<string, Array<{ key: Uint8Array; timestamp: number }>> {
104115
return this.encryptionKeys;
105116
}

src/matrixrtc/MatrixRTCSession.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { MembershipManager } from "./NewMembershipManager.ts";
2828
import { EncryptionManager, type IEncryptionManager } from "./EncryptionManager.ts";
2929
import { LegacyMembershipManager } from "./LegacyMembershipManager.ts";
3030
import { logDurationSync } from "../utils.ts";
31-
import { type Statistics } from "./types.ts";
31+
import { type ParticipantId, type Statistics } from "./types.ts";
3232
import { RoomKeyTransport } from "./RoomKeyTransport.ts";
3333
import type { IMembershipManager } from "./IMembershipManager.ts";
3434
import { RTCEncryptionManager } from "./RTCEncryptionManager.ts";
@@ -49,6 +49,9 @@ export enum MatrixRTCSessionEvent {
4949
JoinStateChanged = "join_state_changed",
5050
// The key used to encrypt media has changed
5151
EncryptionKeyChanged = "encryption_key_changed",
52+
// Request ratcheting the current encryption key. When done `onOwnKeyRatcheted` will be called with the
53+
// ratcheted material.
54+
EncryptionKeyQueryRatchetStep = "encryption_key_ratchet_step",
5255
/** The membership manager had to shut down caused by an unrecoverable error */
5356
MembershipManagerError = "membership_manager_error",
5457
}
@@ -65,6 +68,7 @@ export type MatrixRTCSessionEventHandlerMap = {
6568
participantId: string,
6669
) => void;
6770
[MatrixRTCSessionEvent.MembershipManagerError]: (error: unknown) => void;
71+
[MatrixRTCSessionEvent.EncryptionKeyQueryRatchetStep]: (participantId: string, keyIndex: number) => void;
6872
};
6973

7074
export interface MembershipConfig {
@@ -424,6 +428,13 @@ export class MatrixRTCSession extends TypedEventEmitter<
424428
participantId,
425429
);
426430
},
431+
(participantId: ParticipantId, encryptionKeyIndex: number) => {
432+
this.emit(
433+
MatrixRTCSessionEvent.EncryptionKeyQueryRatchetStep,
434+
participantId,
435+
encryptionKeyIndex,
436+
);
437+
},
427438
this.logger,
428439
);
429440
} else {
@@ -522,6 +533,10 @@ export class MatrixRTCSession extends TypedEventEmitter<
522533
});
523534
}
524535

536+
public onOwnKeyRatcheted(material: ArrayBuffer, keyIndex?: number): void {
537+
this.encryptionManager?.onOwnKeyRatcheted(material, keyIndex);
538+
}
539+
525540
/**
526541
* A map of keys used to encrypt and decrypt (we are using a symmetric
527542
* cipher) given participant's media. This also includes our own key

src/matrixrtc/RTCEncryptionManager.ts

+49-8
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { type CallMembership } from "./CallMembership.ts";
2020
import { decodeBase64, encodeBase64 } from "../base64.ts";
2121
import { type IKeyTransport, type KeyTransportEventListener, KeyTransportEvents } from "./IKeyTransport.ts";
2222
import { logger as rootLogger, type Logger } from "../logger.ts";
23-
import { sleep } from "../utils.ts";
23+
import { defer, type IDeferred, sleep } from "../utils.ts";
2424
import type { InboundEncryptionSession, ParticipantDeviceInfo, ParticipantId, Statistics } from "./types.ts";
2525
import { getParticipantId, KeyBuffer } from "./utils.ts";
2626
import {
@@ -75,6 +75,8 @@ export class RTCEncryptionManager implements IEncryptionManager {
7575

7676
private logger: Logger;
7777

78+
private currentRatchetRequest: IDeferred<{ key: ArrayBuffer; keyIndex: number }> | null = null;
79+
7880
public constructor(
7981
private userId: string,
8082
private deviceId: string,
@@ -86,9 +88,10 @@ export class RTCEncryptionManager implements IEncryptionManager {
8688
encryptionKeyIndex: number,
8789
participantId: ParticipantId,
8890
) => void,
91+
private ratchetKey: (participantId: ParticipantId, encryptionKeyIndex: number) => void,
8992
parentLogger?: Logger,
9093
) {
91-
this.logger = (parentLogger ?? rootLogger).getChild(`[EncryptionManager]`);
94+
this.logger = (parentLogger ?? rootLogger).getChild(`[RTCEncryptionManager]`);
9295
}
9396

9497
public getEncryptionKeys(): Map<string, Array<{ key: Uint8Array; timestamp: number }>> {
@@ -163,7 +166,9 @@ export class RTCEncryptionManager implements IEncryptionManager {
163166
}
164167

165168
public onNewKeyReceived: KeyTransportEventListener = (userId, deviceId, keyBase64Encoded, index, timestamp) => {
166-
this.logger.debug(`Received key over transport ${userId}:${deviceId} at index ${index}`);
169+
this.logger.debug(
170+
`Received key over transport ${userId}:${deviceId} at index ${index} key: ${keyBase64Encoded}`,
171+
);
167172

168173
// We received a new key, notify the video layer of this new key so that it can decrypt the frames properly.
169174
const participantId = getParticipantId(userId, deviceId);
@@ -216,7 +221,13 @@ export class RTCEncryptionManager implements IEncryptionManager {
216221
// get current memberships
217222
const toShareWith: ParticipantDeviceInfo[] = this.getMemberships()
218223
.filter((membership) => {
219-
return membership.sender != undefined;
224+
return (
225+
membership.sender != undefined &&
226+
!(
227+
// filter me out
228+
(membership.sender == this.userId && membership.deviceId == this.deviceId)
229+
)
230+
);
220231
})
221232
.map((membership) => {
222233
return {
@@ -272,13 +283,37 @@ export class RTCEncryptionManager implements IEncryptionManager {
272283
toDistributeTo = toShareWith;
273284
outboundKey = newOutboundKey;
274285
} else if (anyJoined.length > 0) {
275-
// keep the same key
276-
// XXX In the future we want to distribute a ratcheted key not the current one
286+
if (this.outboundSession!.sharedWith.length > 0) {
287+
// This key was already shared with someone, we need to ratchet it
288+
// We want to ratchet the current key and only distribute the ratcheted key to the new joiners
289+
// This needs to send some async messages, so we need to wait for the ratchet to finish
290+
const deferredKey = defer<{ key: ArrayBuffer; keyIndex: number }>();
291+
this.currentRatchetRequest = deferredKey;
292+
this.logger.info(`Query ratcheting key index:${this.outboundSession!.keyId} ...`);
293+
this.ratchetKey(getParticipantId(this.userId, this.deviceId), this.outboundSession!.keyId);
294+
const res = await Promise.race([deferredKey.promise, sleep(1000)]);
295+
if (res === undefined) {
296+
// TODO: we might want to rotate the key instead?
297+
this.logger.error("Ratchet key timed out sharing the same key for now :/");
298+
} else {
299+
const { key, keyIndex } = await deferredKey.promise;
300+
this.logger.info(
301+
`... Ratcheting done key index:${keyIndex} key:${encodeBase64(new Uint8Array(key))}`,
302+
);
303+
this.outboundSession!.key = new Uint8Array(key);
304+
this.onEncryptionKeysChanged(
305+
this.outboundSession!.key,
306+
this.outboundSession!.keyId,
307+
getParticipantId(this.userId, this.deviceId),
308+
);
309+
}
310+
}
277311
toDistributeTo = anyJoined;
278312
outboundKey = this.outboundSession!;
279313
} else {
280-
// no changes
281-
return;
314+
// No one joined or left, it could just be the first key, keep going
315+
toDistributeTo = [];
316+
outboundKey = this.outboundSession!;
282317
}
283318

284319
try {
@@ -318,4 +353,10 @@ export class RTCEncryptionManager implements IEncryptionManager {
318353
globalThis.crypto.getRandomValues(key);
319354
return key;
320355
}
356+
357+
public onOwnKeyRatcheted(key: ArrayBuffer, keyIndex: number | undefined): void {
358+
this.logger.debug(`Own key ratcheted for key index:${keyIndex} key:${encodeBase64(new Uint8Array(key))}`);
359+
360+
this.currentRatchetRequest?.resolve({ key, keyIndex: keyIndex! });
361+
}
321362
}

0 commit comments

Comments
 (0)