livekit-client 2.5.8 → 2.5.9

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.
@@ -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
 
@@ -1,5 +1,6 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { KEY_PROVIDER_DEFAULTS } from '../constants';
1
+ import { describe, expect, it, vitest } from 'vitest';
2
+ import { ENCRYPTION_ALGORITHM, KEY_PROVIDER_DEFAULTS } from '../constants';
3
+ import { KeyHandlerEvent } from '../events';
3
4
  import { createKeyMaterialFromString } from '../utils';
4
5
  import { ParticipantKeyHandler } from './ParticipantKeyHandler';
5
6
 
@@ -35,11 +36,38 @@ describe('ParticipantKeyHandler', () => {
35
36
  expect(keyHandler.getKeySet(0)?.material).toEqual(materialB);
36
37
  });
37
38
 
38
- it('marks invalid if more than failureTolerance failures', async () => {
39
+ it('defaults to key index of 0 when setting key', async () => {
40
+ const keyHandler = new ParticipantKeyHandler(participantIdentity, {
41
+ ...KEY_PROVIDER_DEFAULTS,
42
+ });
43
+
44
+ const materialA = await createKeyMaterialFromString('passwordA');
45
+
46
+ await keyHandler.setKey(materialA);
47
+
48
+ expect(keyHandler.getKeySet(0)?.material).toEqual(materialA);
49
+ });
50
+
51
+ it('defaults to current key index when getting key', async () => {
52
+ const keyHandler = new ParticipantKeyHandler(participantIdentity, {
53
+ ...KEY_PROVIDER_DEFAULTS,
54
+ });
55
+
56
+ const materialA = await createKeyMaterialFromString('passwordA');
57
+
58
+ await keyHandler.setKey(materialA, 10);
59
+
60
+ expect(keyHandler.getKeySet()?.material).toEqual(materialA);
61
+ });
62
+
63
+ it('marks current key invalid if more than failureTolerance failures', async () => {
39
64
  const keyHandler = new ParticipantKeyHandler(participantIdentity, {
40
65
  ...KEY_PROVIDER_DEFAULTS,
41
66
  failureTolerance: 2,
42
67
  });
68
+
69
+ keyHandler.setCurrentKeyIndex(10);
70
+
43
71
  expect(keyHandler.hasValidKey).toBe(true);
44
72
 
45
73
  // 1
@@ -55,13 +83,16 @@ describe('ParticipantKeyHandler', () => {
55
83
  expect(keyHandler.hasValidKey).toBe(false);
56
84
  });
57
85
 
58
- it('marks valid on encryption success', async () => {
86
+ it('marks current key valid on encryption success', async () => {
59
87
  const keyHandler = new ParticipantKeyHandler(participantIdentity, {
60
88
  ...KEY_PROVIDER_DEFAULTS,
61
89
  failureTolerance: 0,
62
90
  });
63
91
 
92
+ keyHandler.setCurrentKeyIndex(10);
93
+
64
94
  expect(keyHandler.hasValidKey).toBe(true);
95
+ expect(keyHandler.hasInvalidKeyAtIndex(0)).toBe(false);
65
96
 
66
97
  keyHandler.decryptionFailure();
67
98
 
@@ -72,13 +103,62 @@ describe('ParticipantKeyHandler', () => {
72
103
  expect(keyHandler.hasValidKey).toBe(true);
73
104
  });
74
105
 
106
+ it('marks specific key invalid if more than failureTolerance failures', async () => {
107
+ const keyHandler = new ParticipantKeyHandler(participantIdentity, {
108
+ ...KEY_PROVIDER_DEFAULTS,
109
+ failureTolerance: 2,
110
+ });
111
+
112
+ // set the current key to something different from what we are testing
113
+ keyHandler.setCurrentKeyIndex(10);
114
+
115
+ expect(keyHandler.hasInvalidKeyAtIndex(5)).toBe(false);
116
+
117
+ // 1
118
+ keyHandler.decryptionFailure(5);
119
+ expect(keyHandler.hasInvalidKeyAtIndex(5)).toBe(false);
120
+
121
+ // 2
122
+ keyHandler.decryptionFailure(5);
123
+ expect(keyHandler.hasInvalidKeyAtIndex(5)).toBe(false);
124
+
125
+ // 3
126
+ keyHandler.decryptionFailure(5);
127
+ expect(keyHandler.hasInvalidKeyAtIndex(5)).toBe(true);
128
+
129
+ expect(keyHandler.hasInvalidKeyAtIndex(10)).toBe(false);
130
+ });
131
+
132
+ it('marks specific key valid on encryption success', async () => {
133
+ const keyHandler = new ParticipantKeyHandler(participantIdentity, {
134
+ ...KEY_PROVIDER_DEFAULTS,
135
+ failureTolerance: 0,
136
+ });
137
+
138
+ // set the current key to something different from what we are testing
139
+ keyHandler.setCurrentKeyIndex(10);
140
+
141
+ expect(keyHandler.hasInvalidKeyAtIndex(5)).toBe(false);
142
+
143
+ keyHandler.decryptionFailure(5);
144
+
145
+ expect(keyHandler.hasInvalidKeyAtIndex(5)).toBe(true);
146
+
147
+ keyHandler.decryptionSuccess(5);
148
+
149
+ expect(keyHandler.hasInvalidKeyAtIndex(5)).toBe(false);
150
+ });
151
+
75
152
  it('marks valid on new key', async () => {
76
153
  const keyHandler = new ParticipantKeyHandler(participantIdentity, {
77
154
  ...KEY_PROVIDER_DEFAULTS,
78
155
  failureTolerance: 0,
79
156
  });
80
157
 
158
+ keyHandler.setCurrentKeyIndex(10);
159
+
81
160
  expect(keyHandler.hasValidKey).toBe(true);
161
+ expect(keyHandler.hasInvalidKeyAtIndex(0)).toBe(false);
82
162
 
83
163
  keyHandler.decryptionFailure();
84
164
 
@@ -108,7 +188,14 @@ describe('ParticipantKeyHandler', () => {
108
188
  expect(keyHandler.getCurrentKeyIndex()).toBe(10);
109
189
  });
110
190
 
111
- it('allows many failures if failureTolerance is -1', async () => {
191
+ it('allows currentKeyIndex to be explicitly set', async () => {
192
+ const keyHandler = new ParticipantKeyHandler(participantIdentity, KEY_PROVIDER_DEFAULTS);
193
+
194
+ keyHandler.setCurrentKeyIndex(10);
195
+ expect(keyHandler.getCurrentKeyIndex()).toBe(10);
196
+ });
197
+
198
+ it('allows many failures if failureTolerance is less than zero', async () => {
112
199
  const keyHandler = new ParticipantKeyHandler(participantIdentity, {
113
200
  ...KEY_PROVIDER_DEFAULTS,
114
201
  failureTolerance: -1,
@@ -119,4 +206,81 @@ describe('ParticipantKeyHandler', () => {
119
206
  expect(keyHandler.hasValidKey).toBe(true);
120
207
  }
121
208
  });
209
+
210
+ describe('resetKeyStatus', () => {
211
+ it('marks all keys as valid if no index is provided', () => {
212
+ const keyHandler = new ParticipantKeyHandler(participantIdentity, {
213
+ ...KEY_PROVIDER_DEFAULTS,
214
+ failureTolerance: 0,
215
+ });
216
+
217
+ for (let i = 0; i < KEY_PROVIDER_DEFAULTS.keyringSize; i++) {
218
+ keyHandler.decryptionFailure(i);
219
+ expect(keyHandler.hasInvalidKeyAtIndex(i)).toBe(true);
220
+ }
221
+
222
+ keyHandler.resetKeyStatus();
223
+
224
+ for (let i = 0; i < KEY_PROVIDER_DEFAULTS.keyringSize; i++) {
225
+ expect(keyHandler.hasInvalidKeyAtIndex(i)).toBe(false);
226
+ }
227
+ });
228
+ });
229
+
230
+ describe('ratchetKey', () => {
231
+ it('emits event', async () => {
232
+ const keyHandler = new ParticipantKeyHandler(participantIdentity, KEY_PROVIDER_DEFAULTS);
233
+
234
+ const material = await createKeyMaterialFromString('password');
235
+
236
+ const keyRatched = vitest.fn();
237
+
238
+ keyHandler.on(KeyHandlerEvent.KeyRatcheted, keyRatched);
239
+
240
+ await keyHandler.setKey(material);
241
+
242
+ await keyHandler.ratchetKey();
243
+
244
+ const newMaterial = keyHandler.getKeySet()?.material;
245
+
246
+ expect(keyRatched).toHaveBeenCalledWith(newMaterial, participantIdentity, 0);
247
+ });
248
+
249
+ it('ratchets keys predictably', async () => {
250
+ // we can't extract the keys directly, so we instead use them to encrypt a known plaintext
251
+ const keyHandler = new ParticipantKeyHandler(participantIdentity, KEY_PROVIDER_DEFAULTS);
252
+
253
+ const originalMaterial = await createKeyMaterialFromString('password');
254
+
255
+ await keyHandler.setKey(originalMaterial);
256
+
257
+ const ciphertexts: Uint8Array[] = [];
258
+
259
+ const plaintext = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
260
+
261
+ const iv = new Uint8Array(12);
262
+ const additionalData = new Uint8Array(0);
263
+
264
+ for (let i = 0; i < 10; i++) {
265
+ const { encryptionKey } = keyHandler.getKeySet()!;
266
+
267
+ const ciphertext = await crypto.subtle.encrypt(
268
+ {
269
+ name: ENCRYPTION_ALGORITHM,
270
+ iv,
271
+ additionalData,
272
+ },
273
+ encryptionKey,
274
+ plaintext,
275
+ );
276
+ ciphertexts.push(new Uint8Array(ciphertext));
277
+ await keyHandler.ratchetKey();
278
+ }
279
+ // check that all ciphertexts are unique
280
+ expect(new Set(ciphertexts.map((x) => new TextDecoder().decode(x))).size).toEqual(
281
+ ciphertexts.length,
282
+ );
283
+ expect(ciphertexts).matchSnapshot('ciphertexts');
284
+ });
285
+ });
122
286
  });
@@ -21,18 +21,19 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
21
21
 
22
22
  private cryptoKeyRing: Array<KeySet | undefined>;
23
23
 
24
+ private decryptionFailureCounts: Array<number>;
25
+
24
26
  private keyProviderOptions: KeyProviderOptions;
25
27
 
26
28
  private ratchetPromiseMap: Map<number, Promise<CryptoKey>>;
27
29
 
28
30
  private participantIdentity: string;
29
31
 
30
- private decryptionFailureCount = 0;
31
-
32
- private _hasValidKey: boolean = true;
33
-
34
- get hasValidKey() {
35
- return this._hasValidKey;
32
+ /**
33
+ * true if the current key has not been marked as invalid
34
+ */
35
+ get hasValidKey(): boolean {
36
+ return !this.hasInvalidKeyAtIndex(this.currentKeyIndex);
36
37
  }
37
38
 
38
39
  constructor(participantIdentity: string, keyProviderOptions: KeyProviderOptions) {
@@ -42,35 +43,64 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
42
43
  throw new TypeError('Keyring size needs to be between 1 and 256');
43
44
  }
44
45
  this.cryptoKeyRing = new Array(keyProviderOptions.keyringSize).fill(undefined);
46
+ this.decryptionFailureCounts = new Array(keyProviderOptions.keyringSize).fill(0);
45
47
  this.keyProviderOptions = keyProviderOptions;
46
48
  this.ratchetPromiseMap = new Map();
47
49
  this.participantIdentity = participantIdentity;
48
- this.resetKeyStatus();
49
50
  }
50
51
 
51
- decryptionFailure() {
52
+ /**
53
+ * Returns true if the key at the given index is marked as invalid.
54
+ *
55
+ * @param keyIndex the index of the key
56
+ */
57
+ hasInvalidKeyAtIndex(keyIndex: number): boolean {
58
+ return (
59
+ this.keyProviderOptions.failureTolerance >= 0 &&
60
+ this.decryptionFailureCounts[keyIndex] > this.keyProviderOptions.failureTolerance
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Informs the key handler that a decryption failure occurred for an encryption key.
66
+ * @internal
67
+ * @param keyIndex the key index for which the failure occurred. Defaults to the current key index.
68
+ */
69
+ decryptionFailure(keyIndex: number = this.currentKeyIndex): void {
52
70
  if (this.keyProviderOptions.failureTolerance < 0) {
53
71
  return;
54
72
  }
55
- this.decryptionFailureCount += 1;
56
73
 
57
- if (this.decryptionFailureCount > this.keyProviderOptions.failureTolerance) {
58
- workerLogger.warn(`key for ${this.participantIdentity} is being marked as invalid`);
59
- this._hasValidKey = false;
74
+ this.decryptionFailureCounts[keyIndex] += 1;
75
+
76
+ if (this.decryptionFailureCounts[keyIndex] > this.keyProviderOptions.failureTolerance) {
77
+ workerLogger.warn(
78
+ `key for ${this.participantIdentity} at index ${keyIndex} is being marked as invalid`,
79
+ );
60
80
  }
61
81
  }
62
82
 
63
- decryptionSuccess() {
64
- this.resetKeyStatus();
83
+ /**
84
+ * Informs the key handler that a frame was successfully decrypted using an encryption key.
85
+ * @internal
86
+ * @param keyIndex the key index for which the success occurred. Defaults to the current key index.
87
+ */
88
+ decryptionSuccess(keyIndex: number = this.currentKeyIndex): void {
89
+ this.resetKeyStatus(keyIndex);
65
90
  }
66
91
 
67
92
  /**
68
93
  * Call this after user initiated ratchet or a new key has been set in order to make sure to mark potentially
69
94
  * invalid keys as valid again
95
+ *
96
+ * @param keyIndex the index of the key. Defaults to the current key index.
70
97
  */
71
- resetKeyStatus() {
72
- this.decryptionFailureCount = 0;
73
- this._hasValidKey = true;
98
+ resetKeyStatus(keyIndex?: number): void {
99
+ if (keyIndex === undefined) {
100
+ this.decryptionFailureCounts.fill(0);
101
+ } else {
102
+ this.decryptionFailureCounts[keyIndex] = 0;
103
+ }
74
104
  }
75
105
 
76
106
  /**
@@ -103,7 +133,7 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
103
133
  );
104
134
 
105
135
  if (setKey) {
106
- this.setKeyFromMaterial(newMaterial, currentKeyIndex, true);
136
+ await this.setKeyFromMaterial(newMaterial, currentKeyIndex, true);
107
137
  this.emit(
108
138
  KeyHandlerEvent.KeyRatcheted,
109
139
  newMaterial,
@@ -130,7 +160,7 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
130
160
  */
131
161
  async setKey(material: CryptoKey, keyIndex = 0) {
132
162
  await this.setKeyFromMaterial(material, keyIndex);
133
- this.resetKeyStatus();
163
+ this.resetKeyStatus(keyIndex);
134
164
  }
135
165
 
136
166
  /**
@@ -161,7 +191,7 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
161
191
 
162
192
  async setCurrentKeyIndex(index: number) {
163
193
  this.currentKeyIndex = index % this.cryptoKeyRing.length;
164
- this.resetKeyStatus();
194
+ this.resetKeyStatus(index);
165
195
  }
166
196
 
167
197
  getCurrentKeyIndex() {