livekit-client 1.11.4 → 1.12.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (134) hide show
  1. package/README.md +13 -1
  2. package/dist/livekit-client.e2ee.worker.js +2 -0
  3. package/dist/livekit-client.e2ee.worker.js.map +1 -0
  4. package/dist/livekit-client.e2ee.worker.mjs +1545 -0
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -0
  6. package/dist/livekit-client.esm.mjs +4786 -4065
  7. package/dist/livekit-client.esm.mjs.map +1 -1
  8. package/dist/livekit-client.umd.js +1 -1
  9. package/dist/livekit-client.umd.js.map +1 -1
  10. package/dist/src/api/SignalClient.d.ts +4 -1
  11. package/dist/src/api/SignalClient.d.ts.map +1 -1
  12. package/dist/src/connectionHelper/checks/turn.d.ts.map +1 -1
  13. package/dist/src/connectionHelper/checks/websocket.d.ts.map +1 -1
  14. package/dist/src/e2ee/E2eeManager.d.ts +45 -0
  15. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -0
  16. package/dist/src/e2ee/KeyProvider.d.ts +42 -0
  17. package/dist/src/e2ee/KeyProvider.d.ts.map +1 -0
  18. package/dist/src/e2ee/constants.d.ts +14 -0
  19. package/dist/src/e2ee/constants.d.ts.map +1 -0
  20. package/dist/src/e2ee/errors.d.ts +11 -0
  21. package/dist/src/e2ee/errors.d.ts.map +1 -0
  22. package/dist/src/e2ee/index.d.ts +4 -0
  23. package/dist/src/e2ee/index.d.ts.map +1 -0
  24. package/dist/src/e2ee/types.d.ts +129 -0
  25. package/dist/src/e2ee/types.d.ts.map +1 -0
  26. package/dist/src/e2ee/utils.d.ts +24 -0
  27. package/dist/src/e2ee/utils.d.ts.map +1 -0
  28. package/dist/src/e2ee/worker/FrameCryptor.d.ts +174 -0
  29. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -0
  30. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +54 -0
  31. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -0
  32. package/dist/src/e2ee/worker/e2ee.worker.d.ts +2 -0
  33. package/dist/src/e2ee/worker/e2ee.worker.d.ts.map +1 -0
  34. package/dist/src/index.d.ts +1 -0
  35. package/dist/src/index.d.ts.map +1 -1
  36. package/dist/src/logger.d.ts +4 -1
  37. package/dist/src/logger.d.ts.map +1 -1
  38. package/dist/src/options.d.ts +5 -0
  39. package/dist/src/options.d.ts.map +1 -1
  40. package/dist/src/proto/livekit_models.d.ts +2 -2
  41. package/dist/src/proto/livekit_models.d.ts.map +1 -1
  42. package/dist/src/room/PCTransport.d.ts +3 -1
  43. package/dist/src/room/PCTransport.d.ts.map +1 -1
  44. package/dist/src/room/RTCEngine.d.ts +17 -3
  45. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  46. package/dist/src/room/Room.d.ts +10 -0
  47. package/dist/src/room/Room.d.ts.map +1 -1
  48. package/dist/src/room/events.d.ts +14 -2
  49. package/dist/src/room/events.d.ts.map +1 -1
  50. package/dist/src/room/participant/LocalParticipant.d.ts +7 -2
  51. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  52. package/dist/src/room/participant/Participant.d.ts +1 -0
  53. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  54. package/dist/src/room/participant/RemoteParticipant.d.ts +6 -4
  55. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  56. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  57. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  58. package/dist/src/room/track/TrackPublication.d.ts +3 -0
  59. package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
  60. package/dist/src/room/track/create.d.ts.map +1 -1
  61. package/dist/src/room/track/options.d.ts +2 -2
  62. package/dist/src/room/track/options.d.ts.map +1 -1
  63. package/dist/src/room/track/utils.d.ts +9 -0
  64. package/dist/src/room/track/utils.d.ts.map +1 -1
  65. package/dist/src/room/utils.d.ts +2 -0
  66. package/dist/src/room/utils.d.ts.map +1 -1
  67. package/dist/src/test/MockMediaStreamTrack.d.ts.map +1 -1
  68. package/dist/src/utils/browserParser.d.ts +2 -0
  69. package/dist/src/utils/browserParser.d.ts.map +1 -1
  70. package/dist/ts4.2/src/api/SignalClient.d.ts +4 -1
  71. package/dist/ts4.2/src/e2ee/E2eeManager.d.ts +45 -0
  72. package/dist/ts4.2/src/e2ee/KeyProvider.d.ts +42 -0
  73. package/dist/ts4.2/src/e2ee/constants.d.ts +14 -0
  74. package/dist/ts4.2/src/e2ee/errors.d.ts +11 -0
  75. package/dist/ts4.2/src/e2ee/index.d.ts +4 -0
  76. package/dist/ts4.2/src/e2ee/types.d.ts +129 -0
  77. package/dist/ts4.2/src/e2ee/utils.d.ts +24 -0
  78. package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +174 -0
  79. package/dist/ts4.2/src/e2ee/worker/ParticipantKeyHandler.d.ts +54 -0
  80. package/dist/ts4.2/src/e2ee/worker/e2ee.worker.d.ts +2 -0
  81. package/dist/ts4.2/src/index.d.ts +1 -0
  82. package/dist/ts4.2/src/logger.d.ts +4 -1
  83. package/dist/ts4.2/src/options.d.ts +5 -0
  84. package/dist/ts4.2/src/proto/livekit_models.d.ts +2 -2
  85. package/dist/ts4.2/src/room/PCTransport.d.ts +3 -1
  86. package/dist/ts4.2/src/room/RTCEngine.d.ts +17 -3
  87. package/dist/ts4.2/src/room/Room.d.ts +10 -0
  88. package/dist/ts4.2/src/room/events.d.ts +14 -2
  89. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +7 -2
  90. package/dist/ts4.2/src/room/participant/Participant.d.ts +1 -0
  91. package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +6 -4
  92. package/dist/ts4.2/src/room/track/TrackPublication.d.ts +3 -0
  93. package/dist/ts4.2/src/room/track/options.d.ts +6 -6
  94. package/dist/ts4.2/src/room/track/utils.d.ts +9 -0
  95. package/dist/ts4.2/src/room/utils.d.ts +2 -0
  96. package/dist/ts4.2/src/utils/browserParser.d.ts +2 -0
  97. package/package.json +17 -7
  98. package/src/api/SignalClient.ts +28 -9
  99. package/src/connectionHelper/checks/turn.ts +1 -0
  100. package/src/connectionHelper/checks/websocket.ts +1 -0
  101. package/src/e2ee/E2eeManager.ts +374 -0
  102. package/src/e2ee/KeyProvider.ts +77 -0
  103. package/src/e2ee/constants.ts +40 -0
  104. package/src/e2ee/errors.ts +16 -0
  105. package/src/e2ee/index.ts +3 -0
  106. package/src/e2ee/types.ts +160 -0
  107. package/src/e2ee/utils.ts +127 -0
  108. package/src/e2ee/worker/FrameCryptor.test.ts +21 -0
  109. package/src/e2ee/worker/FrameCryptor.ts +612 -0
  110. package/src/e2ee/worker/ParticipantKeyHandler.ts +144 -0
  111. package/src/e2ee/worker/e2ee.worker.ts +223 -0
  112. package/src/e2ee/worker/tsconfig.json +6 -0
  113. package/src/index.ts +1 -0
  114. package/src/logger.ts +10 -2
  115. package/src/options.ts +6 -0
  116. package/src/proto/livekit_models.ts +12 -12
  117. package/src/room/PCTransport.ts +39 -9
  118. package/src/room/RTCEngine.ts +127 -34
  119. package/src/room/Room.ts +94 -29
  120. package/src/room/defaults.ts +1 -1
  121. package/src/room/events.ts +14 -0
  122. package/src/room/participant/LocalParticipant.ts +52 -8
  123. package/src/room/participant/Participant.ts +4 -0
  124. package/src/room/participant/RemoteParticipant.ts +19 -15
  125. package/src/room/track/LocalTrack.ts +5 -4
  126. package/src/room/track/RemoteVideoTrack.ts +2 -2
  127. package/src/room/track/TrackPublication.ts +9 -1
  128. package/src/room/track/create.ts +9 -0
  129. package/src/room/track/options.ts +3 -2
  130. package/src/room/track/utils.ts +27 -0
  131. package/src/room/utils.ts +5 -0
  132. package/src/room/worker.d.ts +4 -0
  133. package/src/test/MockMediaStreamTrack.ts +1 -0
  134. package/src/utils/browserParser.ts +5 -0
@@ -0,0 +1,612 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ // TODO code inspired by https://github.com/webrtc/samples/blob/gh-pages/src/content/insertable-streams/endtoend-encryption/js/worker.js
3
+ import EventEmitter from 'eventemitter3';
4
+ import { workerLogger } from '../../logger';
5
+ import type { VideoCodec } from '../../room/track/options';
6
+ import { ENCRYPTION_ALGORITHM, IV_LENGTH, UNENCRYPTED_BYTES } from '../constants';
7
+ import { CryptorError, CryptorErrorReason } from '../errors';
8
+ import {
9
+ CryptorCallbacks,
10
+ CryptorEvent,
11
+ DecodeRatchetOptions,
12
+ KeyProviderOptions,
13
+ KeySet,
14
+ } from '../types';
15
+ import { deriveKeys, isVideoFrame } from '../utils';
16
+ import type { ParticipantKeyHandler } from './ParticipantKeyHandler';
17
+
18
+ export interface FrameCryptorConstructor {
19
+ new (opts?: unknown): BaseFrameCryptor;
20
+ }
21
+
22
+ export interface TransformerInfo {
23
+ readable: ReadableStream;
24
+ writable: WritableStream;
25
+ transformer: TransformStream;
26
+ abortController: AbortController;
27
+ }
28
+
29
+ export class BaseFrameCryptor extends EventEmitter<CryptorCallbacks> {
30
+ encodeFunction(
31
+ encodedFrame: RTCEncodedVideoFrame | RTCEncodedAudioFrame,
32
+ controller: TransformStreamDefaultController,
33
+ ): Promise<any> {
34
+ throw Error('not implemented for subclass');
35
+ }
36
+
37
+ decodeFunction(
38
+ encodedFrame: RTCEncodedVideoFrame | RTCEncodedAudioFrame,
39
+ controller: TransformStreamDefaultController,
40
+ ): Promise<any> {
41
+ throw Error('not implemented for subclass');
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Cryptor is responsible for en-/decrypting media frames.
47
+ * Each Cryptor instance is responsible for en-/decrypting a single mediaStreamTrack.
48
+ */
49
+ export class FrameCryptor extends BaseFrameCryptor {
50
+ private sendCounts: Map<number, number>;
51
+
52
+ private participantId: string | undefined;
53
+
54
+ private trackId: string | undefined;
55
+
56
+ private keys: ParticipantKeyHandler;
57
+
58
+ private videoCodec?: VideoCodec;
59
+
60
+ private rtpMap: Map<number, VideoCodec>;
61
+
62
+ private keyProviderOptions: KeyProviderOptions;
63
+
64
+ /**
65
+ * used for detecting server injected unencrypted frames
66
+ */
67
+ private unencryptedFrameByteTrailer: Uint8Array;
68
+
69
+ constructor(opts: {
70
+ keys: ParticipantKeyHandler;
71
+ participantId: string;
72
+ keyProviderOptions: KeyProviderOptions;
73
+ unencryptedFrameBytes?: Uint8Array;
74
+ }) {
75
+ super();
76
+ this.sendCounts = new Map();
77
+ this.keys = opts.keys;
78
+ this.participantId = opts.participantId;
79
+ this.rtpMap = new Map();
80
+ this.keyProviderOptions = opts.keyProviderOptions;
81
+ this.unencryptedFrameByteTrailer =
82
+ opts.unencryptedFrameBytes ?? new TextEncoder().encode('LKROCKS');
83
+ }
84
+
85
+ /**
86
+ * Assign a different participant to the cryptor.
87
+ * useful for transceiver re-use
88
+ * @param id
89
+ * @param keys
90
+ */
91
+ setParticipant(id: string, keys: ParticipantKeyHandler) {
92
+ this.participantId = id;
93
+ this.keys = keys;
94
+ }
95
+
96
+ unsetParticipant() {
97
+ this.participantId = undefined;
98
+ }
99
+
100
+ getParticipantId() {
101
+ return this.participantId;
102
+ }
103
+
104
+ getTrackId() {
105
+ return this.trackId;
106
+ }
107
+
108
+ /**
109
+ * Update the video codec used by the mediaStreamTrack
110
+ * @param codec
111
+ */
112
+ setVideoCodec(codec: VideoCodec) {
113
+ this.videoCodec = codec;
114
+ }
115
+
116
+ /**
117
+ * rtp payload type map used for figuring out codec of payload type when encoding
118
+ * @param map
119
+ */
120
+ setRtpMap(map: Map<number, VideoCodec>) {
121
+ this.rtpMap = map;
122
+ }
123
+
124
+ setupTransform(
125
+ operation: 'encode' | 'decode',
126
+ readable: ReadableStream,
127
+ writable: WritableStream,
128
+ trackId: string,
129
+ codec?: VideoCodec,
130
+ ) {
131
+ if (codec) {
132
+ console.info('setting codec on cryptor to', codec);
133
+ this.videoCodec = codec;
134
+ }
135
+ const transformFn = operation === 'encode' ? this.encodeFunction : this.decodeFunction;
136
+ const transformStream = new TransformStream({
137
+ transform: transformFn.bind(this),
138
+ });
139
+
140
+ readable
141
+ .pipeThrough(transformStream)
142
+ .pipeTo(writable)
143
+ .catch((e) => {
144
+ console.error(e);
145
+ this.emit('cryptorError', e instanceof CryptorError ? e : new CryptorError(e.message));
146
+ });
147
+ this.trackId = trackId;
148
+ }
149
+
150
+ /**
151
+ * Function that will be injected in a stream and will encrypt the given encoded frames.
152
+ *
153
+ * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
154
+ * @param {TransformStreamDefaultController} controller - TransportStreamController.
155
+ *
156
+ * The VP8 payload descriptor described in
157
+ * https://tools.ietf.org/html/rfc7741#section-4.2
158
+ * is part of the RTP packet and not part of the frame and is not controllable by us.
159
+ * This is fine as the SFU keeps having access to it for routing.
160
+ *
161
+ * The encrypted frame is formed as follows:
162
+ * 1) Find unencrypted byte length, depending on the codec, frame type and kind.
163
+ * 2) Form the GCM IV for the frame as described above.
164
+ * 3) Encrypt the rest of the frame using AES-GCM.
165
+ * 4) Allocate space for the encrypted frame.
166
+ * 5) Copy the unencrypted bytes to the start of the encrypted frame.
167
+ * 6) Append the ciphertext to the encrypted frame.
168
+ * 7) Append the IV.
169
+ * 8) Append a single byte for the key identifier.
170
+ * 9) Enqueue the encrypted frame for sending.
171
+ */
172
+ async encodeFunction(
173
+ encodedFrame: RTCEncodedVideoFrame | RTCEncodedAudioFrame,
174
+ controller: TransformStreamDefaultController,
175
+ ) {
176
+ if (
177
+ !this.keys.isEnabled() ||
178
+ // skip for encryption for empty dtx frames
179
+ encodedFrame.data.byteLength === 0
180
+ ) {
181
+ return controller.enqueue(encodedFrame);
182
+ }
183
+
184
+ const { encryptionKey } = this.keys.getKeySet();
185
+ const keyIndex = this.keys.getCurrentKeyIndex();
186
+
187
+ if (encryptionKey) {
188
+ const iv = this.makeIV(
189
+ encodedFrame.getMetadata().synchronizationSource ?? -1,
190
+ encodedFrame.timestamp,
191
+ );
192
+
193
+ // Thіs is not encrypted and contains the VP8 payload descriptor or the Opus TOC byte.
194
+ const frameHeader = new Uint8Array(
195
+ encodedFrame.data,
196
+ 0,
197
+ this.getUnencryptedBytes(encodedFrame),
198
+ );
199
+
200
+ // Frame trailer contains the R|IV_LENGTH and key index
201
+ const frameTrailer = new Uint8Array(2);
202
+
203
+ frameTrailer[0] = IV_LENGTH;
204
+ frameTrailer[1] = keyIndex;
205
+
206
+ // Construct frame trailer. Similar to the frame header described in
207
+ // https://tools.ietf.org/html/draft-omara-sframe-00#section-4.2
208
+ // but we put it at the end.
209
+ //
210
+ // ---------+-------------------------+-+---------+----
211
+ // payload |IV...(length = IV_LENGTH)|R|IV_LENGTH|KID |
212
+ // ---------+-------------------------+-+---------+----
213
+ try {
214
+ const cipherText = await crypto.subtle.encrypt(
215
+ {
216
+ name: ENCRYPTION_ALGORITHM,
217
+ iv,
218
+ additionalData: new Uint8Array(encodedFrame.data, 0, frameHeader.byteLength),
219
+ },
220
+ encryptionKey,
221
+ new Uint8Array(encodedFrame.data, this.getUnencryptedBytes(encodedFrame)),
222
+ );
223
+
224
+ const newData = new ArrayBuffer(
225
+ frameHeader.byteLength + cipherText.byteLength + iv.byteLength + frameTrailer.byteLength,
226
+ );
227
+ const newUint8 = new Uint8Array(newData);
228
+
229
+ newUint8.set(frameHeader); // copy first bytes.
230
+ newUint8.set(new Uint8Array(cipherText), frameHeader.byteLength); // add ciphertext.
231
+ newUint8.set(new Uint8Array(iv), frameHeader.byteLength + cipherText.byteLength); // append IV.
232
+ newUint8.set(frameTrailer, frameHeader.byteLength + cipherText.byteLength + iv.byteLength); // append frame trailer.
233
+
234
+ encodedFrame.data = newData;
235
+
236
+ return controller.enqueue(encodedFrame);
237
+ } catch (e: any) {
238
+ // TODO: surface this to the app.
239
+ workerLogger.error(e);
240
+ }
241
+ } else {
242
+ this.emit(
243
+ CryptorEvent.Error,
244
+ new CryptorError(`encryption key missing for encoding`, CryptorErrorReason.MissingKey),
245
+ );
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Function that will be injected in a stream and will decrypt the given encoded frames.
251
+ *
252
+ * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
253
+ * @param {TransformStreamDefaultController} controller - TransportStreamController.
254
+ */
255
+ async decodeFunction(
256
+ encodedFrame: RTCEncodedVideoFrame | RTCEncodedAudioFrame,
257
+ controller: TransformStreamDefaultController,
258
+ ) {
259
+ if (
260
+ !this.keys.isEnabled() ||
261
+ // skip for decryption for empty dtx frames
262
+ encodedFrame.data.byteLength === 0 ||
263
+ // skip decryption if frame is server injected
264
+ isFrameServerInjected(encodedFrame.data, this.unencryptedFrameByteTrailer)
265
+ ) {
266
+ return controller.enqueue(encodedFrame);
267
+ }
268
+ const data = new Uint8Array(encodedFrame.data);
269
+ const keyIndex = data[encodedFrame.data.byteLength - 1];
270
+
271
+ if (this.keys.getKeySet(keyIndex) && this.keys.hasValidKey) {
272
+ try {
273
+ const decodedFrame = await this.decryptFrame(encodedFrame, keyIndex);
274
+ if (decodedFrame) {
275
+ return controller.enqueue(decodedFrame);
276
+ }
277
+ } catch (error) {
278
+ if (error instanceof CryptorError && error.reason === CryptorErrorReason.InvalidKey) {
279
+ if (this.keys.hasValidKey) {
280
+ workerLogger.warn('invalid key');
281
+ this.emit(
282
+ CryptorEvent.Error,
283
+ new CryptorError(
284
+ `invalid key for participant ${this.participantId}`,
285
+ CryptorErrorReason.InvalidKey,
286
+ ),
287
+ );
288
+ this.keys.hasValidKey = false;
289
+ }
290
+ } else {
291
+ workerLogger.warn('decoding frame failed', { error });
292
+ }
293
+ }
294
+ }
295
+
296
+ return controller.enqueue(encodedFrame);
297
+ }
298
+
299
+ /**
300
+ * Function that will decrypt the given encoded frame. If the decryption fails, it will
301
+ * ratchet the key for up to RATCHET_WINDOW_SIZE times.
302
+ */
303
+ async decryptFrame(
304
+ encodedFrame: RTCEncodedVideoFrame | RTCEncodedAudioFrame,
305
+ keyIndex: number,
306
+ initialMaterial: KeySet | undefined = undefined,
307
+ ratchetOpts: DecodeRatchetOptions = { ratchetCount: 0 },
308
+ ): Promise<RTCEncodedVideoFrame | RTCEncodedAudioFrame | undefined> {
309
+ const keySet = this.keys.getKeySet(keyIndex);
310
+
311
+ // Construct frame trailer. Similar to the frame header described in
312
+ // https://tools.ietf.org/html/draft-omara-sframe-00#section-4.2
313
+ // but we put it at the end.
314
+ //
315
+ // ---------+-------------------------+-+---------+----
316
+ // payload |IV...(length = IV_LENGTH)|R|IV_LENGTH|KID |
317
+ // ---------+-------------------------+-+---------+----
318
+
319
+ try {
320
+ const frameHeader = new Uint8Array(
321
+ encodedFrame.data,
322
+ 0,
323
+ this.getUnencryptedBytes(encodedFrame),
324
+ );
325
+ const frameTrailer = new Uint8Array(encodedFrame.data, encodedFrame.data.byteLength - 2, 2);
326
+
327
+ const ivLength = frameTrailer[0];
328
+ const iv = new Uint8Array(
329
+ encodedFrame.data,
330
+ encodedFrame.data.byteLength - ivLength - frameTrailer.byteLength,
331
+ ivLength,
332
+ );
333
+
334
+ const cipherTextStart = frameHeader.byteLength;
335
+ const cipherTextLength =
336
+ encodedFrame.data.byteLength -
337
+ (frameHeader.byteLength + ivLength + frameTrailer.byteLength);
338
+
339
+ const plainText = await crypto.subtle.decrypt(
340
+ {
341
+ name: ENCRYPTION_ALGORITHM,
342
+ iv,
343
+ additionalData: new Uint8Array(encodedFrame.data, 0, frameHeader.byteLength),
344
+ },
345
+ ratchetOpts.encryptionKey ?? keySet.encryptionKey,
346
+ new Uint8Array(encodedFrame.data, cipherTextStart, cipherTextLength),
347
+ );
348
+
349
+ const newData = new ArrayBuffer(frameHeader.byteLength + plainText.byteLength);
350
+ const newUint8 = new Uint8Array(newData);
351
+
352
+ newUint8.set(new Uint8Array(encodedFrame.data, 0, frameHeader.byteLength));
353
+ newUint8.set(new Uint8Array(plainText), frameHeader.byteLength);
354
+
355
+ encodedFrame.data = newData;
356
+
357
+ return encodedFrame;
358
+ } catch (error: any) {
359
+ if (this.keyProviderOptions.ratchetWindowSize > 0) {
360
+ if (ratchetOpts.ratchetCount < this.keyProviderOptions.ratchetWindowSize) {
361
+ workerLogger.debug(
362
+ `ratcheting key attempt ${ratchetOpts.ratchetCount} of ${
363
+ this.keyProviderOptions.ratchetWindowSize
364
+ }, for kind ${encodedFrame instanceof RTCEncodedAudioFrame ? 'audio' : 'video'}`,
365
+ );
366
+
367
+ let ratchetedKeySet: KeySet | undefined;
368
+ if (keySet === this.keys.getKeySet(keyIndex)) {
369
+ // only ratchet if the currently set key is still the same as the one used to decrypt this frame
370
+ // if not, it might be that a different frame has already ratcheted and we try with that one first
371
+ const newMaterial = await this.keys.ratchetKey(keyIndex, false);
372
+
373
+ ratchetedKeySet = await deriveKeys(newMaterial, this.keyProviderOptions.ratchetSalt);
374
+ }
375
+
376
+ const frame = await this.decryptFrame(encodedFrame, keyIndex, initialMaterial || keySet, {
377
+ ratchetCount: ratchetOpts.ratchetCount + 1,
378
+ encryptionKey: ratchetedKeySet?.encryptionKey,
379
+ });
380
+ if (frame && ratchetedKeySet) {
381
+ this.keys.setKeySet(ratchetedKeySet, keyIndex, true);
382
+ // decryption was successful, set the new key index to reflect the ratcheted key set
383
+ this.keys.setCurrentKeyIndex(keyIndex);
384
+ }
385
+ return frame;
386
+ } else {
387
+ /**
388
+ * Since the key it is first send and only afterwards actually used for encrypting, there were
389
+ * situations when the decrypting failed due to the fact that the received frame was not encrypted
390
+ * yet and ratcheting, of course, did not solve the problem. So if we fail RATCHET_WINDOW_SIZE times,
391
+ * we come back to the initial key.
392
+ */
393
+ if (initialMaterial) {
394
+ workerLogger.debug('resetting to initial material');
395
+ this.keys.setKeyFromMaterial(initialMaterial.material, keyIndex);
396
+ }
397
+
398
+ this.keys.hasValidKey = false;
399
+
400
+ workerLogger.warn('maximum ratchet attempts exceeded, resetting key');
401
+ this.emit(
402
+ CryptorEvent.Error,
403
+ new CryptorError(
404
+ `valid key missing for participant ${this.participantId}`,
405
+ CryptorErrorReason.MissingKey,
406
+ ),
407
+ );
408
+ }
409
+ } else {
410
+ throw new CryptorError(
411
+ 'Decryption failed, most likely because of an invalid key',
412
+ CryptorErrorReason.InvalidKey,
413
+ );
414
+ }
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Construct the IV used for AES-GCM and sent (in plain) with the packet similar to
420
+ * https://tools.ietf.org/html/rfc7714#section-8.1
421
+ * It concatenates
422
+ * - the 32 bit synchronization source (SSRC) given on the encoded frame,
423
+ * - the 32 bit rtp timestamp given on the encoded frame,
424
+ * - a send counter that is specific to the SSRC. Starts at a random number.
425
+ * The send counter is essentially the pictureId but we currently have to implement this ourselves.
426
+ * There is no XOR with a salt. Note that this IV leaks the SSRC to the receiver but since this is
427
+ * randomly generated and SFUs may not rewrite this is considered acceptable.
428
+ * The SSRC is used to allow demultiplexing multiple streams with the same key, as described in
429
+ * https://tools.ietf.org/html/rfc3711#section-4.1.1
430
+ * The RTP timestamp is 32 bits and advances by the codec clock rate (90khz for video, 48khz for
431
+ * opus audio) every second. For video it rolls over roughly every 13 hours.
432
+ * The send counter will advance at the frame rate (30fps for video, 50fps for 20ms opus audio)
433
+ * every second. It will take a long time to roll over.
434
+ *
435
+ * See also https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams
436
+ */
437
+ private makeIV(synchronizationSource: number, timestamp: number) {
438
+ const iv = new ArrayBuffer(IV_LENGTH);
439
+ const ivView = new DataView(iv);
440
+
441
+ // having to keep our own send count (similar to a picture id) is not ideal.
442
+ if (!this.sendCounts.has(synchronizationSource)) {
443
+ // Initialize with a random offset, similar to the RTP sequence number.
444
+ this.sendCounts.set(synchronizationSource, Math.floor(Math.random() * 0xffff));
445
+ }
446
+
447
+ const sendCount = this.sendCounts.get(synchronizationSource) ?? 0;
448
+
449
+ ivView.setUint32(0, synchronizationSource);
450
+ ivView.setUint32(4, timestamp);
451
+ ivView.setUint32(8, timestamp - (sendCount % 0xffff));
452
+
453
+ this.sendCounts.set(synchronizationSource, sendCount + 1);
454
+
455
+ return iv;
456
+ }
457
+
458
+ getUnencryptedBytes(frame: RTCEncodedVideoFrame | RTCEncodedAudioFrame): number {
459
+ if (isVideoFrame(frame)) {
460
+ let detectedCodec = this.getVideoCodec(frame) ?? this.videoCodec;
461
+
462
+ if (detectedCodec === 'av1' || detectedCodec === 'vp9') {
463
+ throw new Error(`${detectedCodec} is not yet supported for end to end encryption`);
464
+ }
465
+
466
+ if (detectedCodec === 'vp8') {
467
+ return UNENCRYPTED_BYTES[frame.type];
468
+ }
469
+
470
+ const data = new Uint8Array(frame.data);
471
+ try {
472
+ const naluIndices = findNALUIndices(data);
473
+
474
+ // if the detected codec is undefined we test whether it _looks_ like a h264 frame as a best guess
475
+ const isH264 =
476
+ detectedCodec === 'h264' ||
477
+ naluIndices.some((naluIndex) =>
478
+ [NALUType.SLICE_IDR, NALUType.SLICE_NON_IDR].includes(parseNALUType(data[naluIndex])),
479
+ );
480
+
481
+ if (isH264) {
482
+ for (const index of naluIndices) {
483
+ let type = parseNALUType(data[index]);
484
+ switch (type) {
485
+ case NALUType.SLICE_IDR:
486
+ case NALUType.SLICE_NON_IDR:
487
+ return index + 2;
488
+ default:
489
+ break;
490
+ }
491
+ }
492
+ throw new TypeError('Could not find NALU');
493
+ }
494
+ } catch (e) {
495
+ // no op, we just continue and fallback to vp8
496
+ }
497
+
498
+ return UNENCRYPTED_BYTES[frame.type];
499
+ } else {
500
+ return UNENCRYPTED_BYTES.audio;
501
+ }
502
+ }
503
+
504
+ /**
505
+ * inspects frame payloadtype if available and maps it to the codec specified in rtpMap
506
+ */
507
+ getVideoCodec(frame: RTCEncodedVideoFrame): VideoCodec | undefined {
508
+ if (this.rtpMap.size === 0) {
509
+ return undefined;
510
+ }
511
+ // @ts-expect-error payloadType is not yet part of the typescript definition and currently not supported in Safari
512
+ const payloadType = frame.getMetadata().payloadType;
513
+ const codec = payloadType ? this.rtpMap.get(payloadType) : undefined;
514
+ return codec;
515
+ }
516
+ }
517
+
518
+ /**
519
+ * Slice the NALUs present in the supplied buffer, assuming it is already byte-aligned
520
+ * code adapted from https://github.com/medooze/h264-frame-parser/blob/main/lib/NalUnits.ts to return indices only
521
+ */
522
+ export function findNALUIndices(stream: Uint8Array): number[] {
523
+ const result: number[] = [];
524
+ let start = 0,
525
+ pos = 0,
526
+ searchLength = stream.length - 2;
527
+ while (pos < searchLength) {
528
+ // skip until end of current NALU
529
+ while (
530
+ pos < searchLength &&
531
+ !(stream[pos] === 0 && stream[pos + 1] === 0 && stream[pos + 2] === 1)
532
+ )
533
+ pos++;
534
+ if (pos >= searchLength) pos = stream.length;
535
+ // remove trailing zeros from current NALU
536
+ let end = pos;
537
+ while (end > start && stream[end - 1] === 0) end--;
538
+ // save current NALU
539
+ if (start === 0) {
540
+ if (end !== start) throw TypeError('byte stream contains leading data');
541
+ } else {
542
+ result.push(start);
543
+ }
544
+ // begin new NALU
545
+ start = pos = pos + 3;
546
+ }
547
+ return result;
548
+ }
549
+
550
+ export function parseNALUType(startByte: number): NALUType {
551
+ return startByte & kNaluTypeMask;
552
+ }
553
+
554
+ const kNaluTypeMask = 0x1f;
555
+
556
+ export enum NALUType {
557
+ /** Coded slice of a non-IDR picture */
558
+ SLICE_NON_IDR = 1,
559
+ /** Coded slice data partition A */
560
+ SLICE_PARTITION_A = 2,
561
+ /** Coded slice data partition B */
562
+ SLICE_PARTITION_B = 3,
563
+ /** Coded slice data partition C */
564
+ SLICE_PARTITION_C = 4,
565
+ /** Coded slice of an IDR picture */
566
+ SLICE_IDR = 5,
567
+ /** Supplemental enhancement information */
568
+ SEI = 6,
569
+ /** Sequence parameter set */
570
+ SPS = 7,
571
+ /** Picture parameter set */
572
+ PPS = 8,
573
+ /** Access unit delimiter */
574
+ AUD = 9,
575
+ /** End of sequence */
576
+ END_SEQ = 10,
577
+ /** End of stream */
578
+ END_STREAM = 11,
579
+ /** Filler data */
580
+ FILLER_DATA = 12,
581
+ /** Sequence parameter set extension */
582
+ SPS_EXT = 13,
583
+ /** Prefix NAL unit */
584
+ PREFIX_NALU = 14,
585
+ /** Subset sequence parameter set */
586
+ SUBSET_SPS = 15,
587
+ /** Depth parameter set */
588
+ DPS = 16,
589
+
590
+ // 17, 18 reserved
591
+
592
+ /** Coded slice of an auxiliary coded picture without partitioning */
593
+ SLICE_AUX = 19,
594
+ /** Coded slice extension */
595
+ SLICE_EXT = 20,
596
+ /** Coded slice extension for a depth view component or a 3D-AVC texture view component */
597
+ SLICE_LAYER_EXT = 21,
598
+
599
+ // 22, 23 reserved
600
+ }
601
+
602
+ /**
603
+ * we use a magic frame trailer to detect whether a frame is injected
604
+ * by the livekit server and thus to be treated as unencrypted
605
+ * @internal
606
+ */
607
+ export function isFrameServerInjected(frameData: ArrayBuffer, trailerBytes: Uint8Array): boolean {
608
+ const frameTrailer = new Uint8Array(
609
+ frameData.slice(frameData.byteLength - trailerBytes.byteLength),
610
+ );
611
+ return trailerBytes.every((value, index) => value === frameTrailer[index]);
612
+ }