livekit-client 2.15.6 → 2.15.8
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 +253 -118
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +1892 -153
- 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/connectionHelper/checks/publishVideo.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts +16 -2
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/types.d.ts +35 -1
- package/dist/src/e2ee/types.d.ts.map +1 -1
- package/dist/src/e2ee/utils.d.ts +2 -0
- package/dist/src/e2ee/utils.d.ts.map +1 -1
- package/dist/src/e2ee/worker/DataCryptor.d.ts +15 -0
- package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -0
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +3 -2
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
- package/dist/src/e2ee/worker/sifPayload.d.ts +6 -6
- package/dist/src/e2ee/worker/sifPayload.d.ts.map +1 -1
- package/dist/src/index.d.ts +5 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logger.d.ts +1 -0
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/options.d.ts +4 -2
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +5 -2
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +3 -2
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +2 -2
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/errors.d.ts +2 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +1 -3
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +2 -2
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/token-source/TokenSource.d.ts +70 -0
- package/dist/src/room/token-source/TokenSource.d.ts.map +1 -0
- package/dist/src/room/token-source/types.d.ts +68 -0
- package/dist/src/room/token-source/types.d.ts.map +1 -0
- package/dist/src/room/token-source/utils.d.ts +5 -0
- package/dist/src/room/token-source/utils.d.ts.map +1 -0
- package/dist/src/room/track/LocalTrack.d.ts +1 -1
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +7 -3
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/track/utils.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +1 -0
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +2 -1
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/utils/camelToSnakeCase.d.ts +8 -0
- package/dist/src/utils/camelToSnakeCase.d.ts.map +1 -0
- package/dist/ts4.2/{src/e2ee → e2ee}/E2eeManager.d.ts +16 -2
- package/dist/ts4.2/{src/e2ee → e2ee}/types.d.ts +35 -1
- package/dist/ts4.2/{src/e2ee → e2ee}/utils.d.ts +3 -0
- package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +15 -0
- package/dist/ts4.2/{src/e2ee → e2ee}/worker/ParticipantKeyHandler.d.ts +3 -2
- package/dist/ts4.2/{src/e2ee → e2ee}/worker/sifPayload.d.ts +6 -6
- package/dist/ts4.2/{src/index.d.ts → index.d.ts} +5 -3
- package/dist/ts4.2/{src/logger.d.ts → logger.d.ts} +1 -0
- package/dist/ts4.2/{src/options.d.ts → options.d.ts} +4 -2
- package/dist/ts4.2/{src/room → room}/RTCEngine.d.ts +5 -2
- package/dist/ts4.2/{src/room → room}/Room.d.ts +3 -2
- package/dist/ts4.2/{src/room → room}/data-stream/incoming/IncomingDataStreamManager.d.ts +2 -1
- package/dist/ts4.2/{src/room → room}/errors.d.ts +2 -1
- package/dist/ts4.2/{src/room → room}/participant/LocalParticipant.d.ts +1 -3
- package/dist/ts4.2/{src/room → room}/participant/Participant.d.ts +2 -2
- package/dist/ts4.2/room/token-source/TokenSource.d.ts +71 -0
- package/dist/ts4.2/room/token-source/types.d.ts +68 -0
- package/dist/ts4.2/room/token-source/utils.d.ts +5 -0
- package/dist/ts4.2/{src/room → room}/track/LocalTrack.d.ts +1 -1
- package/dist/ts4.2/{src/room → room}/track/options.d.ts +10 -3
- package/dist/ts4.2/{src/room → room}/types.d.ts +1 -0
- package/dist/ts4.2/{src/room → room}/utils.d.ts +2 -1
- package/dist/ts4.2/utils/camelToSnakeCase.d.ts +8 -0
- package/package.json +14 -12
- package/src/connectionHelper/checks/publishVideo.ts +5 -0
- package/src/e2ee/E2eeManager.ts +94 -2
- package/src/e2ee/types.ts +44 -1
- package/src/e2ee/utils.ts +16 -0
- package/src/e2ee/worker/DataCryptor.test.ts +271 -0
- package/src/e2ee/worker/DataCryptor.ts +147 -0
- package/src/e2ee/worker/ParticipantKeyHandler.ts +4 -3
- package/src/e2ee/worker/e2ee.worker.ts +47 -0
- package/src/e2ee/worker/sifPayload.ts +10 -6
- package/src/index.ts +14 -1
- package/src/logger.ts +1 -0
- package/src/options.ts +8 -2
- package/src/room/PCTransport.ts +14 -5
- package/src/room/RTCEngine.ts +55 -6
- package/src/room/Room.ts +39 -17
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +64 -17
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +7 -0
- package/src/room/errors.ts +3 -0
- package/src/room/participant/LocalParticipant.ts +17 -29
- package/src/room/participant/Participant.ts +6 -1
- package/src/room/token-source/TokenSource.ts +285 -0
- package/src/room/token-source/types.ts +84 -0
- package/src/room/token-source/utils.ts +35 -0
- package/src/room/track/LocalAudioTrack.ts +1 -1
- package/src/room/track/LocalTrack.ts +1 -1
- package/src/room/track/options.ts +12 -4
- package/src/room/track/utils.ts +10 -2
- package/src/room/types.ts +1 -0
- package/src/room/utils.ts +8 -4
- package/src/utils/camelToSnakeCase.ts +16 -0
- /package/dist/ts4.2/{src/api → api}/SignalClient.d.ts +0 -0
- /package/dist/ts4.2/{src/api → api}/utils.d.ts +0 -0
- /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/ConnectionCheck.d.ts +0 -0
- /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/Checker.d.ts +0 -0
- /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/cloudRegion.d.ts +0 -0
- /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/connectionProtocol.d.ts +0 -0
- /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/publishAudio.d.ts +0 -0
- /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/publishVideo.d.ts +0 -0
- /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/reconnect.d.ts +0 -0
- /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/turn.d.ts +0 -0
- /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/webrtc.d.ts +0 -0
- /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/websocket.d.ts +0 -0
- /package/dist/ts4.2/{src/e2ee → e2ee}/KeyProvider.d.ts +0 -0
- /package/dist/ts4.2/{src/e2ee → e2ee}/constants.d.ts +0 -0
- /package/dist/ts4.2/{src/e2ee → e2ee}/errors.d.ts +0 -0
- /package/dist/ts4.2/{src/e2ee → e2ee}/events.d.ts +0 -0
- /package/dist/ts4.2/{src/e2ee → e2ee}/index.d.ts +0 -0
- /package/dist/ts4.2/{src/e2ee → e2ee}/worker/FrameCryptor.d.ts +0 -0
- /package/dist/ts4.2/{src/e2ee → e2ee}/worker/e2ee.worker.d.ts +0 -0
- /package/dist/ts4.2/{src/e2ee → e2ee}/worker/naluUtils.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/DefaultReconnectPolicy.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/DeviceManager.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/PCTransport.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/PCTransportManager.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/ReconnectPolicy.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/RegionUrlProvider.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/attribute-typings.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/data-stream/incoming/StreamReader.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/data-stream/outgoing/OutgoingDataStreamManager.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/data-stream/outgoing/StreamWriter.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/defaults.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/events.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/participant/ParticipantTrackPermission.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/participant/RemoteParticipant.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/participant/publishUtils.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/rpc.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/stats.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/timers.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/LocalAudioTrack.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/LocalTrackPublication.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/LocalVideoTrack.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/RemoteAudioTrack.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/RemoteTrack.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/RemoteTrackPublication.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/RemoteVideoTrack.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/Track.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/TrackPublication.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/create.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/facingMode.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/processor/types.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/record.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/types.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/utils.d.ts +0 -0
- /package/dist/ts4.2/{src/test → test}/MockMediaStreamTrack.d.ts +0 -0
- /package/dist/ts4.2/{src/test → test}/mocks.d.ts +0 -0
- /package/dist/ts4.2/{src/utils → utils}/AsyncQueue.d.ts +0 -0
- /package/dist/ts4.2/{src/utils → utils}/browserParser.d.ts +0 -0
- /package/dist/ts4.2/{src/utils → utils}/cloneDeep.d.ts +0 -0
- /package/dist/ts4.2/{src/utils → utils}/dataPacketBuffer.d.ts +0 -0
- /package/dist/ts4.2/{src/utils → utils}/ttlmap.d.ts +0 -0
- /package/dist/ts4.2/{src/version.d.ts → version.d.ts} +0 -0
@@ -0,0 +1,271 @@
|
|
1
|
+
import { describe, expect, it, vitest } from 'vitest';
|
2
|
+
import { IV_LENGTH, KEY_PROVIDER_DEFAULTS } from '../constants';
|
3
|
+
import type { KeyProviderOptions } from '../types';
|
4
|
+
import { createKeyMaterialFromString } from '../utils';
|
5
|
+
import { DataCryptor } from './DataCryptor';
|
6
|
+
import { ParticipantKeyHandler } from './ParticipantKeyHandler';
|
7
|
+
|
8
|
+
function prepareParticipantTestKeys(
|
9
|
+
participantIdentity: string,
|
10
|
+
partialKeyProviderOptions: Partial<KeyProviderOptions>,
|
11
|
+
): ParticipantKeyHandler {
|
12
|
+
const keyProviderOptions = { ...KEY_PROVIDER_DEFAULTS, ...partialKeyProviderOptions };
|
13
|
+
return new ParticipantKeyHandler(participantIdentity, keyProviderOptions);
|
14
|
+
}
|
15
|
+
|
16
|
+
describe('DataCryptor', () => {
|
17
|
+
const participantIdentity = 'testParticipant';
|
18
|
+
|
19
|
+
describe('encrypt', () => {
|
20
|
+
it('throws error when no key set', async () => {
|
21
|
+
const keys = prepareParticipantTestKeys(participantIdentity, {});
|
22
|
+
const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
|
23
|
+
|
24
|
+
await expect(DataCryptor.encrypt(data, keys)).rejects.toThrow('No key set found');
|
25
|
+
});
|
26
|
+
|
27
|
+
it('encrypts data successfully with key', async () => {
|
28
|
+
const keys = prepareParticipantTestKeys(participantIdentity, {});
|
29
|
+
await keys.setKey(await createKeyMaterialFromString('test-key'), 1);
|
30
|
+
|
31
|
+
const plainData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
|
32
|
+
const result = await DataCryptor.encrypt(plainData, keys);
|
33
|
+
|
34
|
+
expect(result.payload).toBeInstanceOf(Uint8Array);
|
35
|
+
expect(result.iv).toBeInstanceOf(Uint8Array);
|
36
|
+
expect(result.iv.length).toBe(IV_LENGTH);
|
37
|
+
expect(result.keyIndex).toBe(1);
|
38
|
+
expect(result.payload).not.toEqual(plainData);
|
39
|
+
expect(result.payload.length).toBeGreaterThan(0);
|
40
|
+
});
|
41
|
+
|
42
|
+
it('generates different IV for each encryption', async () => {
|
43
|
+
const keys = prepareParticipantTestKeys(participantIdentity, {});
|
44
|
+
await keys.setKey(await createKeyMaterialFromString('test-key'), 1);
|
45
|
+
|
46
|
+
const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
|
47
|
+
|
48
|
+
const result1 = await DataCryptor.encrypt(data, keys);
|
49
|
+
const result2 = await DataCryptor.encrypt(data, keys);
|
50
|
+
|
51
|
+
expect(result1.iv).not.toEqual(result2.iv);
|
52
|
+
expect(result1.payload).not.toEqual(result2.payload);
|
53
|
+
});
|
54
|
+
|
55
|
+
it('uses correct key index from key handler', async () => {
|
56
|
+
const keys = prepareParticipantTestKeys(participantIdentity, {});
|
57
|
+
await keys.setKey(await createKeyMaterialFromString('test-key'), 5);
|
58
|
+
|
59
|
+
const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
|
60
|
+
const result = await DataCryptor.encrypt(data, keys);
|
61
|
+
|
62
|
+
expect(result.keyIndex).toBe(5);
|
63
|
+
});
|
64
|
+
});
|
65
|
+
|
66
|
+
describe('decrypt', () => {
|
67
|
+
it('throws error when no key set for index', async () => {
|
68
|
+
const keys = prepareParticipantTestKeys(participantIdentity, {});
|
69
|
+
const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
|
70
|
+
const iv = new Uint8Array(IV_LENGTH);
|
71
|
+
|
72
|
+
await expect(DataCryptor.decrypt(data, iv, keys, 1)).rejects.toThrow('No key set found');
|
73
|
+
});
|
74
|
+
|
75
|
+
it('decrypts data successfully with correct key', async () => {
|
76
|
+
const keys = prepareParticipantTestKeys(participantIdentity, {});
|
77
|
+
await keys.setKey(await createKeyMaterialFromString('test-key'), 1);
|
78
|
+
|
79
|
+
const plainData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
|
80
|
+
|
81
|
+
// First encrypt the data
|
82
|
+
const encrypted = await DataCryptor.encrypt(plainData, keys);
|
83
|
+
|
84
|
+
// Then decrypt it
|
85
|
+
const decrypted = await DataCryptor.decrypt(
|
86
|
+
encrypted.payload,
|
87
|
+
encrypted.iv,
|
88
|
+
keys,
|
89
|
+
encrypted.keyIndex,
|
90
|
+
);
|
91
|
+
|
92
|
+
expect(decrypted.payload).toEqual(plainData);
|
93
|
+
});
|
94
|
+
|
95
|
+
it('fails to decrypt with incorrect key', async () => {
|
96
|
+
const keys1 = prepareParticipantTestKeys('participant1', {});
|
97
|
+
const keys2 = prepareParticipantTestKeys('participant2', {});
|
98
|
+
|
99
|
+
await keys1.setKey(await createKeyMaterialFromString('correct-key'), 1);
|
100
|
+
await keys2.setKey(await createKeyMaterialFromString('wrong-key'), 1);
|
101
|
+
|
102
|
+
const plainData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
|
103
|
+
|
104
|
+
// Encrypt with first key
|
105
|
+
const encrypted = await DataCryptor.encrypt(plainData, keys1);
|
106
|
+
|
107
|
+
// Try to decrypt with second (wrong) key
|
108
|
+
await expect(
|
109
|
+
DataCryptor.decrypt(encrypted.payload, encrypted.iv, keys2, encrypted.keyIndex),
|
110
|
+
).rejects.toThrow();
|
111
|
+
});
|
112
|
+
|
113
|
+
it('handles ratcheting when enabled', async () => {
|
114
|
+
const senderKeys = prepareParticipantTestKeys('sender', {
|
115
|
+
ratchetWindowSize: 2,
|
116
|
+
});
|
117
|
+
const receiverKeys = prepareParticipantTestKeys('receiver', {
|
118
|
+
ratchetWindowSize: 2,
|
119
|
+
});
|
120
|
+
|
121
|
+
// Both start with the same initial key
|
122
|
+
const initialMaterial = await createKeyMaterialFromString('test-key');
|
123
|
+
await senderKeys.setKey(initialMaterial, 1);
|
124
|
+
await receiverKeys.setKey(initialMaterial, 1);
|
125
|
+
|
126
|
+
const plainData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
|
127
|
+
|
128
|
+
// Sender ratchets their key forward
|
129
|
+
await senderKeys.ratchetKey(1, false);
|
130
|
+
|
131
|
+
// Sender encrypts data with the ratcheted key
|
132
|
+
const encrypted = await DataCryptor.encrypt(plainData, senderKeys);
|
133
|
+
|
134
|
+
// Receiver should be able to decrypt by automatically ratcheting their key
|
135
|
+
const decrypted = await DataCryptor.decrypt(
|
136
|
+
encrypted.payload,
|
137
|
+
encrypted.iv,
|
138
|
+
receiverKeys,
|
139
|
+
encrypted.keyIndex,
|
140
|
+
);
|
141
|
+
|
142
|
+
expect(decrypted.payload).toEqual(plainData);
|
143
|
+
});
|
144
|
+
|
145
|
+
it('respects ratchet window size limit', async () => {
|
146
|
+
// Create a scenario where we have valid encrypted data that requires ratcheting but it's disabled
|
147
|
+
const senderKeys = prepareParticipantTestKeys('sender', {
|
148
|
+
ratchetWindowSize: 10, // Large window for sender
|
149
|
+
});
|
150
|
+
const receiverKeys = prepareParticipantTestKeys('receiver', {
|
151
|
+
ratchetWindowSize: 1, // No ratcheting allowed for receiver
|
152
|
+
});
|
153
|
+
|
154
|
+
// Both start with the same initial key
|
155
|
+
const initialMaterial = await createKeyMaterialFromString('test-key');
|
156
|
+
await senderKeys.setKey(initialMaterial, 1);
|
157
|
+
await receiverKeys.setKey(initialMaterial, 1);
|
158
|
+
|
159
|
+
const plainData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
|
160
|
+
|
161
|
+
// Sender ratchets their key forward once
|
162
|
+
await senderKeys.ratchetKey(1);
|
163
|
+
await senderKeys.ratchetKey(1);
|
164
|
+
|
165
|
+
// Sender encrypts data with the ratcheted key
|
166
|
+
const encrypted = await DataCryptor.encrypt(plainData, senderKeys);
|
167
|
+
|
168
|
+
// Receiver should fail to decrypt with invalid key because ratcheting is limited (window size 1)
|
169
|
+
await expect(
|
170
|
+
DataCryptor.decrypt(encrypted.payload, encrypted.iv, receiverKeys, encrypted.keyIndex),
|
171
|
+
).rejects.toThrow('valid key missing for participant');
|
172
|
+
});
|
173
|
+
|
174
|
+
it('throws CryptorError when ratcheting disabled and decryption fails', async () => {
|
175
|
+
const keys = prepareParticipantTestKeys(participantIdentity, {
|
176
|
+
ratchetWindowSize: 0,
|
177
|
+
});
|
178
|
+
|
179
|
+
await keys.setKey(await createKeyMaterialFromString('test-key'), 1);
|
180
|
+
|
181
|
+
const invalidData = new Uint8Array([99, 98, 97, 96, 95, 94, 93, 92]);
|
182
|
+
const iv = new Uint8Array(IV_LENGTH);
|
183
|
+
crypto.getRandomValues(iv);
|
184
|
+
|
185
|
+
await expect(DataCryptor.decrypt(invalidData, iv, keys, 1)).rejects.toThrow(
|
186
|
+
'Decryption failed',
|
187
|
+
);
|
188
|
+
});
|
189
|
+
});
|
190
|
+
|
191
|
+
describe('round-trip encryption/decryption', () => {
|
192
|
+
it('encrypts and decrypts data correctly', async () => {
|
193
|
+
const keys = prepareParticipantTestKeys(participantIdentity, {});
|
194
|
+
await keys.setKey(await createKeyMaterialFromString('round-trip-key'), 2);
|
195
|
+
|
196
|
+
const originalData = new Uint8Array([
|
197
|
+
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
|
198
|
+
26, 27, 28, 29, 30, 31, 32,
|
199
|
+
]);
|
200
|
+
|
201
|
+
const encrypted = await DataCryptor.encrypt(originalData, keys);
|
202
|
+
const decrypted = await DataCryptor.decrypt(
|
203
|
+
encrypted.payload,
|
204
|
+
encrypted.iv,
|
205
|
+
keys,
|
206
|
+
encrypted.keyIndex,
|
207
|
+
);
|
208
|
+
|
209
|
+
expect(decrypted.payload).toEqual(originalData);
|
210
|
+
});
|
211
|
+
|
212
|
+
it('handles empty data', async () => {
|
213
|
+
const keys = prepareParticipantTestKeys(participantIdentity, {});
|
214
|
+
await keys.setKey(await createKeyMaterialFromString('empty-data-key'), 1);
|
215
|
+
|
216
|
+
const emptyData = new Uint8Array(0);
|
217
|
+
|
218
|
+
const encrypted = await DataCryptor.encrypt(emptyData, keys);
|
219
|
+
const decrypted = await DataCryptor.decrypt(
|
220
|
+
encrypted.payload,
|
221
|
+
encrypted.iv,
|
222
|
+
keys,
|
223
|
+
encrypted.keyIndex,
|
224
|
+
);
|
225
|
+
|
226
|
+
expect(decrypted.payload).toEqual(emptyData);
|
227
|
+
});
|
228
|
+
|
229
|
+
it('handles large data', async () => {
|
230
|
+
const keys = prepareParticipantTestKeys(participantIdentity, {});
|
231
|
+
await keys.setKey(await createKeyMaterialFromString('large-data-key'), 1);
|
232
|
+
|
233
|
+
const largeData = new Uint8Array(1024);
|
234
|
+
for (let i = 0; i < largeData.length; i++) {
|
235
|
+
largeData[i] = i % 256;
|
236
|
+
}
|
237
|
+
|
238
|
+
const encrypted = await DataCryptor.encrypt(largeData, keys);
|
239
|
+
const decrypted = await DataCryptor.decrypt(
|
240
|
+
encrypted.payload,
|
241
|
+
encrypted.iv,
|
242
|
+
keys,
|
243
|
+
encrypted.keyIndex,
|
244
|
+
);
|
245
|
+
|
246
|
+
expect(decrypted.payload).toEqual(largeData);
|
247
|
+
});
|
248
|
+
});
|
249
|
+
|
250
|
+
describe('IV generation', () => {
|
251
|
+
it('generates unique IVs with performance.now() timestamp', async () => {
|
252
|
+
const keys = prepareParticipantTestKeys(participantIdentity, {});
|
253
|
+
await keys.setKey(await createKeyMaterialFromString('iv-test-key'), 1);
|
254
|
+
|
255
|
+
const data = new Uint8Array([1, 2, 3, 4]);
|
256
|
+
|
257
|
+
vitest.useFakeTimers();
|
258
|
+
const time1 = 1000;
|
259
|
+
vitest.setSystemTime(time1);
|
260
|
+
const result1 = await DataCryptor.encrypt(data, keys);
|
261
|
+
|
262
|
+
vitest.setSystemTime(2000);
|
263
|
+
const result2 = await DataCryptor.encrypt(data, keys);
|
264
|
+
|
265
|
+
vitest.useRealTimers();
|
266
|
+
|
267
|
+
// IVs should be different due to different timestamps and sendCount
|
268
|
+
expect(result1.iv).not.toEqual(result2.iv);
|
269
|
+
});
|
270
|
+
});
|
271
|
+
});
|
@@ -0,0 +1,147 @@
|
|
1
|
+
import { workerLogger } from '../../logger';
|
2
|
+
import { ENCRYPTION_ALGORITHM } from '../constants';
|
3
|
+
import { CryptorError, CryptorErrorReason } from '../errors';
|
4
|
+
import type { DecodeRatchetOptions, KeySet, RatchetResult } from '../types';
|
5
|
+
import { deriveKeys } from '../utils';
|
6
|
+
import type { ParticipantKeyHandler } from './ParticipantKeyHandler';
|
7
|
+
|
8
|
+
export class DataCryptor {
|
9
|
+
private static sendCount = 0;
|
10
|
+
|
11
|
+
private static makeIV(timestamp: number) {
|
12
|
+
const iv = new ArrayBuffer(12);
|
13
|
+
const ivView = new DataView(iv);
|
14
|
+
const randomBytes = crypto.getRandomValues(new Uint32Array(1));
|
15
|
+
ivView.setUint32(0, randomBytes[0]);
|
16
|
+
ivView.setUint32(4, timestamp);
|
17
|
+
ivView.setUint32(8, timestamp - (DataCryptor.sendCount % 0xffff));
|
18
|
+
DataCryptor.sendCount++;
|
19
|
+
|
20
|
+
return iv;
|
21
|
+
}
|
22
|
+
|
23
|
+
static async encrypt(
|
24
|
+
data: Uint8Array,
|
25
|
+
keys: ParticipantKeyHandler,
|
26
|
+
): Promise<{
|
27
|
+
payload: Uint8Array;
|
28
|
+
iv: Uint8Array;
|
29
|
+
keyIndex: number;
|
30
|
+
}> {
|
31
|
+
const iv = DataCryptor.makeIV(performance.now());
|
32
|
+
const keySet = await keys.getKeySet();
|
33
|
+
if (!keySet) {
|
34
|
+
throw new Error('No key set found');
|
35
|
+
}
|
36
|
+
|
37
|
+
const cipherText = await crypto.subtle.encrypt(
|
38
|
+
{
|
39
|
+
name: ENCRYPTION_ALGORITHM,
|
40
|
+
iv,
|
41
|
+
},
|
42
|
+
keySet.encryptionKey,
|
43
|
+
new Uint8Array(data),
|
44
|
+
);
|
45
|
+
|
46
|
+
return {
|
47
|
+
payload: new Uint8Array(cipherText),
|
48
|
+
iv: new Uint8Array(iv),
|
49
|
+
keyIndex: keys.getCurrentKeyIndex(),
|
50
|
+
};
|
51
|
+
}
|
52
|
+
|
53
|
+
static async decrypt(
|
54
|
+
data: Uint8Array,
|
55
|
+
iv: Uint8Array,
|
56
|
+
keys: ParticipantKeyHandler,
|
57
|
+
keyIndex: number = 0,
|
58
|
+
initialMaterial?: KeySet,
|
59
|
+
ratchetOpts: DecodeRatchetOptions = { ratchetCount: 0 },
|
60
|
+
): Promise<{
|
61
|
+
payload: Uint8Array;
|
62
|
+
}> {
|
63
|
+
const keySet = await keys.getKeySet(keyIndex);
|
64
|
+
if (!keySet) {
|
65
|
+
throw new Error('No key set found');
|
66
|
+
}
|
67
|
+
|
68
|
+
try {
|
69
|
+
const plainText = await crypto.subtle.decrypt(
|
70
|
+
{
|
71
|
+
name: ENCRYPTION_ALGORITHM,
|
72
|
+
iv,
|
73
|
+
},
|
74
|
+
keySet.encryptionKey,
|
75
|
+
new Uint8Array(data),
|
76
|
+
);
|
77
|
+
return {
|
78
|
+
payload: new Uint8Array(plainText),
|
79
|
+
};
|
80
|
+
} catch (error: any) {
|
81
|
+
if (keys.keyProviderOptions.ratchetWindowSize > 0) {
|
82
|
+
if (ratchetOpts.ratchetCount < keys.keyProviderOptions.ratchetWindowSize) {
|
83
|
+
workerLogger.debug(
|
84
|
+
`DataCryptor: ratcheting key attempt ${ratchetOpts.ratchetCount} of ${
|
85
|
+
keys.keyProviderOptions.ratchetWindowSize
|
86
|
+
}, for data packet`,
|
87
|
+
);
|
88
|
+
|
89
|
+
let ratchetedKeySet: KeySet | undefined;
|
90
|
+
let ratchetResult: RatchetResult | undefined;
|
91
|
+
if ((initialMaterial ?? keySet) === keys.getKeySet(keyIndex)) {
|
92
|
+
// only ratchet if the currently set key is still the same as the one used to decrypt this frame
|
93
|
+
// if not, it might be that a different frame has already ratcheted and we try with that one first
|
94
|
+
ratchetResult = await keys.ratchetKey(keyIndex, false);
|
95
|
+
|
96
|
+
ratchetedKeySet = await deriveKeys(
|
97
|
+
ratchetResult.cryptoKey,
|
98
|
+
keys.keyProviderOptions.ratchetSalt,
|
99
|
+
);
|
100
|
+
}
|
101
|
+
|
102
|
+
const decryptedData = await DataCryptor.decrypt(
|
103
|
+
data,
|
104
|
+
iv,
|
105
|
+
keys,
|
106
|
+
keyIndex,
|
107
|
+
initialMaterial,
|
108
|
+
{
|
109
|
+
ratchetCount: ratchetOpts.ratchetCount + 1,
|
110
|
+
encryptionKey: ratchetedKeySet?.encryptionKey,
|
111
|
+
},
|
112
|
+
);
|
113
|
+
|
114
|
+
if (decryptedData && ratchetedKeySet) {
|
115
|
+
// before updating the keys, make sure that the keySet used for this frame is still the same as the currently set key
|
116
|
+
// if it's not, a new key might have been set already, which we don't want to override
|
117
|
+
if ((initialMaterial ?? keySet) === keys.getKeySet(keyIndex)) {
|
118
|
+
keys.setKeySet(ratchetedKeySet, keyIndex, ratchetResult);
|
119
|
+
// decryption was successful, set the new key index to reflect the ratcheted key set
|
120
|
+
keys.setCurrentKeyIndex(keyIndex);
|
121
|
+
}
|
122
|
+
}
|
123
|
+
return decryptedData;
|
124
|
+
} else {
|
125
|
+
/**
|
126
|
+
* Because we only set a new key once decryption has been successful,
|
127
|
+
* we can be sure that we don't need to reset the key to the initial material at this point
|
128
|
+
* as the key has not been updated on the keyHandler instance
|
129
|
+
*/
|
130
|
+
|
131
|
+
workerLogger.warn('DataCryptor: maximum ratchet attempts exceeded');
|
132
|
+
throw new CryptorError(
|
133
|
+
`DataCryptor: valid key missing for participant ${keys.participantIdentity}`,
|
134
|
+
CryptorErrorReason.InvalidKey,
|
135
|
+
keys.participantIdentity,
|
136
|
+
);
|
137
|
+
}
|
138
|
+
} else {
|
139
|
+
throw new CryptorError(
|
140
|
+
`DataCryptor: Decryption failed: ${error.message}`,
|
141
|
+
CryptorErrorReason.InvalidKey,
|
142
|
+
keys.participantIdentity,
|
143
|
+
);
|
144
|
+
}
|
145
|
+
}
|
146
|
+
}
|
147
|
+
}
|
@@ -23,11 +23,12 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
|
|
23
23
|
|
24
24
|
private decryptionFailureCounts: Array<number>;
|
25
25
|
|
26
|
-
private keyProviderOptions: KeyProviderOptions;
|
27
|
-
|
28
26
|
private ratchetPromiseMap: Map<number, Promise<RatchetResult>>;
|
29
27
|
|
30
|
-
|
28
|
+
readonly participantIdentity: string;
|
29
|
+
|
30
|
+
/** @internal */
|
31
|
+
readonly keyProviderOptions: KeyProviderOptions;
|
31
32
|
|
32
33
|
/**
|
33
34
|
* true if the current key has not been marked as invalid
|
@@ -5,7 +5,9 @@ import { KEY_PROVIDER_DEFAULTS } from '../constants';
|
|
5
5
|
import { CryptorErrorReason } from '../errors';
|
6
6
|
import { CryptorEvent, KeyHandlerEvent } from '../events';
|
7
7
|
import type {
|
8
|
+
DecryptDataResponseMessage,
|
8
9
|
E2EEWorkerMessage,
|
10
|
+
EncryptDataResponseMessage,
|
9
11
|
ErrorMessage,
|
10
12
|
InitAck,
|
11
13
|
KeyProviderOptions,
|
@@ -14,6 +16,7 @@ import type {
|
|
14
16
|
RatchetResult,
|
15
17
|
ScriptTransformOptions,
|
16
18
|
} from '../types';
|
19
|
+
import { DataCryptor } from './DataCryptor';
|
17
20
|
import { FrameCryptor, encryptionEnabledMap } from './FrameCryptor';
|
18
21
|
import { ParticipantKeyHandler } from './ParticipantKeyHandler';
|
19
22
|
|
@@ -81,6 +84,50 @@ onmessage = (ev) => {
|
|
81
84
|
data.codec,
|
82
85
|
);
|
83
86
|
break;
|
87
|
+
|
88
|
+
case 'encryptDataRequest':
|
89
|
+
const {
|
90
|
+
payload: encryptedPayload,
|
91
|
+
iv,
|
92
|
+
keyIndex,
|
93
|
+
} = await DataCryptor.encrypt(
|
94
|
+
data.payload,
|
95
|
+
getParticipantKeyHandler(data.participantIdentity),
|
96
|
+
);
|
97
|
+
console.log('encrypted payload', {
|
98
|
+
original: data.payload,
|
99
|
+
encrypted: encryptedPayload,
|
100
|
+
iv,
|
101
|
+
});
|
102
|
+
postMessage({
|
103
|
+
kind: 'encryptDataResponse',
|
104
|
+
data: {
|
105
|
+
payload: encryptedPayload,
|
106
|
+
iv,
|
107
|
+
keyIndex,
|
108
|
+
uuid: data.uuid,
|
109
|
+
},
|
110
|
+
} satisfies EncryptDataResponseMessage);
|
111
|
+
break;
|
112
|
+
|
113
|
+
case 'decryptDataRequest':
|
114
|
+
const { payload: decryptedPayload } = await DataCryptor.decrypt(
|
115
|
+
data.payload,
|
116
|
+
data.iv,
|
117
|
+
getParticipantKeyHandler(data.participantIdentity),
|
118
|
+
data.keyIndex,
|
119
|
+
);
|
120
|
+
console.log('decrypted payload', {
|
121
|
+
original: data.payload,
|
122
|
+
decrypted: decryptedPayload,
|
123
|
+
iv: data.iv,
|
124
|
+
});
|
125
|
+
postMessage({
|
126
|
+
kind: 'decryptDataResponse',
|
127
|
+
data: { payload: decryptedPayload, uuid: data.uuid },
|
128
|
+
} satisfies DecryptDataResponseMessage);
|
129
|
+
break;
|
130
|
+
|
84
131
|
case 'setKey':
|
85
132
|
if (useSharedKey) {
|
86
133
|
await setSharedKey(data.key, data.keyIndex);
|
@@ -2,25 +2,29 @@ import type { VideoCodec } from '../..';
|
|
2
2
|
|
3
3
|
// Payload definitions taken from https://github.com/livekit/livekit/blob/master/pkg/sfu/downtrack.go#L104
|
4
4
|
|
5
|
-
export const VP8KeyFrame8x8 = new Uint8Array([
|
5
|
+
export const VP8KeyFrame8x8: Uint8Array = new Uint8Array([
|
6
6
|
0x10, 0x02, 0x00, 0x9d, 0x01, 0x2a, 0x08, 0x00, 0x08, 0x00, 0x00, 0x47, 0x08, 0x85, 0x85, 0x88,
|
7
7
|
0x85, 0x84, 0x88, 0x02, 0x02, 0x00, 0x0c, 0x0d, 0x60, 0x00, 0xfe, 0xff, 0xab, 0x50, 0x80,
|
8
8
|
]);
|
9
9
|
|
10
|
-
export const H264KeyFrame2x2SPS = new Uint8Array([
|
10
|
+
export const H264KeyFrame2x2SPS: Uint8Array = new Uint8Array([
|
11
11
|
0x67, 0x42, 0xc0, 0x1f, 0x0f, 0xd9, 0x1f, 0x88, 0x88, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04, 0x00,
|
12
12
|
0x00, 0x03, 0x00, 0xc8, 0x3c, 0x60, 0xc9, 0x20,
|
13
13
|
]);
|
14
14
|
|
15
|
-
export const H264KeyFrame2x2PPS = new Uint8Array([0x68, 0x87, 0xcb, 0x83, 0xcb, 0x20]);
|
15
|
+
export const H264KeyFrame2x2PPS: Uint8Array = new Uint8Array([0x68, 0x87, 0xcb, 0x83, 0xcb, 0x20]);
|
16
16
|
|
17
|
-
export const H264KeyFrame2x2IDR = new Uint8Array([
|
17
|
+
export const H264KeyFrame2x2IDR: Uint8Array = new Uint8Array([
|
18
18
|
0x65, 0x88, 0x84, 0x0a, 0xf2, 0x62, 0x80, 0x00, 0xa7, 0xbe,
|
19
19
|
]);
|
20
20
|
|
21
|
-
export const H264KeyFrame2x2 = [
|
21
|
+
export const H264KeyFrame2x2: Uint8Array[] = [
|
22
|
+
H264KeyFrame2x2SPS,
|
23
|
+
H264KeyFrame2x2PPS,
|
24
|
+
H264KeyFrame2x2IDR,
|
25
|
+
];
|
22
26
|
|
23
|
-
export const OpusSilenceFrame = new Uint8Array([
|
27
|
+
export const OpusSilenceFrame: Uint8Array = new Uint8Array([
|
24
28
|
0xf8, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
25
29
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
26
30
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
package/src/index.ts
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
import { Mutex } from '@livekit/mutex';
|
2
|
-
import {
|
2
|
+
import {
|
3
|
+
DataPacket_Kind,
|
4
|
+
DisconnectReason,
|
5
|
+
Encryption_Type,
|
6
|
+
SubscriptionError,
|
7
|
+
TrackType,
|
8
|
+
} from '@livekit/protocol';
|
3
9
|
import { LogLevel, LoggerNames, getLogger, setLogExtension, setLogLevel } from './logger';
|
4
10
|
import DefaultReconnectPolicy from './room/DefaultReconnectPolicy';
|
5
11
|
import type { ReconnectContext, ReconnectPolicy } from './room/ReconnectPolicy';
|
@@ -33,12 +39,14 @@ import {
|
|
33
39
|
createAudioAnalyser,
|
34
40
|
getEmptyAudioStreamTrack,
|
35
41
|
getEmptyVideoStreamTrack,
|
42
|
+
isAudioCodec,
|
36
43
|
isAudioTrack,
|
37
44
|
isBrowserSupported,
|
38
45
|
isLocalParticipant,
|
39
46
|
isLocalTrack,
|
40
47
|
isRemoteParticipant,
|
41
48
|
isRemoteTrack,
|
49
|
+
isVideoCodec,
|
42
50
|
isVideoTrack,
|
43
51
|
supportsAV1,
|
44
52
|
supportsAdaptiveStream,
|
@@ -58,6 +66,8 @@ export * from './room/errors';
|
|
58
66
|
export * from './room/events';
|
59
67
|
export * from './room/track/Track';
|
60
68
|
export * from './room/track/create';
|
69
|
+
export * from './room/token-source/TokenSource';
|
70
|
+
export * from './room/token-source/types';
|
61
71
|
export { facingModeFromDeviceLabel, facingModeFromLocalTrack } from './room/track/facingMode';
|
62
72
|
export * from './room/track/options';
|
63
73
|
export * from './room/track/processor/types';
|
@@ -79,6 +89,7 @@ export {
|
|
79
89
|
ConnectionState,
|
80
90
|
CriticalTimers,
|
81
91
|
DataPacket_Kind,
|
92
|
+
Encryption_Type,
|
82
93
|
DefaultReconnectPolicy,
|
83
94
|
DisconnectReason,
|
84
95
|
LocalAudioTrack,
|
@@ -113,9 +124,11 @@ export {
|
|
113
124
|
supportsDynacast,
|
114
125
|
supportsVP9,
|
115
126
|
Mutex,
|
127
|
+
isAudioCodec,
|
116
128
|
isAudioTrack,
|
117
129
|
isLocalTrack,
|
118
130
|
isRemoteTrack,
|
131
|
+
isVideoCodec,
|
119
132
|
isVideoTrack,
|
120
133
|
isLocalParticipant,
|
121
134
|
isRemoteParticipant,
|
package/src/logger.ts
CHANGED
package/src/options.ts
CHANGED
@@ -87,10 +87,16 @@ export interface InternalRoomOptions {
|
|
87
87
|
|
88
88
|
webAudioMix: boolean | WebAudioSettings;
|
89
89
|
|
90
|
+
// /**
|
91
|
+
// * @deprecated Use `encryption` field instead.
|
92
|
+
// */
|
93
|
+
e2ee?: E2EEOptions;
|
94
|
+
|
90
95
|
/**
|
91
96
|
* @experimental
|
97
|
+
* Options for enabling end-to-end encryption.
|
92
98
|
*/
|
93
|
-
|
99
|
+
encryption?: E2EEOptions;
|
94
100
|
|
95
101
|
loggerName?: string;
|
96
102
|
}
|
@@ -98,7 +104,7 @@ export interface InternalRoomOptions {
|
|
98
104
|
/**
|
99
105
|
* Options for when creating a new room
|
100
106
|
*/
|
101
|
-
export interface RoomOptions extends Partial<InternalRoomOptions
|
107
|
+
export interface RoomOptions extends Partial<Omit<InternalRoomOptions, 'encryption'>> {}
|
102
108
|
|
103
109
|
/**
|
104
110
|
* @internal
|