livekit-client 2.5.5 → 2.5.7
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 +5443 -736
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +2129 -1954
- 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 +2 -2
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +2 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +2 -2
- package/dist/src/room/participant/Participant.d.ts +1 -1
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +1 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +1 -1
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/mocks.d.ts +1 -1
- package/dist/src/test/mocks.d.ts.map +1 -1
- package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +2 -2
- package/dist/ts4.2/src/index.d.ts +2 -2
- package/dist/ts4.2/src/room/Room.d.ts +2 -1
- package/dist/ts4.2/src/room/events.d.ts +2 -2
- package/dist/ts4.2/src/room/participant/Participant.d.ts +1 -1
- package/dist/ts4.2/src/room/track/Track.d.ts +1 -1
- package/dist/ts4.2/src/room/utils.d.ts +1 -1
- package/dist/ts4.2/src/test/mocks.d.ts +1 -1
- package/package.json +10 -11
- package/src/e2ee/E2eeManager.ts +8 -8
- package/src/e2ee/worker/FrameCryptor.test.ts +249 -2
- package/src/e2ee/worker/FrameCryptor.ts +13 -7
- package/src/e2ee/worker/ParticipantKeyHandler.test.ts +122 -0
- package/src/e2ee/worker/ParticipantKeyHandler.ts +1 -1
- package/src/e2ee/worker/e2ee.worker.ts +82 -78
- package/src/index.ts +2 -0
- package/src/room/Room.ts +16 -10
- package/src/room/events.ts +2 -2
- package/src/room/participant/Participant.ts +3 -2
- package/src/room/track/Track.ts +1 -1
- package/src/room/track/utils.ts +1 -1
- package/src/room/utils.ts +1 -1
- package/src/test/mocks.ts +1 -1
@@ -1,13 +1,109 @@
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
2
|
-
import {
|
1
|
+
import { afterEach, describe, expect, it, vitest } from 'vitest';
|
2
|
+
import { IV_LENGTH, KEY_PROVIDER_DEFAULTS } from '../constants';
|
3
|
+
import { CryptorEvent } from '../events';
|
4
|
+
import type { KeyProviderOptions } from '../types';
|
5
|
+
import { createKeyMaterialFromString } from '../utils';
|
6
|
+
import { FrameCryptor, encryptionEnabledMap, isFrameServerInjected } from './FrameCryptor';
|
7
|
+
import { ParticipantKeyHandler } from './ParticipantKeyHandler';
|
8
|
+
|
9
|
+
function mockEncryptedRTCEncodedVideoFrame(keyIndex: number): RTCEncodedVideoFrame {
|
10
|
+
const trailer = mockFrameTrailer(keyIndex);
|
11
|
+
const data = new Uint8Array(trailer.length + 10);
|
12
|
+
data.set(trailer, 10);
|
13
|
+
return mockRTCEncodedVideoFrame(data);
|
14
|
+
}
|
15
|
+
|
16
|
+
function mockRTCEncodedVideoFrame(data: Uint8Array): RTCEncodedVideoFrame {
|
17
|
+
return {
|
18
|
+
data: data.buffer,
|
19
|
+
timestamp: vitest.getMockedSystemTime()?.getTime() ?? 0,
|
20
|
+
type: 'key',
|
21
|
+
getMetadata(): RTCEncodedVideoFrameMetadata {
|
22
|
+
return {};
|
23
|
+
},
|
24
|
+
};
|
25
|
+
}
|
26
|
+
|
27
|
+
function mockFrameTrailer(keyIndex: number): Uint8Array {
|
28
|
+
const frameTrailer = new Uint8Array(2);
|
29
|
+
|
30
|
+
frameTrailer[0] = IV_LENGTH;
|
31
|
+
frameTrailer[1] = keyIndex;
|
32
|
+
|
33
|
+
return frameTrailer;
|
34
|
+
}
|
35
|
+
|
36
|
+
class TestUnderlyingSource<T> implements UnderlyingSource<T> {
|
37
|
+
controller: ReadableStreamController<T>;
|
38
|
+
|
39
|
+
start(controller: ReadableStreamController<T>): void {
|
40
|
+
this.controller = controller;
|
41
|
+
}
|
42
|
+
|
43
|
+
write(chunk: T): void {
|
44
|
+
this.controller.enqueue(chunk as any);
|
45
|
+
}
|
46
|
+
|
47
|
+
close(): void {
|
48
|
+
this.controller.close();
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
class TestUnderlyingSink<T> implements UnderlyingSink<T> {
|
53
|
+
public chunks: T[] = [];
|
54
|
+
|
55
|
+
write(chunk: T): void {
|
56
|
+
this.chunks.push(chunk);
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
function prepareParticipantTestDecoder(
|
61
|
+
participantIdentity: string,
|
62
|
+
partialKeyProviderOptions: Partial<KeyProviderOptions>,
|
63
|
+
): {
|
64
|
+
keys: ParticipantKeyHandler;
|
65
|
+
cryptor: FrameCryptor;
|
66
|
+
input: TestUnderlyingSource<RTCEncodedVideoFrame>;
|
67
|
+
output: TestUnderlyingSink<RTCEncodedVideoFrame>;
|
68
|
+
} {
|
69
|
+
const keyProviderOptions = { ...KEY_PROVIDER_DEFAULTS, ...partialKeyProviderOptions };
|
70
|
+
const keys = new ParticipantKeyHandler(participantIdentity, keyProviderOptions);
|
71
|
+
|
72
|
+
encryptionEnabledMap.set(participantIdentity, true);
|
73
|
+
|
74
|
+
const cryptor = new FrameCryptor({
|
75
|
+
participantIdentity,
|
76
|
+
keys,
|
77
|
+
keyProviderOptions,
|
78
|
+
sifTrailer: new Uint8Array(),
|
79
|
+
});
|
80
|
+
|
81
|
+
const input = new TestUnderlyingSource<RTCEncodedVideoFrame>();
|
82
|
+
const output = new TestUnderlyingSink<RTCEncodedVideoFrame>();
|
83
|
+
cryptor.setupTransform(
|
84
|
+
'decode',
|
85
|
+
new ReadableStream(input),
|
86
|
+
new WritableStream(output),
|
87
|
+
'testTrack',
|
88
|
+
);
|
89
|
+
|
90
|
+
return { keys, cryptor, input, output };
|
91
|
+
}
|
3
92
|
|
4
93
|
describe('FrameCryptor', () => {
|
94
|
+
const participantIdentity = 'testParticipant';
|
95
|
+
|
96
|
+
afterEach(() => {
|
97
|
+
encryptionEnabledMap.clear();
|
98
|
+
});
|
99
|
+
|
5
100
|
it('identifies server injected frame correctly', () => {
|
6
101
|
const frameTrailer = new TextEncoder().encode('LKROCKS');
|
7
102
|
const frameData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, ...frameTrailer]).buffer;
|
8
103
|
|
9
104
|
expect(isFrameServerInjected(frameData, frameTrailer)).toBe(true);
|
10
105
|
});
|
106
|
+
|
11
107
|
it('identifies server non server injected frame correctly', () => {
|
12
108
|
const frameTrailer = new TextEncoder().encode('LKROCKS');
|
13
109
|
const frameData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, ...frameTrailer, 10]);
|
@@ -16,4 +112,155 @@ describe('FrameCryptor', () => {
|
|
16
112
|
frameData.fill(0);
|
17
113
|
expect(isFrameServerInjected(frameData.buffer, frameTrailer)).toBe(false);
|
18
114
|
});
|
115
|
+
|
116
|
+
it('passthrough if participant encryption disabled', async () => {
|
117
|
+
vitest.useFakeTimers();
|
118
|
+
try {
|
119
|
+
const { input, output } = prepareParticipantTestDecoder(participantIdentity, {});
|
120
|
+
|
121
|
+
// disable encryption for participant
|
122
|
+
encryptionEnabledMap.set(participantIdentity, false);
|
123
|
+
|
124
|
+
const frame = mockEncryptedRTCEncodedVideoFrame(1);
|
125
|
+
|
126
|
+
input.write(frame);
|
127
|
+
await vitest.advanceTimersToNextTimerAsync();
|
128
|
+
|
129
|
+
expect(output.chunks).toEqual([frame]);
|
130
|
+
} finally {
|
131
|
+
vitest.useRealTimers();
|
132
|
+
}
|
133
|
+
});
|
134
|
+
|
135
|
+
it('passthrough for empty frame', async () => {
|
136
|
+
vitest.useFakeTimers();
|
137
|
+
try {
|
138
|
+
const { input, output } = prepareParticipantTestDecoder(participantIdentity, {});
|
139
|
+
|
140
|
+
// empty frame
|
141
|
+
const frame = mockRTCEncodedVideoFrame(new Uint8Array(0));
|
142
|
+
|
143
|
+
input.write(frame);
|
144
|
+
await vitest.advanceTimersToNextTimerAsync();
|
145
|
+
|
146
|
+
expect(output.chunks).toEqual([frame]);
|
147
|
+
} finally {
|
148
|
+
vitest.useRealTimers();
|
149
|
+
}
|
150
|
+
});
|
151
|
+
|
152
|
+
it('drops frames when invalid key', async () => {
|
153
|
+
vitest.useFakeTimers();
|
154
|
+
try {
|
155
|
+
const { keys, input, output } = prepareParticipantTestDecoder(participantIdentity, {
|
156
|
+
failureTolerance: 0,
|
157
|
+
});
|
158
|
+
|
159
|
+
expect(keys.hasValidKey).toBe(true);
|
160
|
+
|
161
|
+
await keys.setKey(await createKeyMaterialFromString('password'), 0);
|
162
|
+
|
163
|
+
input.write(mockEncryptedRTCEncodedVideoFrame(1));
|
164
|
+
await vitest.advanceTimersToNextTimerAsync();
|
165
|
+
|
166
|
+
expect(output.chunks).toEqual([]);
|
167
|
+
expect(keys.hasValidKey).toBe(false);
|
168
|
+
|
169
|
+
// this should still fail as keys are all marked as invalid
|
170
|
+
input.write(mockEncryptedRTCEncodedVideoFrame(0));
|
171
|
+
await vitest.advanceTimersToNextTimerAsync();
|
172
|
+
|
173
|
+
expect(output.chunks).toEqual([]);
|
174
|
+
expect(keys.hasValidKey).toBe(false);
|
175
|
+
} finally {
|
176
|
+
vitest.useRealTimers();
|
177
|
+
}
|
178
|
+
});
|
179
|
+
|
180
|
+
it('marks key invalid after too many failures', async () => {
|
181
|
+
const { keys, cryptor, input } = prepareParticipantTestDecoder(participantIdentity, {
|
182
|
+
failureTolerance: 1,
|
183
|
+
});
|
184
|
+
|
185
|
+
expect(keys.hasValidKey).toBe(true);
|
186
|
+
|
187
|
+
await keys.setKey(await createKeyMaterialFromString('password'), 0);
|
188
|
+
|
189
|
+
vitest.spyOn(keys, 'getKeySet');
|
190
|
+
vitest.spyOn(keys, 'decryptionFailure');
|
191
|
+
|
192
|
+
const errorListener = vitest.fn().mockImplementation((e) => {
|
193
|
+
console.log('error', e);
|
194
|
+
});
|
195
|
+
cryptor.on(CryptorEvent.Error, errorListener);
|
196
|
+
|
197
|
+
input.write(mockEncryptedRTCEncodedVideoFrame(1));
|
198
|
+
|
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);
|
205
|
+
|
206
|
+
vitest.clearAllMocks();
|
207
|
+
|
208
|
+
input.write(mockEncryptedRTCEncodedVideoFrame(1));
|
209
|
+
|
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);
|
216
|
+
|
217
|
+
vitest.clearAllMocks();
|
218
|
+
|
219
|
+
// this should still fail as keys are all marked as invalid
|
220
|
+
input.write(mockEncryptedRTCEncodedVideoFrame(0));
|
221
|
+
|
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
|
+
});
|
228
|
+
|
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
|
+
});
|
233
|
+
|
234
|
+
const material = await createKeyMaterialFromString('password');
|
235
|
+
await keys.setKey(material, 0);
|
236
|
+
|
237
|
+
expect(keys.hasValidKey).toBe(true);
|
238
|
+
|
239
|
+
input.write(mockEncryptedRTCEncodedVideoFrame(1));
|
240
|
+
|
241
|
+
expect(keys.hasValidKey).toBe(false);
|
242
|
+
|
243
|
+
await keys.setKey(material, 0);
|
244
|
+
|
245
|
+
expect(keys.hasValidKey).toBe(true);
|
246
|
+
});
|
247
|
+
|
248
|
+
it('mark as valid when a new key is set on new index', async () => {
|
249
|
+
const { keys, input } = prepareParticipantTestDecoder(participantIdentity, {
|
250
|
+
failureTolerance: 0,
|
251
|
+
});
|
252
|
+
|
253
|
+
const material = await createKeyMaterialFromString('password');
|
254
|
+
await keys.setKey(material, 0);
|
255
|
+
|
256
|
+
expect(keys.hasValidKey).toBe(true);
|
257
|
+
|
258
|
+
input.write(mockEncryptedRTCEncodedVideoFrame(1));
|
259
|
+
|
260
|
+
expect(keys.hasValidKey).toBe(false);
|
261
|
+
|
262
|
+
await keys.setKey(material, 1);
|
263
|
+
|
264
|
+
expect(keys.hasValidKey).toBe(true);
|
265
|
+
});
|
19
266
|
});
|
@@ -6,7 +6,7 @@ import { workerLogger } from '../../logger';
|
|
6
6
|
import type { VideoCodec } from '../../room/track/options';
|
7
7
|
import { ENCRYPTION_ALGORITHM, IV_LENGTH, UNENCRYPTED_BYTES } from '../constants';
|
8
8
|
import { CryptorError, CryptorErrorReason } from '../errors';
|
9
|
-
import { CryptorCallbacks, CryptorEvent } from '../events';
|
9
|
+
import { type CryptorCallbacks, CryptorEvent } from '../events';
|
10
10
|
import type { DecodeRatchetOptions, KeyProviderOptions, KeySet } from '../types';
|
11
11
|
import { deriveKeys, isVideoFrame, needsRbspUnescaping, parseRbsp, writeRbsp } from '../utils';
|
12
12
|
import type { ParticipantKeyHandler } from './ParticipantKeyHandler';
|
@@ -156,8 +156,8 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
156
156
|
|
157
157
|
setupTransform(
|
158
158
|
operation: 'encode' | 'decode',
|
159
|
-
readable: ReadableStream
|
160
|
-
writable: WritableStream
|
159
|
+
readable: ReadableStream<RTCEncodedVideoFrame | RTCEncodedAudioFrame>,
|
160
|
+
writable: WritableStream<RTCEncodedVideoFrame | RTCEncodedAudioFrame>,
|
161
161
|
trackId: string,
|
162
162
|
codec?: VideoCodec,
|
163
163
|
) {
|
@@ -233,11 +233,17 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
233
233
|
}
|
234
234
|
const keySet = this.keys.getKeySet();
|
235
235
|
if (!keySet) {
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
236
|
+
this.emit(
|
237
|
+
CryptorEvent.Error,
|
238
|
+
new CryptorError(
|
239
|
+
`key set not found for ${
|
240
|
+
this.participantIdentity
|
241
|
+
} at index ${this.keys.getCurrentKeyIndex()}`,
|
242
|
+
CryptorErrorReason.MissingKey,
|
243
|
+
this.participantIdentity,
|
244
|
+
),
|
240
245
|
);
|
246
|
+
return;
|
241
247
|
}
|
242
248
|
const { encryptionKey } = keySet;
|
243
249
|
const keyIndex = this.keys.getCurrentKeyIndex();
|
@@ -0,0 +1,122 @@
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
2
|
+
import { KEY_PROVIDER_DEFAULTS } from '../constants';
|
3
|
+
import { createKeyMaterialFromString } from '../utils';
|
4
|
+
import { ParticipantKeyHandler } from './ParticipantKeyHandler';
|
5
|
+
|
6
|
+
describe('ParticipantKeyHandler', () => {
|
7
|
+
const participantIdentity = 'testParticipant';
|
8
|
+
|
9
|
+
it('keyringSize must be greater than 0', () => {
|
10
|
+
expect(() => {
|
11
|
+
new ParticipantKeyHandler(participantIdentity, { ...KEY_PROVIDER_DEFAULTS, keyringSize: 0 });
|
12
|
+
}).toThrowError(TypeError);
|
13
|
+
});
|
14
|
+
|
15
|
+
it('keyringSize must be max 256', () => {
|
16
|
+
expect(() => {
|
17
|
+
new ParticipantKeyHandler(participantIdentity, {
|
18
|
+
...KEY_PROVIDER_DEFAULTS,
|
19
|
+
keyringSize: 257,
|
20
|
+
});
|
21
|
+
}).toThrowError(TypeError);
|
22
|
+
});
|
23
|
+
|
24
|
+
it('get and sets keys at an index', async () => {
|
25
|
+
const keyHandler = new ParticipantKeyHandler(participantIdentity, {
|
26
|
+
...KEY_PROVIDER_DEFAULTS,
|
27
|
+
keyringSize: 128,
|
28
|
+
});
|
29
|
+
const materialA = await createKeyMaterialFromString('passwordA');
|
30
|
+
const materialB = await createKeyMaterialFromString('passwordB');
|
31
|
+
await keyHandler.setKey(materialA, 0);
|
32
|
+
expect(keyHandler.getKeySet(0)).toBeDefined();
|
33
|
+
expect(keyHandler.getKeySet(0)?.material).toEqual(materialA);
|
34
|
+
await keyHandler.setKey(materialB, 0);
|
35
|
+
expect(keyHandler.getKeySet(0)?.material).toEqual(materialB);
|
36
|
+
});
|
37
|
+
|
38
|
+
it('marks invalid if more than failureTolerance failures', async () => {
|
39
|
+
const keyHandler = new ParticipantKeyHandler(participantIdentity, {
|
40
|
+
...KEY_PROVIDER_DEFAULTS,
|
41
|
+
failureTolerance: 2,
|
42
|
+
});
|
43
|
+
expect(keyHandler.hasValidKey).toBe(true);
|
44
|
+
|
45
|
+
// 1
|
46
|
+
keyHandler.decryptionFailure();
|
47
|
+
expect(keyHandler.hasValidKey).toBe(true);
|
48
|
+
|
49
|
+
// 2
|
50
|
+
keyHandler.decryptionFailure();
|
51
|
+
expect(keyHandler.hasValidKey).toBe(true);
|
52
|
+
|
53
|
+
// 3
|
54
|
+
keyHandler.decryptionFailure();
|
55
|
+
expect(keyHandler.hasValidKey).toBe(false);
|
56
|
+
});
|
57
|
+
|
58
|
+
it('marks valid on encryption success', async () => {
|
59
|
+
const keyHandler = new ParticipantKeyHandler(participantIdentity, {
|
60
|
+
...KEY_PROVIDER_DEFAULTS,
|
61
|
+
failureTolerance: 0,
|
62
|
+
});
|
63
|
+
|
64
|
+
expect(keyHandler.hasValidKey).toBe(true);
|
65
|
+
|
66
|
+
keyHandler.decryptionFailure();
|
67
|
+
|
68
|
+
expect(keyHandler.hasValidKey).toBe(false);
|
69
|
+
|
70
|
+
keyHandler.decryptionSuccess();
|
71
|
+
|
72
|
+
expect(keyHandler.hasValidKey).toBe(true);
|
73
|
+
});
|
74
|
+
|
75
|
+
it('marks valid on new key', async () => {
|
76
|
+
const keyHandler = new ParticipantKeyHandler(participantIdentity, {
|
77
|
+
...KEY_PROVIDER_DEFAULTS,
|
78
|
+
failureTolerance: 0,
|
79
|
+
});
|
80
|
+
|
81
|
+
expect(keyHandler.hasValidKey).toBe(true);
|
82
|
+
|
83
|
+
keyHandler.decryptionFailure();
|
84
|
+
|
85
|
+
expect(keyHandler.hasValidKey).toBe(false);
|
86
|
+
|
87
|
+
await keyHandler.setKey(await createKeyMaterialFromString('passwordA'));
|
88
|
+
|
89
|
+
expect(keyHandler.hasValidKey).toBe(true);
|
90
|
+
});
|
91
|
+
|
92
|
+
it('updates currentKeyIndex on new key', async () => {
|
93
|
+
const keyHandler = new ParticipantKeyHandler(participantIdentity, KEY_PROVIDER_DEFAULTS);
|
94
|
+
const material = await createKeyMaterialFromString('password');
|
95
|
+
|
96
|
+
expect(keyHandler.getCurrentKeyIndex()).toBe(0);
|
97
|
+
|
98
|
+
// default is zero
|
99
|
+
await keyHandler.setKey(material);
|
100
|
+
expect(keyHandler.getCurrentKeyIndex()).toBe(0);
|
101
|
+
|
102
|
+
// should go to next index
|
103
|
+
await keyHandler.setKey(material, 1);
|
104
|
+
expect(keyHandler.getCurrentKeyIndex()).toBe(1);
|
105
|
+
|
106
|
+
// should be able to jump ahead
|
107
|
+
await keyHandler.setKey(material, 10);
|
108
|
+
expect(keyHandler.getCurrentKeyIndex()).toBe(10);
|
109
|
+
});
|
110
|
+
|
111
|
+
it('allows many failures if failureTolerance is -1', async () => {
|
112
|
+
const keyHandler = new ParticipantKeyHandler(participantIdentity, {
|
113
|
+
...KEY_PROVIDER_DEFAULTS,
|
114
|
+
failureTolerance: -1,
|
115
|
+
});
|
116
|
+
expect(keyHandler.hasValidKey).toBe(true);
|
117
|
+
for (let i = 0; i < 100; i++) {
|
118
|
+
keyHandler.decryptionFailure();
|
119
|
+
expect(keyHandler.hasValidKey).toBe(true);
|
120
|
+
}
|
121
|
+
});
|
122
|
+
});
|
@@ -38,7 +38,7 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
|
|
38
38
|
constructor(participantIdentity: string, keyProviderOptions: KeyProviderOptions) {
|
39
39
|
super();
|
40
40
|
this.currentKeyIndex = 0;
|
41
|
-
if (keyProviderOptions.keyringSize < 1 || keyProviderOptions.keyringSize >
|
41
|
+
if (keyProviderOptions.keyringSize < 1 || keyProviderOptions.keyringSize > 256) {
|
42
42
|
throw new TypeError('Keyring size needs to be between 1 and 256');
|
43
43
|
}
|
44
44
|
this.cryptoKeyRing = new Array(keyProviderOptions.keyringSize).fill(undefined);
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import { workerLogger } from '../../logger';
|
2
|
-
import { VideoCodec } from '../../room/track/options';
|
2
|
+
import type { VideoCodec } from '../../room/track/options';
|
3
|
+
import { AsyncQueue } from '../../utils/AsyncQueue';
|
3
4
|
import { KEY_PROVIDER_DEFAULTS } from '../constants';
|
4
5
|
import { CryptorErrorReason } from '../errors';
|
5
6
|
import { CryptorEvent, KeyHandlerEvent } from '../events';
|
@@ -17,6 +18,7 @@ import { ParticipantKeyHandler } from './ParticipantKeyHandler';
|
|
17
18
|
const participantCryptors: FrameCryptor[] = [];
|
18
19
|
const participantKeys: Map<string, ParticipantKeyHandler> = new Map();
|
19
20
|
let sharedKeyHandler: ParticipantKeyHandler | undefined;
|
21
|
+
let messageQueue = new AsyncQueue();
|
20
22
|
|
21
23
|
let isEncryptionEnabled: boolean = false;
|
22
24
|
|
@@ -31,85 +33,87 @@ let rtpMap: Map<number, VideoCodec> = new Map();
|
|
31
33
|
workerLogger.setDefaultLevel('info');
|
32
34
|
|
33
35
|
onmessage = (ev) => {
|
34
|
-
|
36
|
+
messageQueue.run(async () => {
|
37
|
+
const { kind, data }: E2EEWorkerMessage = ev.data;
|
35
38
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
workerLogger.info(
|
52
|
-
`updated e2ee enabled status for ${data.participantIdentity} to ${data.enabled}`,
|
53
|
-
);
|
54
|
-
// acknowledge enable call successful
|
55
|
-
postMessage(ev.data);
|
56
|
-
break;
|
57
|
-
case 'decode':
|
58
|
-
let cryptor = getTrackCryptor(data.participantIdentity, data.trackId);
|
59
|
-
cryptor.setupTransform(
|
60
|
-
kind,
|
61
|
-
data.readableStream,
|
62
|
-
data.writableStream,
|
63
|
-
data.trackId,
|
64
|
-
data.codec,
|
65
|
-
);
|
66
|
-
break;
|
67
|
-
case 'encode':
|
68
|
-
let pubCryptor = getTrackCryptor(data.participantIdentity, data.trackId);
|
69
|
-
pubCryptor.setupTransform(
|
70
|
-
kind,
|
71
|
-
data.readableStream,
|
72
|
-
data.writableStream,
|
73
|
-
data.trackId,
|
74
|
-
data.codec,
|
75
|
-
);
|
76
|
-
break;
|
77
|
-
case 'setKey':
|
78
|
-
if (useSharedKey) {
|
79
|
-
setSharedKey(data.key, data.keyIndex);
|
80
|
-
} else if (data.participantIdentity) {
|
39
|
+
switch (kind) {
|
40
|
+
case 'init':
|
41
|
+
workerLogger.setLevel(data.loglevel);
|
42
|
+
workerLogger.info('worker initialized');
|
43
|
+
keyProviderOptions = data.keyProviderOptions;
|
44
|
+
useSharedKey = !!data.keyProviderOptions.sharedKey;
|
45
|
+
// acknowledge init successful
|
46
|
+
const ackMsg: InitAck = {
|
47
|
+
kind: 'initAck',
|
48
|
+
data: { enabled: isEncryptionEnabled },
|
49
|
+
};
|
50
|
+
postMessage(ackMsg);
|
51
|
+
break;
|
52
|
+
case 'enable':
|
53
|
+
setEncryptionEnabled(data.enabled, data.participantIdentity);
|
81
54
|
workerLogger.info(
|
82
|
-
`
|
55
|
+
`updated e2ee enabled status for ${data.participantIdentity} to ${data.enabled}`,
|
83
56
|
);
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
57
|
+
// acknowledge enable call successful
|
58
|
+
postMessage(ev.data);
|
59
|
+
break;
|
60
|
+
case 'decode':
|
61
|
+
let cryptor = getTrackCryptor(data.participantIdentity, data.trackId);
|
62
|
+
cryptor.setupTransform(
|
63
|
+
kind,
|
64
|
+
data.readableStream,
|
65
|
+
data.writableStream,
|
66
|
+
data.trackId,
|
67
|
+
data.codec,
|
68
|
+
);
|
69
|
+
break;
|
70
|
+
case 'encode':
|
71
|
+
let pubCryptor = getTrackCryptor(data.participantIdentity, data.trackId);
|
72
|
+
pubCryptor.setupTransform(
|
73
|
+
kind,
|
74
|
+
data.readableStream,
|
75
|
+
data.writableStream,
|
76
|
+
data.trackId,
|
77
|
+
data.codec,
|
78
|
+
);
|
79
|
+
break;
|
80
|
+
case 'setKey':
|
81
|
+
if (useSharedKey) {
|
82
|
+
await setSharedKey(data.key, data.keyIndex);
|
83
|
+
} else if (data.participantIdentity) {
|
84
|
+
workerLogger.info(
|
85
|
+
`set participant sender key ${data.participantIdentity} index ${data.keyIndex}`,
|
86
|
+
);
|
87
|
+
await getParticipantKeyHandler(data.participantIdentity).setKey(data.key, data.keyIndex);
|
88
|
+
} else {
|
89
|
+
workerLogger.error('no participant Id was provided and shared key usage is disabled');
|
101
90
|
}
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
91
|
+
break;
|
92
|
+
case 'removeTransform':
|
93
|
+
unsetCryptorParticipant(data.trackId, data.participantIdentity);
|
94
|
+
break;
|
95
|
+
case 'updateCodec':
|
96
|
+
getTrackCryptor(data.participantIdentity, data.trackId).setVideoCodec(data.codec);
|
97
|
+
break;
|
98
|
+
case 'setRTPMap':
|
99
|
+
// this is only used for the local participant
|
100
|
+
rtpMap = data.map;
|
101
|
+
participantCryptors.forEach((cr) => {
|
102
|
+
if (cr.getParticipantIdentity() === data.participantIdentity) {
|
103
|
+
cr.setRtpMap(data.map);
|
104
|
+
}
|
105
|
+
});
|
106
|
+
break;
|
107
|
+
case 'ratchetRequest':
|
108
|
+
handleRatchetRequest(data);
|
109
|
+
break;
|
110
|
+
case 'setSifTrailer':
|
111
|
+
handleSifTrailer(data.trailer);
|
112
|
+
break;
|
113
|
+
default:
|
114
|
+
break;
|
115
|
+
}
|
116
|
+
});
|
113
117
|
};
|
114
118
|
|
115
119
|
async function handleRatchetRequest(data: RatchetRequestMessage['data']) {
|
@@ -210,9 +214,9 @@ function setEncryptionEnabled(enable: boolean, participantIdentity: string) {
|
|
210
214
|
encryptionEnabledMap.set(participantIdentity, enable);
|
211
215
|
}
|
212
216
|
|
213
|
-
function setSharedKey(key: CryptoKey, index?: number) {
|
217
|
+
async function setSharedKey(key: CryptoKey, index?: number) {
|
214
218
|
workerLogger.info('set shared key', { index });
|
215
|
-
getSharedKeyHandler().setKey(key, index);
|
219
|
+
await getSharedKeyHandler().setKey(key, index);
|
216
220
|
}
|
217
221
|
|
218
222
|
function setupCryptorErrorEvents(cryptor: FrameCryptor) {
|
package/src/index.ts
CHANGED
@@ -27,6 +27,7 @@ import type { LiveKitReactNativeInfo } from './room/types';
|
|
27
27
|
import type { AudioAnalyserOptions } from './room/utils';
|
28
28
|
import {
|
29
29
|
Mutex,
|
30
|
+
compareVersions,
|
30
31
|
createAudioAnalyser,
|
31
32
|
getEmptyAudioStreamTrack,
|
32
33
|
getEmptyVideoStreamTrack,
|
@@ -81,6 +82,7 @@ export {
|
|
81
82
|
Room,
|
82
83
|
SubscriptionError,
|
83
84
|
TrackPublication,
|
85
|
+
compareVersions,
|
84
86
|
createAudioAnalyser,
|
85
87
|
getBrowser,
|
86
88
|
getEmptyAudioStreamTrack,
|