livekit-client 2.5.7 → 2.5.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. package/README.md +2 -2
  2. package/dist/livekit-client.e2ee.worker.js +1 -1
  3. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  4. package/dist/livekit-client.e2ee.worker.mjs +53 -20
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +39 -3
  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/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/room/Room.d.ts +3 -1
  14. package/dist/src/room/Room.d.ts.map +1 -1
  15. package/dist/src/room/events.d.ts +5 -1
  16. package/dist/src/room/events.d.ts.map +1 -1
  17. package/dist/src/room/participant/LocalParticipant.d.ts +7 -0
  18. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  19. package/dist/ts4.2/src/e2ee/worker/ParticipantKeyHandler.d.ts +25 -5
  20. package/dist/ts4.2/src/room/Room.d.ts +3 -1
  21. package/dist/ts4.2/src/room/events.d.ts +5 -1
  22. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +7 -0
  23. package/package.json +7 -7
  24. package/src/e2ee/worker/FrameCryptor.test.ts +311 -113
  25. package/src/e2ee/worker/FrameCryptor.ts +10 -5
  26. package/src/e2ee/worker/ParticipantKeyHandler.test.ts +169 -5
  27. package/src/e2ee/worker/ParticipantKeyHandler.ts +50 -20
  28. package/src/e2ee/worker/__snapshots__/ParticipantKeyHandler.test.ts.snap +356 -0
  29. package/src/room/Room.ts +8 -0
  30. package/src/room/events.ts +5 -0
  31. package/src/room/participant/LocalParticipant.ts +26 -1
  32. package/src/room/track/LocalTrackPublication.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