livekit-client 2.11.3 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +30 -23
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +126 -27
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts +2 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/KeyProvider.d.ts +8 -5
- package/dist/src/e2ee/KeyProvider.d.ts.map +1 -1
- package/dist/src/e2ee/constants.d.ts.map +1 -1
- package/dist/src/e2ee/events.d.ts +8 -3
- package/dist/src/e2ee/events.d.ts.map +1 -1
- package/dist/src/e2ee/types.d.ts +5 -3
- package/dist/src/e2ee/types.d.ts.map +1 -1
- package/dist/src/e2ee/utils.d.ts +1 -1
- package/dist/src/e2ee/utils.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +5 -6
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +2 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +1 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +11 -1
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/ts4.2/src/api/SignalClient.d.ts +2 -1
- package/dist/ts4.2/src/e2ee/KeyProvider.d.ts +8 -5
- package/dist/ts4.2/src/e2ee/events.d.ts +8 -3
- package/dist/ts4.2/src/e2ee/types.d.ts +5 -3
- package/dist/ts4.2/src/e2ee/utils.d.ts +1 -1
- package/dist/ts4.2/src/e2ee/worker/ParticipantKeyHandler.d.ts +5 -6
- package/dist/ts4.2/src/room/RTCEngine.d.ts +2 -1
- package/dist/ts4.2/src/room/Room.d.ts +1 -0
- package/dist/ts4.2/src/room/events.d.ts +11 -1
- package/dist/ts4.2/src/version.d.ts +1 -1
- package/package.json +2 -2
- package/src/api/SignalClient.ts +10 -0
- package/src/e2ee/E2eeManager.ts +8 -12
- package/src/e2ee/KeyProvider.ts +13 -6
- package/src/e2ee/constants.ts +0 -1
- package/src/e2ee/events.ts +12 -3
- package/src/e2ee/types.ts +8 -3
- package/src/e2ee/utils.ts +1 -2
- package/src/e2ee/worker/FrameCryptor.ts +8 -4
- package/src/e2ee/worker/ParticipantKeyHandler.test.ts +104 -4
- package/src/e2ee/worker/ParticipantKeyHandler.ts +23 -26
- package/src/e2ee/worker/e2ee.worker.ts +9 -4
- package/src/room/RTCEngine.ts +7 -0
- package/src/room/Room.ts +27 -2
- package/src/room/events.ts +11 -0
- package/src/room/participant/LocalParticipant.ts +0 -5
- package/src/room/track/RemoteAudioTrack.ts +3 -2
- package/src/version.ts +1 -1
@@ -1,6 +1,6 @@
|
|
1
1
|
import type TypedEventEmitter from 'typed-emitter';
|
2
2
|
import type { ParticipantKeyHandlerCallbacks } from '../events';
|
3
|
-
import type { KeyProviderOptions, KeySet } from '../types';
|
3
|
+
import type { KeyProviderOptions, KeySet, RatchetResult } from '../types';
|
4
4
|
declare const ParticipantKeyHandler_base: new () => TypedEventEmitter<ParticipantKeyHandlerCallbacks>;
|
5
5
|
/**
|
6
6
|
* ParticipantKeyHandler is responsible for providing a cryptor instance with the
|
@@ -52,10 +52,9 @@ export declare class ParticipantKeyHandler extends ParticipantKeyHandler_base {
|
|
52
52
|
* returns the ratcheted material
|
53
53
|
* if `setKey` is true (default), it will also set the ratcheted key directly on the crypto key ring
|
54
54
|
* @param keyIndex
|
55
|
-
* @param setKey
|
56
|
-
* @param extractable allow key extraction (get the key in plaintext) on the ratcheted new key (default: false)
|
55
|
+
* @param setKey
|
57
56
|
*/
|
58
|
-
ratchetKey(keyIndex?: number, setKey?: boolean
|
57
|
+
ratchetKey(keyIndex?: number, setKey?: boolean): Promise<RatchetResult>;
|
59
58
|
/**
|
60
59
|
* takes in a key material with `deriveBits` and `deriveKey` set as key usages
|
61
60
|
* and derives encryption keys from the material and sets it on the key ring buffer
|
@@ -69,8 +68,8 @@ export declare class ParticipantKeyHandler extends ParticipantKeyHandler_base {
|
|
69
68
|
* together with the material
|
70
69
|
* also updates the currentKeyIndex
|
71
70
|
*/
|
72
|
-
setKeyFromMaterial(material: CryptoKey, keyIndex: number,
|
73
|
-
setKeySet(keySet: KeySet, keyIndex: number,
|
71
|
+
setKeyFromMaterial(material: CryptoKey, keyIndex: number, ratchetedResult?: RatchetResult | null): Promise<void>;
|
72
|
+
setKeySet(keySet: KeySet, keyIndex: number, ratchetedResult?: RatchetResult | null): void;
|
74
73
|
setCurrentKeyIndex(index: number): Promise<void>;
|
75
74
|
getCurrentKeyIndex(): number;
|
76
75
|
/**
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import type { AddTrackRequest, ConnectionQualityUpdate, JoinResponse, StreamStateUpdate, SubscriptionPermissionUpdate, SubscriptionResponse } from '@livekit/protocol';
|
2
|
-
import { DataPacket, DataPacket_Kind, DisconnectReason, ParticipantInfo, RequestResponse, Room as RoomModel, SpeakerInfo, SubscribedQualityUpdate, TrackInfo, TrackUnpublishedResponse, Transcription } from '@livekit/protocol';
|
2
|
+
import { DataPacket, DataPacket_Kind, DisconnectReason, ParticipantInfo, RequestResponse, Room as RoomModel, RoomMovedResponse, SpeakerInfo, SubscribedQualityUpdate, TrackInfo, TrackUnpublishedResponse, Transcription } from '@livekit/protocol';
|
3
3
|
import type TypedEventEmitter from 'typed-emitter';
|
4
4
|
import type { SignalOptions } from '../api/SignalClient';
|
5
5
|
import { SignalClient } from '../api/SignalClient';
|
@@ -153,6 +153,7 @@ export type EngineEventCallbacks = {
|
|
153
153
|
dcBufferStatusChanged: (isLow: boolean, kind: DataPacket_Kind) => void;
|
154
154
|
participantUpdate: (infos: ParticipantInfo[]) => void;
|
155
155
|
roomUpdate: (room: RoomModel) => void;
|
156
|
+
roomMoved: (room: RoomMovedResponse) => void;
|
156
157
|
connectionQualityUpdate: (update: ConnectionQualityUpdate) => void;
|
157
158
|
speakersChanged: (speakerUpdates: SpeakerInfo[]) => void;
|
158
159
|
streamStateChanged: (update: StreamStateUpdate) => void;
|
@@ -286,6 +286,7 @@ export type RoomEventCallbacks = {
|
|
286
286
|
reconnected: () => void;
|
287
287
|
disconnected: (reason?: DisconnectReason) => void;
|
288
288
|
connectionStateChanged: (state: ConnectionState) => void;
|
289
|
+
moved: (name: string, token: string) => void;
|
289
290
|
mediaDevicesChanged: () => void;
|
290
291
|
participantConnected: (participant: RemoteParticipant) => void;
|
291
292
|
participantDisconnected: (participant: RemoteParticipant) => void;
|
@@ -45,6 +45,15 @@ export declare enum RoomEvent {
|
|
45
45
|
* args: ([[ConnectionState]])
|
46
46
|
*/
|
47
47
|
ConnectionStateChanged = "connectionStateChanged",
|
48
|
+
/**
|
49
|
+
* When participant has been moved to a different room by the service request.
|
50
|
+
* The behavior looks like the participant has been disconnected and reconnected to a different room
|
51
|
+
* seamlessly without connection state transition.
|
52
|
+
* A new token will be provided for reconnecting to the new room if needed.
|
53
|
+
*
|
54
|
+
* args: ([[room: string, token: string]])
|
55
|
+
*/
|
56
|
+
Moved = "moved",
|
48
57
|
/**
|
49
58
|
* When input or output devices on the machine have changed.
|
50
59
|
*/
|
@@ -491,7 +500,8 @@ export declare enum EngineEvent {
|
|
491
500
|
LocalTrackSubscribed = "localTrackSubscribed",
|
492
501
|
Offline = "offline",
|
493
502
|
SignalRequestResponse = "signalRequestResponse",
|
494
|
-
SignalConnected = "signalConnected"
|
503
|
+
SignalConnected = "signalConnected",
|
504
|
+
RoomMoved = "roomMoved"
|
495
505
|
}
|
496
506
|
export declare enum TrackEvent {
|
497
507
|
Message = "message",
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "livekit-client",
|
3
|
-
"version": "2.
|
3
|
+
"version": "2.12.0",
|
4
4
|
"description": "JavaScript/TypeScript client SDK for LiveKit",
|
5
5
|
"main": "./dist/livekit-client.umd.js",
|
6
6
|
"unpkg": "./dist/livekit-client.umd.js",
|
@@ -37,7 +37,7 @@
|
|
37
37
|
"license": "Apache-2.0",
|
38
38
|
"dependencies": {
|
39
39
|
"@livekit/mutex": "1.1.1",
|
40
|
-
"@livekit/protocol": "1.
|
40
|
+
"@livekit/protocol": "1.38.0",
|
41
41
|
"events": "^3.3.0",
|
42
42
|
"loglevel": "^1.9.2",
|
43
43
|
"sdp-transform": "^2.15.0",
|
package/src/api/SignalClient.ts
CHANGED
@@ -15,6 +15,7 @@ import {
|
|
15
15
|
ReconnectResponse,
|
16
16
|
RequestResponse,
|
17
17
|
Room,
|
18
|
+
RoomMovedResponse,
|
18
19
|
SessionDescription,
|
19
20
|
SignalRequest,
|
20
21
|
SignalResponse,
|
@@ -148,6 +149,8 @@ export class SignalClient {
|
|
148
149
|
|
149
150
|
onLocalTrackSubscribed?: (trackSid: string) => void;
|
150
151
|
|
152
|
+
onRoomMoved?: (res: RoomMovedResponse) => void;
|
153
|
+
|
151
154
|
connectOptions?: ConnectOpts;
|
152
155
|
|
153
156
|
ws?: WebSocket;
|
@@ -774,6 +777,13 @@ export class SignalClient {
|
|
774
777
|
if (this.onLocalTrackSubscribed) {
|
775
778
|
this.onLocalTrackSubscribed(msg.value.trackSid);
|
776
779
|
}
|
780
|
+
} else if (msg.case === 'roomMoved') {
|
781
|
+
if (this.onTokenRefresh) {
|
782
|
+
this.onTokenRefresh(msg.value.token);
|
783
|
+
}
|
784
|
+
if (this.onRoomMoved) {
|
785
|
+
this.onRoomMoved(msg.value);
|
786
|
+
}
|
777
787
|
} else {
|
778
788
|
this.log.debug('unsupported message', { ...this.logContext, msgCase: msg.case });
|
779
789
|
}
|
package/src/e2ee/E2eeManager.ts
CHANGED
@@ -152,7 +152,12 @@ export class E2EEManager
|
|
152
152
|
}
|
153
153
|
break;
|
154
154
|
case 'ratchetKey':
|
155
|
-
this.keyProvider.emit(
|
155
|
+
this.keyProvider.emit(
|
156
|
+
KeyProviderEvent.KeyRatcheted,
|
157
|
+
data.ratchetResult,
|
158
|
+
data.participantIdentity,
|
159
|
+
data.keyIndex,
|
160
|
+
);
|
156
161
|
break;
|
157
162
|
default:
|
158
163
|
break;
|
@@ -222,19 +227,11 @@ export class E2EEManager
|
|
222
227
|
keyProvider
|
223
228
|
.on(KeyProviderEvent.SetKey, (keyInfo) => this.postKey(keyInfo))
|
224
229
|
.on(KeyProviderEvent.RatchetRequest, (participantId, keyIndex) =>
|
225
|
-
this.postRatchetRequest(
|
226
|
-
participantId,
|
227
|
-
keyIndex,
|
228
|
-
keyProvider.getOptions().allowKeyExtraction,
|
229
|
-
),
|
230
|
+
this.postRatchetRequest(participantId, keyIndex),
|
230
231
|
);
|
231
232
|
}
|
232
233
|
|
233
|
-
private postRatchetRequest(
|
234
|
-
participantIdentity?: string,
|
235
|
-
keyIndex?: number,
|
236
|
-
extractable?: boolean,
|
237
|
-
) {
|
234
|
+
private postRatchetRequest(participantIdentity?: string, keyIndex?: number) {
|
238
235
|
if (!this.worker) {
|
239
236
|
throw Error('could not ratchet key, worker is missing');
|
240
237
|
}
|
@@ -243,7 +240,6 @@ export class E2EEManager
|
|
243
240
|
data: {
|
244
241
|
participantIdentity: participantIdentity,
|
245
242
|
keyIndex,
|
246
|
-
extractable,
|
247
243
|
},
|
248
244
|
};
|
249
245
|
this.worker.postMessage(msg);
|
package/src/e2ee/KeyProvider.ts
CHANGED
@@ -3,7 +3,7 @@ import type TypedEventEmitter from 'typed-emitter';
|
|
3
3
|
import log from '../logger';
|
4
4
|
import { KEY_PROVIDER_DEFAULTS } from './constants';
|
5
5
|
import { type KeyProviderCallbacks, KeyProviderEvent } from './events';
|
6
|
-
import type { KeyInfo, KeyProviderOptions } from './types';
|
6
|
+
import type { KeyInfo, KeyProviderOptions, RatchetResult } from './types';
|
7
7
|
import { createKeyMaterialFromBuffer, createKeyMaterialFromString } from './utils';
|
8
8
|
|
9
9
|
/**
|
@@ -39,13 +39,20 @@ export class BaseKeyProvider extends (EventEmitter as new () => TypedEventEmitte
|
|
39
39
|
}
|
40
40
|
|
41
41
|
/**
|
42
|
-
*
|
43
|
-
*
|
44
|
-
*
|
42
|
+
* Callback being invoked after a key has been ratcheted.
|
43
|
+
* Can happen when:
|
44
|
+
* - A decryption failure occurs and the key is auto-ratcheted
|
45
|
+
* - A ratchet request is sent (see {@link ratchetKey()})
|
46
|
+
* @param ratchetResult Contains the ratcheted chain key (exportable to other participants) and the derived new key material.
|
47
|
+
* @param participantId
|
45
48
|
* @param keyIndex
|
46
49
|
*/
|
47
|
-
protected onKeyRatcheted = (
|
48
|
-
|
50
|
+
protected onKeyRatcheted = (
|
51
|
+
ratchetResult: RatchetResult,
|
52
|
+
participantId?: string,
|
53
|
+
keyIndex?: number,
|
54
|
+
) => {
|
55
|
+
log.debug('key ratcheted event received', { ratchetResult, participantId, keyIndex });
|
49
56
|
};
|
50
57
|
|
51
58
|
getKeys() {
|
package/src/e2ee/constants.ts
CHANGED
package/src/e2ee/events.ts
CHANGED
@@ -1,26 +1,35 @@
|
|
1
1
|
import type Participant from '../room/participant/Participant';
|
2
2
|
import type { CryptorError } from './errors';
|
3
|
-
import type { KeyInfo } from './types';
|
3
|
+
import type { KeyInfo, RatchetResult } from './types';
|
4
4
|
|
5
5
|
export enum KeyProviderEvent {
|
6
6
|
SetKey = 'setKey',
|
7
|
+
/** Event for requesting to ratchet the key used to encrypt the stream */
|
7
8
|
RatchetRequest = 'ratchetRequest',
|
9
|
+
/** Emitted when a key is ratcheted. Could be after auto-ratcheting on decryption failure or
|
10
|
+
* following a `RatchetRequest`, will contain the ratcheted key material */
|
8
11
|
KeyRatcheted = 'keyRatcheted',
|
9
12
|
}
|
10
13
|
|
11
14
|
export type KeyProviderCallbacks = {
|
12
15
|
[KeyProviderEvent.SetKey]: (keyInfo: KeyInfo) => void;
|
13
16
|
[KeyProviderEvent.RatchetRequest]: (participantIdentity?: string, keyIndex?: number) => void;
|
14
|
-
[KeyProviderEvent.KeyRatcheted]: (
|
17
|
+
[KeyProviderEvent.KeyRatcheted]: (
|
18
|
+
ratchetedResult: RatchetResult,
|
19
|
+
participantIdentity?: string,
|
20
|
+
keyIndex?: number,
|
21
|
+
) => void;
|
15
22
|
};
|
16
23
|
|
17
24
|
export enum KeyHandlerEvent {
|
25
|
+
/** Emitted when a key has been ratcheted. Is emitted when any key has been ratcheted
|
26
|
+
* i.e. when the FrameCryptor tried to ratchet when decryption is failing */
|
18
27
|
KeyRatcheted = 'keyRatcheted',
|
19
28
|
}
|
20
29
|
|
21
30
|
export type ParticipantKeyHandlerCallbacks = {
|
22
31
|
[KeyHandlerEvent.KeyRatcheted]: (
|
23
|
-
|
32
|
+
ratchetResult: RatchetResult,
|
24
33
|
participantIdentity: string,
|
25
34
|
keyIndex?: number,
|
26
35
|
) => void;
|
package/src/e2ee/types.ts
CHANGED
@@ -74,7 +74,6 @@ export interface RatchetRequestMessage extends BaseMessage {
|
|
74
74
|
data: {
|
75
75
|
participantIdentity?: string;
|
76
76
|
keyIndex?: number;
|
77
|
-
extractable?: boolean;
|
78
77
|
};
|
79
78
|
}
|
80
79
|
|
@@ -83,7 +82,7 @@ export interface RatchetMessage extends BaseMessage {
|
|
83
82
|
data: {
|
84
83
|
participantIdentity: string;
|
85
84
|
keyIndex?: number;
|
86
|
-
|
85
|
+
ratchetResult: RatchetResult;
|
87
86
|
};
|
88
87
|
}
|
89
88
|
|
@@ -125,13 +124,19 @@ export type E2EEWorkerMessage =
|
|
125
124
|
|
126
125
|
export type KeySet = { material: CryptoKey; encryptionKey: CryptoKey };
|
127
126
|
|
127
|
+
export type RatchetResult = {
|
128
|
+
// The ratchet chain key, which is used to derive the next key.
|
129
|
+
// Can be shared/exported to other participants.
|
130
|
+
chainKey: ArrayBuffer;
|
131
|
+
cryptoKey: CryptoKey;
|
132
|
+
};
|
133
|
+
|
128
134
|
export type KeyProviderOptions = {
|
129
135
|
sharedKey: boolean;
|
130
136
|
ratchetSalt: string;
|
131
137
|
ratchetWindowSize: number;
|
132
138
|
failureTolerance: number;
|
133
139
|
keyringSize: number;
|
134
|
-
allowKeyExtraction: boolean;
|
135
140
|
};
|
136
141
|
|
137
142
|
export type KeyInfo = {
|
package/src/e2ee/utils.ts
CHANGED
@@ -27,14 +27,13 @@ export async function importKey(
|
|
27
27
|
keyBytes: Uint8Array | ArrayBuffer,
|
28
28
|
algorithm: string | { name: string } = { name: ENCRYPTION_ALGORITHM },
|
29
29
|
usage: 'derive' | 'encrypt' = 'encrypt',
|
30
|
-
extractable: boolean = false,
|
31
30
|
) {
|
32
31
|
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
|
33
32
|
return crypto.subtle.importKey(
|
34
33
|
'raw',
|
35
34
|
keyBytes,
|
36
35
|
algorithm,
|
37
|
-
|
36
|
+
false,
|
38
37
|
usage === 'derive' ? ['deriveBits', 'deriveKey'] : ['encrypt', 'decrypt'],
|
39
38
|
);
|
40
39
|
}
|
@@ -7,7 +7,7 @@ import type { VideoCodec } from '../../room/track/options';
|
|
7
7
|
import { ENCRYPTION_ALGORITHM, IV_LENGTH, UNENCRYPTED_BYTES } from '../constants';
|
8
8
|
import { CryptorError, CryptorErrorReason } from '../errors';
|
9
9
|
import { type CryptorCallbacks, CryptorEvent } from '../events';
|
10
|
-
import type { DecodeRatchetOptions, KeyProviderOptions, KeySet } from '../types';
|
10
|
+
import type { DecodeRatchetOptions, KeyProviderOptions, KeySet, RatchetResult } from '../types';
|
11
11
|
import { deriveKeys, isVideoFrame, needsRbspUnescaping, parseRbsp, writeRbsp } from '../utils';
|
12
12
|
import type { ParticipantKeyHandler } from './ParticipantKeyHandler';
|
13
13
|
import { SifGuard } from './SifGuard';
|
@@ -477,12 +477,16 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
477
477
|
);
|
478
478
|
|
479
479
|
let ratchetedKeySet: KeySet | undefined;
|
480
|
+
let ratchetResult: RatchetResult | undefined;
|
480
481
|
if ((initialMaterial ?? keySet) === this.keys.getKeySet(keyIndex)) {
|
481
482
|
// only ratchet if the currently set key is still the same as the one used to decrypt this frame
|
482
483
|
// if not, it might be that a different frame has already ratcheted and we try with that one first
|
483
|
-
|
484
|
+
ratchetResult = await this.keys.ratchetKey(keyIndex, false);
|
484
485
|
|
485
|
-
ratchetedKeySet = await deriveKeys(
|
486
|
+
ratchetedKeySet = await deriveKeys(
|
487
|
+
ratchetResult.cryptoKey,
|
488
|
+
this.keyProviderOptions.ratchetSalt,
|
489
|
+
);
|
486
490
|
}
|
487
491
|
|
488
492
|
const frame = await this.decryptFrame(encodedFrame, keyIndex, initialMaterial || keySet, {
|
@@ -493,7 +497,7 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
493
497
|
// before updating the keys, make sure that the keySet used for this frame is still the same as the currently set key
|
494
498
|
// if it's not, a new key might have been set already, which we don't want to override
|
495
499
|
if ((initialMaterial ?? keySet) === this.keys.getKeySet(keyIndex)) {
|
496
|
-
this.keys.setKeySet(ratchetedKeySet, keyIndex,
|
500
|
+
this.keys.setKeySet(ratchetedKeySet, keyIndex, ratchetResult);
|
497
501
|
// decryption was successful, set the new key index to reflect the ratcheted key set
|
498
502
|
this.keys.setCurrentKeyIndex(keyIndex);
|
499
503
|
}
|
@@ -1,7 +1,7 @@
|
|
1
|
-
import { describe, expect, it, vitest } from 'vitest';
|
1
|
+
import { describe, expect, it, test, vitest } from 'vitest';
|
2
2
|
import { ENCRYPTION_ALGORITHM, KEY_PROVIDER_DEFAULTS } from '../constants';
|
3
3
|
import { KeyHandlerEvent } from '../events';
|
4
|
-
import { createKeyMaterialFromString } from '../utils';
|
4
|
+
import { createKeyMaterialFromString, importKey } from '../utils';
|
5
5
|
import { ParticipantKeyHandler } from './ParticipantKeyHandler';
|
6
6
|
|
7
7
|
describe('ParticipantKeyHandler', () => {
|
@@ -239,11 +239,18 @@ describe('ParticipantKeyHandler', () => {
|
|
239
239
|
|
240
240
|
await keyHandler.setKey(material);
|
241
241
|
|
242
|
-
await keyHandler.ratchetKey();
|
242
|
+
const ratchetResult = await keyHandler.ratchetKey();
|
243
243
|
|
244
244
|
const newMaterial = keyHandler.getKeySet()?.material;
|
245
245
|
|
246
|
-
expect(keyRatched).toHaveBeenCalledWith(
|
246
|
+
expect(keyRatched).toHaveBeenCalledWith(
|
247
|
+
{
|
248
|
+
chainKey: ratchetResult.chainKey,
|
249
|
+
cryptoKey: newMaterial,
|
250
|
+
},
|
251
|
+
participantIdentity,
|
252
|
+
0,
|
253
|
+
);
|
247
254
|
});
|
248
255
|
|
249
256
|
it('ratchets keys predictably', async () => {
|
@@ -283,4 +290,97 @@ describe('ParticipantKeyHandler', () => {
|
|
283
290
|
expect(ciphertexts).matchSnapshot('ciphertexts');
|
284
291
|
});
|
285
292
|
});
|
293
|
+
|
294
|
+
describe(`E2EE Ratcheting`, () => {
|
295
|
+
test('Should be possible to share ratcheted material to remote participant', async () => {
|
296
|
+
const senderKeyHandler = new ParticipantKeyHandler('test-sender', KEY_PROVIDER_DEFAULTS);
|
297
|
+
// Initial key
|
298
|
+
const initialMaterial = new Uint8Array(32);
|
299
|
+
crypto.getRandomValues(initialMaterial);
|
300
|
+
const rootMaterial = await importKey(initialMaterial, 'HKDF', 'derive');
|
301
|
+
await senderKeyHandler.setKeyFromMaterial(rootMaterial, 0);
|
302
|
+
|
303
|
+
const iv = new Uint8Array(12);
|
304
|
+
crypto.getRandomValues(iv);
|
305
|
+
|
306
|
+
const firstMessagePreRatchet = new TextEncoder().encode(
|
307
|
+
'Hello world, this is the first message',
|
308
|
+
);
|
309
|
+
const firstCipherText = await encrypt(senderKeyHandler, 0, iv, firstMessagePreRatchet);
|
310
|
+
|
311
|
+
let ratchetBufferResolve: (key: ArrayBuffer) => void;
|
312
|
+
const expectEmitted = new Promise<ArrayBuffer>(async (resolve) => {
|
313
|
+
ratchetBufferResolve = resolve;
|
314
|
+
});
|
315
|
+
|
316
|
+
senderKeyHandler.on(KeyHandlerEvent.KeyRatcheted, (material, identity, keyIndex) => {
|
317
|
+
expect(identity).toEqual('test-sender');
|
318
|
+
expect(keyIndex).toEqual(0);
|
319
|
+
ratchetBufferResolve(material.chainKey);
|
320
|
+
});
|
321
|
+
|
322
|
+
const currentKeyIndex = senderKeyHandler.getCurrentKeyIndex();
|
323
|
+
const ratchetResult = await senderKeyHandler.ratchetKey(currentKeyIndex, true);
|
324
|
+
|
325
|
+
// Notice that ratchetedKeySet is not exportable, so we cannot share it out-of-band.
|
326
|
+
// This is a limitation of webcrypto for KDFs keys, they cannot be exported.
|
327
|
+
expect(ratchetResult.cryptoKey.extractable).toBe(false);
|
328
|
+
|
329
|
+
const ratchetedMaterial = await expectEmitted;
|
330
|
+
|
331
|
+
// The ratcheted material can be sent out-of-band to new participants. And they
|
332
|
+
// should be able to generate the same keyMaterial
|
333
|
+
|
334
|
+
const generatedMaterial = await importKey(ratchetedMaterial, 'HKDF', 'derive');
|
335
|
+
const receiverKeyHandler = new ParticipantKeyHandler('test-receiver', KEY_PROVIDER_DEFAULTS);
|
336
|
+
await receiverKeyHandler.setKeyFromMaterial(generatedMaterial, 0);
|
337
|
+
|
338
|
+
// Now sender should be able to encrypt to recipient
|
339
|
+
|
340
|
+
const plainText = new TextEncoder().encode('Hello world, this is a test message');
|
341
|
+
|
342
|
+
const cipherText = await encrypt(senderKeyHandler, 0, iv, plainText);
|
343
|
+
|
344
|
+
const clearTextBuffer = await decrypt(receiverKeyHandler, 0, iv, cipherText);
|
345
|
+
|
346
|
+
const clearText = new Uint8Array(clearTextBuffer);
|
347
|
+
expect(clearText).toEqual(plainText);
|
348
|
+
|
349
|
+
// The receiver should not be able to decrypt the first message
|
350
|
+
const decryptPromise = decrypt(receiverKeyHandler, 0, iv, firstCipherText);
|
351
|
+
await expect(decryptPromise).rejects.toThrowError();
|
352
|
+
});
|
353
|
+
|
354
|
+
async function encrypt(
|
355
|
+
participantKeyHandler: ParticipantKeyHandler,
|
356
|
+
keyIndex: number,
|
357
|
+
iv: Uint8Array,
|
358
|
+
data: Uint8Array,
|
359
|
+
): Promise<ArrayBuffer> {
|
360
|
+
return crypto.subtle.encrypt(
|
361
|
+
{
|
362
|
+
name: ENCRYPTION_ALGORITHM,
|
363
|
+
iv,
|
364
|
+
},
|
365
|
+
participantKeyHandler.getKeySet(keyIndex)!.encryptionKey,
|
366
|
+
data,
|
367
|
+
);
|
368
|
+
}
|
369
|
+
|
370
|
+
async function decrypt(
|
371
|
+
participantKeyHandler: ParticipantKeyHandler,
|
372
|
+
keyIndex: number,
|
373
|
+
iv: Uint8Array,
|
374
|
+
cipherText: ArrayBuffer,
|
375
|
+
): Promise<ArrayBuffer> {
|
376
|
+
return crypto.subtle.decrypt(
|
377
|
+
{
|
378
|
+
name: ENCRYPTION_ALGORITHM,
|
379
|
+
iv,
|
380
|
+
},
|
381
|
+
participantKeyHandler.getKeySet(keyIndex)!.encryptionKey,
|
382
|
+
cipherText,
|
383
|
+
);
|
384
|
+
}
|
385
|
+
});
|
286
386
|
});
|
@@ -2,7 +2,7 @@ import { EventEmitter } from 'events';
|
|
2
2
|
import type TypedEventEmitter from 'typed-emitter';
|
3
3
|
import { workerLogger } from '../../logger';
|
4
4
|
import { KeyHandlerEvent, type ParticipantKeyHandlerCallbacks } from '../events';
|
5
|
-
import type { KeyProviderOptions, KeySet } from '../types';
|
5
|
+
import type { KeyProviderOptions, KeySet, RatchetResult } from '../types';
|
6
6
|
import { deriveKeys, importKey, ratchet } from '../utils';
|
7
7
|
|
8
8
|
// TODO ParticipantKeyHandlers currently don't get destroyed on participant disconnect
|
@@ -25,7 +25,7 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
|
|
25
25
|
|
26
26
|
private keyProviderOptions: KeyProviderOptions;
|
27
27
|
|
28
|
-
private ratchetPromiseMap: Map<number, Promise<
|
28
|
+
private ratchetPromiseMap: Map<number, Promise<RatchetResult>>;
|
29
29
|
|
30
30
|
private participantIdentity: string;
|
31
31
|
|
@@ -108,17 +108,16 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
|
|
108
108
|
* returns the ratcheted material
|
109
109
|
* if `setKey` is true (default), it will also set the ratcheted key directly on the crypto key ring
|
110
110
|
* @param keyIndex
|
111
|
-
* @param setKey
|
112
|
-
* @param extractable allow key extraction (get the key in plaintext) on the ratcheted new key (default: false)
|
111
|
+
* @param setKey
|
113
112
|
*/
|
114
|
-
ratchetKey(keyIndex?: number, setKey = true
|
113
|
+
ratchetKey(keyIndex?: number, setKey = true): Promise<RatchetResult> {
|
115
114
|
const currentKeyIndex = keyIndex ?? this.getCurrentKeyIndex();
|
116
115
|
|
117
116
|
const existingPromise = this.ratchetPromiseMap.get(currentKeyIndex);
|
118
117
|
if (typeof existingPromise !== 'undefined') {
|
119
118
|
return existingPromise;
|
120
119
|
}
|
121
|
-
const ratchetPromise = new Promise<
|
120
|
+
const ratchetPromise = new Promise<RatchetResult>(async (resolve, reject) => {
|
122
121
|
try {
|
123
122
|
const keySet = this.getKeySet(currentKeyIndex);
|
124
123
|
if (!keySet) {
|
@@ -127,23 +126,17 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
|
|
127
126
|
);
|
128
127
|
}
|
129
128
|
const currentMaterial = keySet.material;
|
130
|
-
const
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
129
|
+
const chainKey = await ratchet(currentMaterial, this.keyProviderOptions.ratchetSalt);
|
130
|
+
const newMaterial = await importKey(chainKey, currentMaterial.algorithm.name, 'derive');
|
131
|
+
const ratchetResult: RatchetResult = {
|
132
|
+
chainKey,
|
133
|
+
cryptoKey: newMaterial,
|
134
|
+
};
|
137
135
|
if (setKey) {
|
138
|
-
|
139
|
-
this.
|
140
|
-
KeyHandlerEvent.KeyRatcheted,
|
141
|
-
newMaterial,
|
142
|
-
this.participantIdentity,
|
143
|
-
currentKeyIndex,
|
144
|
-
);
|
136
|
+
// Set the new key and emit a ratchet event with the ratcheted chain key
|
137
|
+
await this.setKeyFromMaterial(newMaterial, currentKeyIndex, ratchetResult);
|
145
138
|
}
|
146
|
-
resolve(
|
139
|
+
resolve(ratchetResult);
|
147
140
|
} catch (e) {
|
148
141
|
reject(e);
|
149
142
|
} finally {
|
@@ -171,7 +164,11 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
|
|
171
164
|
* together with the material
|
172
165
|
* also updates the currentKeyIndex
|
173
166
|
*/
|
174
|
-
async setKeyFromMaterial(
|
167
|
+
async setKeyFromMaterial(
|
168
|
+
material: CryptoKey,
|
169
|
+
keyIndex: number,
|
170
|
+
ratchetedResult: RatchetResult | null = null,
|
171
|
+
) {
|
175
172
|
const keySet = await deriveKeys(material, this.keyProviderOptions.ratchetSalt);
|
176
173
|
const newIndex = keyIndex >= 0 ? keyIndex % this.cryptoKeyRing.length : this.currentKeyIndex;
|
177
174
|
workerLogger.debug(`setting new key with index ${keyIndex}`, {
|
@@ -179,15 +176,15 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
|
|
179
176
|
algorithm: material.algorithm,
|
180
177
|
ratchetSalt: this.keyProviderOptions.ratchetSalt,
|
181
178
|
});
|
182
|
-
this.setKeySet(keySet, newIndex,
|
179
|
+
this.setKeySet(keySet, newIndex, ratchetedResult);
|
183
180
|
if (newIndex >= 0) this.currentKeyIndex = newIndex;
|
184
181
|
}
|
185
182
|
|
186
|
-
setKeySet(keySet: KeySet, keyIndex: number,
|
183
|
+
setKeySet(keySet: KeySet, keyIndex: number, ratchetedResult: RatchetResult | null = null) {
|
187
184
|
this.cryptoKeyRing[keyIndex % this.cryptoKeyRing.length] = keySet;
|
188
185
|
|
189
|
-
if (
|
190
|
-
this.emit(KeyHandlerEvent.KeyRatcheted,
|
186
|
+
if (ratchetedResult) {
|
187
|
+
this.emit(KeyHandlerEvent.KeyRatcheted, ratchetedResult, this.participantIdentity, keyIndex);
|
191
188
|
}
|
192
189
|
}
|
193
190
|
|
@@ -11,6 +11,7 @@ import type {
|
|
11
11
|
KeyProviderOptions,
|
12
12
|
RatchetMessage,
|
13
13
|
RatchetRequestMessage,
|
14
|
+
RatchetResult,
|
14
15
|
} from '../types';
|
15
16
|
import { FrameCryptor, encryptionEnabledMap } from './FrameCryptor';
|
16
17
|
import { ParticipantKeyHandler } from './ParticipantKeyHandler';
|
@@ -119,11 +120,11 @@ onmessage = (ev) => {
|
|
119
120
|
async function handleRatchetRequest(data: RatchetRequestMessage['data']) {
|
120
121
|
if (useSharedKey) {
|
121
122
|
const keyHandler = getSharedKeyHandler();
|
122
|
-
await keyHandler.ratchetKey(data.keyIndex
|
123
|
+
await keyHandler.ratchetKey(data.keyIndex);
|
123
124
|
keyHandler.resetKeyStatus();
|
124
125
|
} else if (data.participantIdentity) {
|
125
126
|
const keyHandler = getParticipantKeyHandler(data.participantIdentity);
|
126
|
-
await keyHandler.ratchetKey(data.keyIndex
|
127
|
+
await keyHandler.ratchetKey(data.keyIndex);
|
127
128
|
keyHandler.resetKeyStatus();
|
128
129
|
} else {
|
129
130
|
workerLogger.error(
|
@@ -229,13 +230,17 @@ function setupCryptorErrorEvents(cryptor: FrameCryptor) {
|
|
229
230
|
});
|
230
231
|
}
|
231
232
|
|
232
|
-
function emitRatchetedKeys(
|
233
|
+
function emitRatchetedKeys(
|
234
|
+
ratchetResult: RatchetResult,
|
235
|
+
participantIdentity: string,
|
236
|
+
keyIndex?: number,
|
237
|
+
) {
|
233
238
|
const msg: RatchetMessage = {
|
234
239
|
kind: `ratchetKey`,
|
235
240
|
data: {
|
236
241
|
participantIdentity,
|
237
242
|
keyIndex,
|
238
|
-
|
243
|
+
ratchetResult,
|
239
244
|
},
|
240
245
|
};
|
241
246
|
postMessage(msg);
|
package/src/room/RTCEngine.ts
CHANGED
@@ -16,6 +16,7 @@ import {
|
|
16
16
|
type ReconnectResponse,
|
17
17
|
RequestResponse,
|
18
18
|
Room as RoomModel,
|
19
|
+
RoomMovedResponse,
|
19
20
|
RpcAck,
|
20
21
|
RpcResponse,
|
21
22
|
SignalTarget,
|
@@ -529,6 +530,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
529
530
|
this.emit(EngineEvent.SubscribedQualityUpdate, update);
|
530
531
|
};
|
531
532
|
|
533
|
+
this.client.onRoomMoved = (res: RoomMovedResponse) => {
|
534
|
+
this.participantSid = res.participant?.sid;
|
535
|
+
this.emit(EngineEvent.RoomMoved, res);
|
536
|
+
};
|
537
|
+
|
532
538
|
this.client.onClose = () => {
|
533
539
|
this.handleDisconnect('signal', ReconnectReason.RR_SIGNAL_DISCONNECTED);
|
534
540
|
};
|
@@ -1493,6 +1499,7 @@ export type EngineEventCallbacks = {
|
|
1493
1499
|
dcBufferStatusChanged: (isLow: boolean, kind: DataPacket_Kind) => void;
|
1494
1500
|
participantUpdate: (infos: ParticipantInfo[]) => void;
|
1495
1501
|
roomUpdate: (room: RoomModel) => void;
|
1502
|
+
roomMoved: (room: RoomMovedResponse) => void;
|
1496
1503
|
connectionQualityUpdate: (update: ConnectionQualityUpdate) => void;
|
1497
1504
|
speakersChanged: (speakerUpdates: SpeakerInfo[]) => void;
|
1498
1505
|
streamStateChanged: (update: StreamStateUpdate) => void;
|