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.
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +49 -20
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +2 -2
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +25 -5
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/ts4.2/src/e2ee/worker/ParticipantKeyHandler.d.ts +25 -5
- package/package.json +2 -2
- package/src/e2ee/worker/FrameCryptor.test.ts +311 -113
- package/src/e2ee/worker/FrameCryptor.ts +10 -5
- package/src/e2ee/worker/ParticipantKeyHandler.test.ts +169 -5
- package/src/e2ee/worker/ParticipantKeyHandler.ts +50 -20
- package/src/e2ee/worker/__snapshots__/ParticipantKeyHandler.test.ts.snap +356 -0
- package/src/room/participant/LocalParticipant.ts +4 -1
@@ -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.
|
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
|
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('
|
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
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
64
|
-
|
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
|
-
|
73
|
-
|
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() {
|