livekit-client 2.5.8 → 2.5.10

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 (45) 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 +543 -5128
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +104 -98
  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.map +1 -1
  10. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  11. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +25 -5
  12. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
  13. package/dist/src/index.d.ts +2 -1
  14. package/dist/src/index.d.ts.map +1 -1
  15. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  16. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  17. package/dist/src/room/Room.d.ts.map +1 -1
  18. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  19. package/dist/src/room/track/LocalTrack.d.ts +1 -1
  20. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  21. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  22. package/dist/src/room/utils.d.ts +0 -10
  23. package/dist/src/room/utils.d.ts.map +1 -1
  24. package/dist/ts4.2/src/e2ee/worker/ParticipantKeyHandler.d.ts +25 -5
  25. package/dist/ts4.2/src/index.d.ts +2 -1
  26. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +1 -1
  27. package/dist/ts4.2/src/room/utils.d.ts +0 -10
  28. package/package.json +3 -2
  29. package/src/api/SignalClient.ts +2 -1
  30. package/src/e2ee/worker/FrameCryptor.test.ts +311 -113
  31. package/src/e2ee/worker/FrameCryptor.ts +10 -5
  32. package/src/e2ee/worker/ParticipantKeyHandler.test.ts +169 -5
  33. package/src/e2ee/worker/ParticipantKeyHandler.ts +50 -20
  34. package/src/e2ee/worker/__snapshots__/ParticipantKeyHandler.test.ts.snap +356 -0
  35. package/src/index.ts +1 -1
  36. package/src/room/PCTransportManager.ts +2 -1
  37. package/src/room/RTCEngine.ts +3 -1
  38. package/src/room/Room.ts +1 -1
  39. package/src/room/participant/LocalParticipant.ts +4 -1
  40. package/src/room/track/LocalTrack.ts +2 -1
  41. package/src/room/track/LocalVideoTrack.ts +2 -1
  42. package/src/room/track/options.ts +5 -5
  43. package/src/room/utils.ts +0 -38
  44. package/src/utils/AsyncQueue.test.ts +2 -2
  45. package/src/utils/AsyncQueue.ts +1 -1
@@ -60,6 +60,21 @@ class TestUnderlyingSink<T> implements UnderlyingSink<T> {
60
60
  function prepareParticipantTestDecoder(
61
61
  participantIdentity: string,
62
62
  partialKeyProviderOptions: Partial<KeyProviderOptions>,
63
+ ) {
64
+ return prepareParticipantTest('decode', participantIdentity, partialKeyProviderOptions);
65
+ }
66
+
67
+ function prepareParticipantTestEncoder(
68
+ participantIdentity: string,
69
+ partialKeyProviderOptions: Partial<KeyProviderOptions>,
70
+ ) {
71
+ return prepareParticipantTest('encode', participantIdentity, partialKeyProviderOptions);
72
+ }
73
+
74
+ function prepareParticipantTest(
75
+ mode: 'encode' | 'decode',
76
+ participantIdentity: string,
77
+ partialKeyProviderOptions: Partial<KeyProviderOptions>,
63
78
  ): {
64
79
  keys: ParticipantKeyHandler;
65
80
  cryptor: FrameCryptor;
@@ -80,12 +95,7 @@ function prepareParticipantTestDecoder(
80
95
 
81
96
  const input = new TestUnderlyingSource<RTCEncodedVideoFrame>();
82
97
  const output = new TestUnderlyingSink<RTCEncodedVideoFrame>();
83
- cryptor.setupTransform(
84
- 'decode',
85
- new ReadableStream(input),
86
- new WritableStream(output),
87
- 'testTrack',
88
- );
98
+ cryptor.setupTransform(mode, new ReadableStream(input), new WritableStream(output), 'testTrack');
89
99
 
90
100
  return { keys, cryptor, input, output };
91
101
  }
@@ -93,10 +103,6 @@ function prepareParticipantTestDecoder(
93
103
  describe('FrameCryptor', () => {
94
104
  const participantIdentity = 'testParticipant';
95
105
 
96
- afterEach(() => {
97
- encryptionEnabledMap.clear();
98
- });
99
-
100
106
  it('identifies server injected frame correctly', () => {
101
107
  const frameTrailer = new TextEncoder().encode('LKROCKS');
102
108
  const frameData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, ...frameTrailer]).buffer;
@@ -113,154 +119,346 @@ describe('FrameCryptor', () => {
113
119
  expect(isFrameServerInjected(frameData.buffer, frameTrailer)).toBe(false);
114
120
  });
115
121
 
116
- it('passthrough if participant encryption disabled', async () => {
117
- vitest.useFakeTimers();
118
- try {
119
- const { input, output } = prepareParticipantTestDecoder(participantIdentity, {});
122
+ describe('encode', () => {
123
+ afterEach(() => {
124
+ encryptionEnabledMap.clear();
125
+ });
120
126
 
121
- // disable encryption for participant
122
- encryptionEnabledMap.set(participantIdentity, false);
127
+ it('passthrough if participant encryption disabled', async () => {
128
+ vitest.useFakeTimers();
129
+ try {
130
+ const { input, output } = prepareParticipantTestEncoder(participantIdentity, {});
123
131
 
124
- const frame = mockEncryptedRTCEncodedVideoFrame(1);
132
+ // disable encryption for participant
133
+ encryptionEnabledMap.set(participantIdentity, false);
125
134
 
126
- input.write(frame);
127
- await vitest.advanceTimersToNextTimerAsync();
135
+ const frame = mockRTCEncodedVideoFrame(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]));
128
136
 
129
- expect(output.chunks).toEqual([frame]);
130
- } finally {
131
- vitest.useRealTimers();
132
- }
133
- });
137
+ input.write(frame);
138
+ await vitest.advanceTimersToNextTimerAsync();
134
139
 
135
- it('passthrough for empty frame', async () => {
136
- vitest.useFakeTimers();
137
- try {
138
- const { input, output } = prepareParticipantTestDecoder(participantIdentity, {});
140
+ expect(output.chunks).toEqual([frame]);
141
+ } finally {
142
+ vitest.useRealTimers();
143
+ }
144
+ });
139
145
 
140
- // empty frame
141
- const frame = mockRTCEncodedVideoFrame(new Uint8Array(0));
146
+ it('passthrough for empty frame', async () => {
147
+ vitest.useFakeTimers();
148
+ try {
149
+ const { input, output } = prepareParticipantTestEncoder(participantIdentity, {});
142
150
 
143
- input.write(frame);
144
- await vitest.advanceTimersToNextTimerAsync();
151
+ // empty frame
152
+ const frame = mockRTCEncodedVideoFrame(new Uint8Array(0));
145
153
 
146
- expect(output.chunks).toEqual([frame]);
147
- } finally {
148
- vitest.useRealTimers();
149
- }
150
- });
154
+ input.write(frame);
155
+ await vitest.advanceTimersToNextTimerAsync();
156
+
157
+ expect(output.chunks).toEqual([frame]);
158
+ } finally {
159
+ vitest.useRealTimers();
160
+ }
161
+ });
162
+
163
+ it('immediately drops frame and emits error if no key set', async () => {
164
+ vitest.useFakeTimers();
165
+ try {
166
+ const { cryptor, input, output } = prepareParticipantTestEncoder(participantIdentity, {});
167
+
168
+ const errorListener = vitest.fn();
169
+ cryptor.on(CryptorEvent.Error, errorListener);
170
+
171
+ const frame = mockRTCEncodedVideoFrame(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]));
151
172
 
152
- it('drops frames when invalid key', async () => {
153
- vitest.useFakeTimers();
154
- try {
155
- const { keys, input, output } = prepareParticipantTestDecoder(participantIdentity, {
156
- failureTolerance: 0,
157
- });
173
+ input.write(frame);
174
+ await vitest.advanceTimersToNextTimerAsync();
158
175
 
159
- expect(keys.hasValidKey).toBe(true);
176
+ expect(output.chunks).toEqual([]);
177
+ expect(errorListener).toHaveBeenCalled();
178
+ } finally {
179
+ vitest.useRealTimers();
180
+ }
181
+ });
182
+
183
+ it('encrypts frame', async () => {
184
+ vitest.useFakeTimers();
185
+ try {
186
+ const { keys, input, output } = prepareParticipantTestEncoder(participantIdentity, {});
187
+
188
+ await keys.setKey(await createKeyMaterialFromString('key1'), 1);
160
189
 
161
- await keys.setKey(await createKeyMaterialFromString('password'), 0);
190
+ const plainTextData = new Uint8Array([
191
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
192
+ ]);
193
+ const frame = mockRTCEncodedVideoFrame(plainTextData);
162
194
 
163
- input.write(mockEncryptedRTCEncodedVideoFrame(1));
164
- await vitest.advanceTimersToNextTimerAsync();
195
+ input.write(frame);
196
+ await vitest.waitFor(() => expect(output.chunks).toHaveLength(1));
165
197
 
166
- expect(output.chunks).toEqual([]);
167
- expect(keys.hasValidKey).toBe(false);
198
+ expect(output.chunks).toEqual([frame]);
199
+ expect(frame.data.byteLength).toBeGreaterThan(16);
168
200
 
169
- // this should still fail as keys are all marked as invalid
170
- input.write(mockEncryptedRTCEncodedVideoFrame(0));
171
- await vitest.advanceTimersToNextTimerAsync();
201
+ // first bytes are unencrypted
202
+ expect(new Uint8Array(frame.data.slice(0, 10))).toEqual(plainTextData.subarray(0, 10));
172
203
 
173
- expect(output.chunks).toEqual([]);
174
- expect(keys.hasValidKey).toBe(false);
175
- } finally {
176
- vitest.useRealTimers();
177
- }
204
+ // remainder should not be the same
205
+ expect(new Uint8Array(frame.data.slice(10, 16))).not.toEqual(
206
+ plainTextData.subarray(10, 16),
207
+ );
208
+
209
+ const frameTrailer = new Uint8Array(frame.data.slice(frame.data.byteLength - 2));
210
+ // IV length
211
+ expect(frameTrailer[0]).toEqual(IV_LENGTH);
212
+ // key index
213
+ expect(frameTrailer[1]).toEqual(1);
214
+ } finally {
215
+ vitest.useRealTimers();
216
+ }
217
+ });
178
218
  });
179
219
 
180
- it('marks key invalid after too many failures', async () => {
181
- const { keys, cryptor, input } = prepareParticipantTestDecoder(participantIdentity, {
182
- failureTolerance: 1,
220
+ describe('decode', () => {
221
+ afterEach(() => {
222
+ encryptionEnabledMap.clear();
183
223
  });
184
224
 
185
- expect(keys.hasValidKey).toBe(true);
225
+ it('passthrough if participant encryption disabled', async () => {
226
+ vitest.useFakeTimers();
227
+ try {
228
+ const { input, output } = prepareParticipantTestDecoder(participantIdentity, {});
186
229
 
187
- await keys.setKey(await createKeyMaterialFromString('password'), 0);
230
+ // disable encryption for participant
231
+ encryptionEnabledMap.set(participantIdentity, false);
188
232
 
189
- vitest.spyOn(keys, 'getKeySet');
190
- vitest.spyOn(keys, 'decryptionFailure');
233
+ const frame = mockEncryptedRTCEncodedVideoFrame(1);
191
234
 
192
- const errorListener = vitest.fn().mockImplementation((e) => {
193
- console.log('error', e);
235
+ input.write(frame);
236
+ await vitest.advanceTimersToNextTimerAsync();
237
+
238
+ expect(output.chunks).toEqual([frame]);
239
+ } finally {
240
+ vitest.useRealTimers();
241
+ }
194
242
  });
195
- cryptor.on(CryptorEvent.Error, errorListener);
196
243
 
197
- input.write(mockEncryptedRTCEncodedVideoFrame(1));
244
+ it('passthrough for empty frame', async () => {
245
+ vitest.useFakeTimers();
246
+ try {
247
+ const { input, output } = prepareParticipantTestDecoder(participantIdentity, {});
198
248
 
199
- await vitest.waitFor(() => expect(keys.decryptionFailure).toHaveBeenCalled());
200
- expect(errorListener).toHaveBeenCalled();
201
- expect(keys.decryptionFailure).toHaveBeenCalledTimes(1);
202
- expect(keys.getKeySet).toHaveBeenCalled();
203
- expect(keys.getKeySet).toHaveBeenLastCalledWith(1);
204
- expect(keys.hasValidKey).toBe(true);
249
+ // empty frame
250
+ const frame = mockRTCEncodedVideoFrame(new Uint8Array(0));
205
251
 
206
- vitest.clearAllMocks();
252
+ input.write(frame);
253
+ await vitest.advanceTimersToNextTimerAsync();
207
254
 
208
- input.write(mockEncryptedRTCEncodedVideoFrame(1));
255
+ expect(output.chunks).toEqual([frame]);
256
+ } finally {
257
+ vitest.useRealTimers();
258
+ }
259
+ });
209
260
 
210
- await vitest.waitFor(() => expect(keys.decryptionFailure).toHaveBeenCalled());
211
- expect(errorListener).toHaveBeenCalled();
212
- expect(keys.decryptionFailure).toHaveBeenCalledTimes(1);
213
- expect(keys.getKeySet).toHaveBeenCalled();
214
- expect(keys.getKeySet).toHaveBeenLastCalledWith(1);
215
- expect(keys.hasValidKey).toBe(false);
261
+ it('immediately drops frames when key marked invalid', async () => {
262
+ vitest.useFakeTimers();
263
+ try {
264
+ const { keys, input, output } = prepareParticipantTestDecoder(participantIdentity, {
265
+ failureTolerance: 0,
266
+ });
216
267
 
217
- vitest.clearAllMocks();
268
+ keys.decryptionFailure();
218
269
 
219
- // this should still fail as keys are all marked as invalid
220
- input.write(mockEncryptedRTCEncodedVideoFrame(0));
270
+ input.write(mockEncryptedRTCEncodedVideoFrame(1));
271
+ await vitest.advanceTimersToNextTimerAsync();
221
272
 
222
- await vitest.waitFor(() => expect(keys.getKeySet).toHaveBeenCalled());
223
- // decryptionFailure() isn't called in this case
224
- expect(keys.getKeySet).toHaveBeenCalled();
225
- expect(keys.getKeySet).toHaveBeenLastCalledWith(0);
226
- expect(keys.hasValidKey).toBe(false);
227
- });
273
+ expect(output.chunks).toEqual([]);
228
274
 
229
- it('mark as valid when a new key is set on same index', async () => {
230
- const { keys, input } = prepareParticipantTestDecoder(participantIdentity, {
231
- failureTolerance: 0,
232
- });
275
+ keys.decryptionFailure();
233
276
 
234
- const material = await createKeyMaterialFromString('password');
235
- await keys.setKey(material, 0);
277
+ input.write(mockEncryptedRTCEncodedVideoFrame(0));
278
+ await vitest.advanceTimersToNextTimerAsync();
236
279
 
237
- expect(keys.hasValidKey).toBe(true);
280
+ expect(output.chunks).toEqual([]);
281
+ } finally {
282
+ vitest.useRealTimers();
283
+ }
284
+ });
238
285
 
239
- input.write(mockEncryptedRTCEncodedVideoFrame(1));
286
+ it('calls decryptionFailure on missing key and emits error', async () => {
287
+ vitest.useFakeTimers();
288
+ try {
289
+ const { cryptor, keys, input } = prepareParticipantTestDecoder(participantIdentity, {});
290
+
291
+ const errorListener = vitest.fn();
292
+ cryptor.on(CryptorEvent.Error, errorListener);
293
+ vitest.spyOn(keys, 'decryptionFailure');
294
+
295
+ // no key is set at this index
296
+ input.write(mockEncryptedRTCEncodedVideoFrame(1));
297
+ await vitest.advanceTimersToNextTimerAsync();
298
+
299
+ expect(keys.decryptionFailure).toHaveBeenCalledTimes(1);
300
+ expect(keys.decryptionFailure).toHaveBeenCalledWith(1);
301
+ expect(errorListener).toHaveBeenCalled();
302
+ } finally {
303
+ vitest.useRealTimers();
304
+ }
305
+ });
240
306
 
241
- expect(keys.hasValidKey).toBe(false);
307
+ it('immediately drops frame if no key', async () => {
308
+ vitest.useFakeTimers();
309
+ try {
310
+ const { input, output } = prepareParticipantTestDecoder(participantIdentity, {});
242
311
 
243
- await keys.setKey(material, 0);
312
+ vitest.spyOn(crypto.subtle, 'decrypt');
244
313
 
245
- expect(keys.hasValidKey).toBe(true);
246
- });
314
+ input.write(mockEncryptedRTCEncodedVideoFrame(1));
315
+ await vitest.advanceTimersToNextTimerAsync();
247
316
 
248
- it('mark as valid when a new key is set on new index', async () => {
249
- const { keys, input } = prepareParticipantTestDecoder(participantIdentity, {
250
- failureTolerance: 0,
317
+ expect(crypto.subtle.decrypt).not.toHaveBeenCalled();
318
+ expect(output.chunks).toEqual([]);
319
+ } finally {
320
+ vitest.useRealTimers();
321
+ }
251
322
  });
252
323
 
253
- const material = await createKeyMaterialFromString('password');
254
- await keys.setKey(material, 0);
324
+ it('calls decryptionFailure with incorrect key and emits error', async () => {
325
+ vitest.useFakeTimers();
326
+ try {
327
+ const { cryptor, keys, input, output } = prepareParticipantTestDecoder(
328
+ participantIdentity,
329
+ { ratchetWindowSize: 0 },
330
+ );
331
+
332
+ vitest.spyOn(crypto.subtle, 'decrypt');
333
+ vitest.spyOn(keys, 'decryptionFailure');
334
+ const errorListener = vitest.fn();
335
+ cryptor.on(CryptorEvent.Error, errorListener);
336
+
337
+ await keys.setKey(await createKeyMaterialFromString('incorrect key'), 1);
338
+
339
+ const frame = mockRTCEncodedVideoFrame(
340
+ new Uint8Array([
341
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 254, 96, 91, 111, 187, 132, 31, 12, 207, 136, 17, 221,
342
+ 233, 116, 174, 6, 50, 37, 214, 71, 119, 196, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255,
343
+ 199, 51, 12, 1,
344
+ ]),
345
+ );
346
+ // global.RTCEncodedAudioFrame = vitest.fn();
347
+ input.write(frame);
348
+ await vitest.waitFor(() => expect(keys.decryptionFailure).toHaveBeenCalled());
349
+
350
+ expect(crypto.subtle.decrypt).toHaveBeenCalled();
351
+ expect(output.chunks).toEqual([]);
352
+ expect(errorListener).toHaveBeenCalled();
353
+ expect(keys.decryptionFailure).toHaveBeenCalledTimes(1);
354
+ expect(keys.decryptionFailure).toHaveBeenCalledWith(1);
355
+ } finally {
356
+ vitest.useRealTimers();
357
+ }
358
+ });
359
+
360
+ it('decrypts frame with correct key', async () => {
361
+ vitest.useFakeTimers();
362
+ try {
363
+ const { keys, input, output } = prepareParticipantTestDecoder(participantIdentity, {});
364
+
365
+ vitest.spyOn(keys, 'decryptionSuccess');
255
366
 
256
- expect(keys.hasValidKey).toBe(true);
367
+ await keys.setKey(await createKeyMaterialFromString('key1'), 1);
257
368
 
258
- input.write(mockEncryptedRTCEncodedVideoFrame(1));
369
+ const frame = mockRTCEncodedVideoFrame(
370
+ new Uint8Array([
371
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 254, 96, 91, 111, 187, 132, 31, 12, 207, 136, 17, 221,
372
+ 233, 116, 174, 6, 50, 37, 214, 71, 119, 196, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255,
373
+ 199, 51, 12, 1,
374
+ ]),
375
+ );
376
+ input.write(frame);
377
+ await vitest.waitFor(() => expect(output.chunks).toHaveLength(1));
259
378
 
260
- expect(keys.hasValidKey).toBe(false);
379
+ expect(output.chunks).toEqual([frame]);
261
380
 
262
- await keys.setKey(material, 1);
381
+ expect(keys.decryptionSuccess).toHaveBeenCalledTimes(1);
382
+ expect(keys.decryptionSuccess).toHaveBeenCalledWith(1);
263
383
 
264
- expect(keys.hasValidKey).toBe(true);
384
+ expect(frame.data.byteLength).toBe(16);
385
+
386
+ expect(new Uint8Array(frame.data)).toEqual(
387
+ new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
388
+ );
389
+ } finally {
390
+ vitest.useRealTimers();
391
+ }
392
+ });
393
+
394
+ it('recovers from delayed use of rotated key', async () => {
395
+ vitest.useFakeTimers();
396
+ try {
397
+ // 1. we (the local participant) have just joined a room and do not have the existing key (index 0) for the existing/remote participant
398
+ const { keys, input, output } = prepareParticipantTestDecoder(participantIdentity, {
399
+ failureTolerance: 1,
400
+ ratchetWindowSize: 0,
401
+ });
402
+ vitest.spyOn(keys, 'decryptionFailure');
403
+
404
+ // 2. we receive some frames from the existing participant encrypted with the existing key 0 that we don't have
405
+ input.write(mockEncryptedRTCEncodedVideoFrame(0));
406
+ input.write(mockEncryptedRTCEncodedVideoFrame(0));
407
+ input.write(mockEncryptedRTCEncodedVideoFrame(0));
408
+ input.write(mockEncryptedRTCEncodedVideoFrame(0));
409
+
410
+ // 3. we should have marked key at index 0 as invalid by now and dropped all the frames
411
+ await vitest.waitFor(() => expect(keys.decryptionFailure).toHaveBeenCalledTimes(2));
412
+ expect(keys.hasInvalidKeyAtIndex(0)).toBe(true);
413
+ expect(output.chunks).toEqual([]);
414
+
415
+ // 4. the existing participant then notices that we have joined the room and generates a new key (with a new key index 1)
416
+ // and distributes it out of band to us
417
+ await keys.setKey(await createKeyMaterialFromString('key1'), 1);
418
+
419
+ // 5. the existing participant waits a period of time before using the new key and continues sending media using the previous key 0.
420
+ // we receive these frames and should drop them as we still don't have the key.
421
+ input.write(mockEncryptedRTCEncodedVideoFrame(0));
422
+ input.write(mockEncryptedRTCEncodedVideoFrame(0));
423
+ input.write(mockEncryptedRTCEncodedVideoFrame(0));
424
+ input.write(mockEncryptedRTCEncodedVideoFrame(0));
425
+
426
+ await vitest.advanceTimersToNextTimerAsync();
427
+ expect(output.chunks).toEqual([]);
428
+
429
+ // 6. the existing participant moves over to the new key index 1 and we start to receive frames for index 1 that we
430
+ // should be able to decrypt even though we had the previous failures.
431
+ input.write(
432
+ mockRTCEncodedVideoFrame(
433
+ new Uint8Array([
434
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 254, 96, 91, 111, 187, 132, 31, 12, 207, 136, 17, 221,
435
+ 233, 116, 174, 6, 50, 37, 214, 71, 119, 196, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255,
436
+ 199, 51, 12, 1,
437
+ ]),
438
+ ),
439
+ );
440
+
441
+ input.write(
442
+ mockRTCEncodedVideoFrame(
443
+ new Uint8Array([
444
+ 99, 2, 3, 4, 5, 6, 7, 8, 9, 10, 154, 108, 209, 239, 253, 33, 72, 111, 13, 125, 10,
445
+ 101, 28, 209, 141, 162, 0, 238, 189, 254, 66, 156, 255, 255, 255, 255, 0, 0, 0, 0,
446
+ 255, 255, 96, 247, 12, 1,
447
+ ]),
448
+ ),
449
+ );
450
+
451
+ await vitest.waitFor(() => expect(output.chunks.length).toEqual(2));
452
+
453
+ expect(new Uint8Array(output.chunks[0].data)).toEqual(
454
+ new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
455
+ );
456
+ expect(new Uint8Array(output.chunks[1].data)).toEqual(
457
+ new Uint8Array([99, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
458
+ );
459
+ } finally {
460
+ vitest.useRealTimers();
461
+ }
462
+ });
265
463
  });
266
464
  });
@@ -357,10 +357,15 @@ export class FrameCryptor extends BaseFrameCryptor {
357
357
  const data = new Uint8Array(encodedFrame.data);
358
358
  const keyIndex = data[encodedFrame.data.byteLength - 1];
359
359
 
360
- if (this.keys.getKeySet(keyIndex) && this.keys.hasValidKey) {
360
+ if (this.keys.hasInvalidKeyAtIndex(keyIndex)) {
361
+ // drop frame
362
+ return;
363
+ }
364
+
365
+ if (this.keys.getKeySet(keyIndex)) {
361
366
  try {
362
367
  const decodedFrame = await this.decryptFrame(encodedFrame, keyIndex);
363
- this.keys.decryptionSuccess();
368
+ this.keys.decryptionSuccess(keyIndex);
364
369
  if (decodedFrame) {
365
370
  return controller.enqueue(decodedFrame);
366
371
  }
@@ -369,13 +374,13 @@ export class FrameCryptor extends BaseFrameCryptor {
369
374
  // emit an error if the key handler thinks we have a valid key
370
375
  if (this.keys.hasValidKey) {
371
376
  this.emit(CryptorEvent.Error, error);
372
- this.keys.decryptionFailure();
377
+ this.keys.decryptionFailure(keyIndex);
373
378
  }
374
379
  } else {
375
380
  workerLogger.warn('decoding frame failed', { error });
376
381
  }
377
382
  }
378
- } else if (!this.keys.getKeySet(keyIndex) && this.keys.hasValidKey) {
383
+ } else {
379
384
  // emit an error if the key index is out of bounds but the key handler thinks we still have a valid key
380
385
  workerLogger.warn(`skipping decryption due to missing key at index ${keyIndex}`);
381
386
  this.emit(
@@ -386,7 +391,7 @@ export class FrameCryptor extends BaseFrameCryptor {
386
391
  this.participantIdentity,
387
392
  ),
388
393
  );
389
- this.keys.decryptionFailure();
394
+ this.keys.decryptionFailure(keyIndex);
390
395
  }
391
396
  }
392
397