livekit-client 1.11.4 → 1.12.0

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