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.
Files changed (60) hide show
  1. package/dist/livekit-client.e2ee.worker.js +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs +30 -23
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +126 -27
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.umd.js +1 -1
  8. package/dist/livekit-client.umd.js.map +1 -1
  9. package/dist/src/api/SignalClient.d.ts +2 -1
  10. package/dist/src/api/SignalClient.d.ts.map +1 -1
  11. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  12. package/dist/src/e2ee/KeyProvider.d.ts +8 -5
  13. package/dist/src/e2ee/KeyProvider.d.ts.map +1 -1
  14. package/dist/src/e2ee/constants.d.ts.map +1 -1
  15. package/dist/src/e2ee/events.d.ts +8 -3
  16. package/dist/src/e2ee/events.d.ts.map +1 -1
  17. package/dist/src/e2ee/types.d.ts +5 -3
  18. package/dist/src/e2ee/types.d.ts.map +1 -1
  19. package/dist/src/e2ee/utils.d.ts +1 -1
  20. package/dist/src/e2ee/utils.d.ts.map +1 -1
  21. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  22. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +5 -6
  23. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
  24. package/dist/src/room/RTCEngine.d.ts +2 -1
  25. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  26. package/dist/src/room/Room.d.ts +1 -0
  27. package/dist/src/room/Room.d.ts.map +1 -1
  28. package/dist/src/room/events.d.ts +11 -1
  29. package/dist/src/room/events.d.ts.map +1 -1
  30. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  31. package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -1
  32. package/dist/src/version.d.ts +1 -1
  33. package/dist/ts4.2/src/api/SignalClient.d.ts +2 -1
  34. package/dist/ts4.2/src/e2ee/KeyProvider.d.ts +8 -5
  35. package/dist/ts4.2/src/e2ee/events.d.ts +8 -3
  36. package/dist/ts4.2/src/e2ee/types.d.ts +5 -3
  37. package/dist/ts4.2/src/e2ee/utils.d.ts +1 -1
  38. package/dist/ts4.2/src/e2ee/worker/ParticipantKeyHandler.d.ts +5 -6
  39. package/dist/ts4.2/src/room/RTCEngine.d.ts +2 -1
  40. package/dist/ts4.2/src/room/Room.d.ts +1 -0
  41. package/dist/ts4.2/src/room/events.d.ts +11 -1
  42. package/dist/ts4.2/src/version.d.ts +1 -1
  43. package/package.json +2 -2
  44. package/src/api/SignalClient.ts +10 -0
  45. package/src/e2ee/E2eeManager.ts +8 -12
  46. package/src/e2ee/KeyProvider.ts +13 -6
  47. package/src/e2ee/constants.ts +0 -1
  48. package/src/e2ee/events.ts +12 -3
  49. package/src/e2ee/types.ts +8 -3
  50. package/src/e2ee/utils.ts +1 -2
  51. package/src/e2ee/worker/FrameCryptor.ts +8 -4
  52. package/src/e2ee/worker/ParticipantKeyHandler.test.ts +104 -4
  53. package/src/e2ee/worker/ParticipantKeyHandler.ts +23 -26
  54. package/src/e2ee/worker/e2ee.worker.ts +9 -4
  55. package/src/room/RTCEngine.ts +7 -0
  56. package/src/room/Room.ts +27 -2
  57. package/src/room/events.ts +11 -0
  58. package/src/room/participant/LocalParticipant.ts +0 -5
  59. package/src/room/track/RemoteAudioTrack.ts +3 -2
  60. 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 set the new key. Will emit KeyHandlerEvent.KeyRatcheted after key generation (default: true)
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, extractable?: boolean): Promise<CryptoKey>;
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, emitRatchetEvent?: boolean): Promise<void>;
73
- setKeySet(keySet: KeySet, keyIndex: number, emitRatchetEvent?: boolean): void;
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",
@@ -1,3 +1,3 @@
1
1
  export declare const version: string;
2
- export declare const protocolVersion = 15;
2
+ export declare const protocolVersion = 16;
3
3
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livekit-client",
3
- "version": "2.11.3",
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.36.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",
@@ -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
  }
@@ -152,7 +152,12 @@ export class E2EEManager
152
152
  }
153
153
  break;
154
154
  case 'ratchetKey':
155
- this.keyProvider.emit(KeyProviderEvent.KeyRatcheted, data.material, data.keyIndex);
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);
@@ -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
- * callback being invoked after a ratchet request has been performed on a participant
43
- * that surfaces the new key material.
44
- * @param material
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 = (material: CryptoKey, keyIndex?: number) => {
48
- log.debug('key ratcheted event received', { material, keyIndex });
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() {
@@ -37,7 +37,6 @@ export const KEY_PROVIDER_DEFAULTS: KeyProviderOptions = {
37
37
  ratchetWindowSize: 8,
38
38
  failureTolerance: DECRYPTION_FAILURE_TOLERANCE,
39
39
  keyringSize: 16,
40
- allowKeyExtraction: false,
41
40
  } as const;
42
41
 
43
42
  export const MAX_SIF_COUNT = 100;
@@ -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]: (material: CryptoKey, keyIndex?: number) => void;
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
- material: CryptoKey,
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
- material: CryptoKey;
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
- extractable,
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
- const newMaterial = await this.keys.ratchetKey(keyIndex, false);
484
+ ratchetResult = await this.keys.ratchetKey(keyIndex, false);
484
485
 
485
- ratchetedKeySet = await deriveKeys(newMaterial, this.keyProviderOptions.ratchetSalt);
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, true);
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(newMaterial, participantIdentity, 0);
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<CryptoKey>>;
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 set the new key. Will emit KeyHandlerEvent.KeyRatcheted after key generation (default: true)
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, extractable = false): Promise<CryptoKey> {
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<CryptoKey>(async (resolve, reject) => {
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 newMaterial = await importKey(
131
- await ratchet(currentMaterial, this.keyProviderOptions.ratchetSalt),
132
- currentMaterial.algorithm.name,
133
- 'derive',
134
- extractable,
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
- await this.setKeyFromMaterial(newMaterial, currentKeyIndex, true);
139
- this.emit(
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(newMaterial);
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(material: CryptoKey, keyIndex: number, emitRatchetEvent = false) {
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, emitRatchetEvent);
179
+ this.setKeySet(keySet, newIndex, ratchetedResult);
183
180
  if (newIndex >= 0) this.currentKeyIndex = newIndex;
184
181
  }
185
182
 
186
- setKeySet(keySet: KeySet, keyIndex: number, emitRatchetEvent = false) {
183
+ setKeySet(keySet: KeySet, keyIndex: number, ratchetedResult: RatchetResult | null = null) {
187
184
  this.cryptoKeyRing[keyIndex % this.cryptoKeyRing.length] = keySet;
188
185
 
189
- if (emitRatchetEvent) {
190
- this.emit(KeyHandlerEvent.KeyRatcheted, keySet.material, this.participantIdentity, keyIndex);
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, true, data.extractable);
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, true, data.extractable);
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(material: CryptoKey, participantIdentity: string, keyIndex?: number) {
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
- material,
243
+ ratchetResult,
239
244
  },
240
245
  };
241
246
  postMessage(msg);
@@ -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;