livekit-client 2.5.8 → 2.5.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -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() {