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.
Files changed (173) hide show
  1. package/dist/livekit-client.e2ee.worker.js +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs +253 -118
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +1892 -153
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.umd.js +1 -1
  8. package/dist/livekit-client.umd.js.map +1 -1
  9. package/dist/src/connectionHelper/checks/publishVideo.d.ts.map +1 -1
  10. package/dist/src/e2ee/E2eeManager.d.ts +16 -2
  11. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  12. package/dist/src/e2ee/types.d.ts +35 -1
  13. package/dist/src/e2ee/types.d.ts.map +1 -1
  14. package/dist/src/e2ee/utils.d.ts +2 -0
  15. package/dist/src/e2ee/utils.d.ts.map +1 -1
  16. package/dist/src/e2ee/worker/DataCryptor.d.ts +15 -0
  17. package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -0
  18. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +3 -2
  19. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
  20. package/dist/src/e2ee/worker/sifPayload.d.ts +6 -6
  21. package/dist/src/e2ee/worker/sifPayload.d.ts.map +1 -1
  22. package/dist/src/index.d.ts +5 -3
  23. package/dist/src/index.d.ts.map +1 -1
  24. package/dist/src/logger.d.ts +1 -0
  25. package/dist/src/logger.d.ts.map +1 -1
  26. package/dist/src/options.d.ts +4 -2
  27. package/dist/src/options.d.ts.map +1 -1
  28. package/dist/src/room/PCTransport.d.ts.map +1 -1
  29. package/dist/src/room/RTCEngine.d.ts +5 -2
  30. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  31. package/dist/src/room/Room.d.ts +3 -2
  32. package/dist/src/room/Room.d.ts.map +1 -1
  33. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +2 -2
  34. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
  35. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
  36. package/dist/src/room/errors.d.ts +2 -1
  37. package/dist/src/room/errors.d.ts.map +1 -1
  38. package/dist/src/room/participant/LocalParticipant.d.ts +1 -3
  39. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  40. package/dist/src/room/participant/Participant.d.ts +2 -2
  41. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  42. package/dist/src/room/token-source/TokenSource.d.ts +70 -0
  43. package/dist/src/room/token-source/TokenSource.d.ts.map +1 -0
  44. package/dist/src/room/token-source/types.d.ts +68 -0
  45. package/dist/src/room/token-source/types.d.ts.map +1 -0
  46. package/dist/src/room/token-source/utils.d.ts +5 -0
  47. package/dist/src/room/token-source/utils.d.ts.map +1 -0
  48. package/dist/src/room/track/LocalTrack.d.ts +1 -1
  49. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  50. package/dist/src/room/track/options.d.ts +7 -3
  51. package/dist/src/room/track/options.d.ts.map +1 -1
  52. package/dist/src/room/track/utils.d.ts.map +1 -1
  53. package/dist/src/room/types.d.ts +1 -0
  54. package/dist/src/room/types.d.ts.map +1 -1
  55. package/dist/src/room/utils.d.ts +2 -1
  56. package/dist/src/room/utils.d.ts.map +1 -1
  57. package/dist/src/utils/camelToSnakeCase.d.ts +8 -0
  58. package/dist/src/utils/camelToSnakeCase.d.ts.map +1 -0
  59. package/dist/ts4.2/{src/e2ee → e2ee}/E2eeManager.d.ts +16 -2
  60. package/dist/ts4.2/{src/e2ee → e2ee}/types.d.ts +35 -1
  61. package/dist/ts4.2/{src/e2ee → e2ee}/utils.d.ts +3 -0
  62. package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +15 -0
  63. package/dist/ts4.2/{src/e2ee → e2ee}/worker/ParticipantKeyHandler.d.ts +3 -2
  64. package/dist/ts4.2/{src/e2ee → e2ee}/worker/sifPayload.d.ts +6 -6
  65. package/dist/ts4.2/{src/index.d.ts → index.d.ts} +5 -3
  66. package/dist/ts4.2/{src/logger.d.ts → logger.d.ts} +1 -0
  67. package/dist/ts4.2/{src/options.d.ts → options.d.ts} +4 -2
  68. package/dist/ts4.2/{src/room → room}/RTCEngine.d.ts +5 -2
  69. package/dist/ts4.2/{src/room → room}/Room.d.ts +3 -2
  70. package/dist/ts4.2/{src/room → room}/data-stream/incoming/IncomingDataStreamManager.d.ts +2 -1
  71. package/dist/ts4.2/{src/room → room}/errors.d.ts +2 -1
  72. package/dist/ts4.2/{src/room → room}/participant/LocalParticipant.d.ts +1 -3
  73. package/dist/ts4.2/{src/room → room}/participant/Participant.d.ts +2 -2
  74. package/dist/ts4.2/room/token-source/TokenSource.d.ts +71 -0
  75. package/dist/ts4.2/room/token-source/types.d.ts +68 -0
  76. package/dist/ts4.2/room/token-source/utils.d.ts +5 -0
  77. package/dist/ts4.2/{src/room → room}/track/LocalTrack.d.ts +1 -1
  78. package/dist/ts4.2/{src/room → room}/track/options.d.ts +10 -3
  79. package/dist/ts4.2/{src/room → room}/types.d.ts +1 -0
  80. package/dist/ts4.2/{src/room → room}/utils.d.ts +2 -1
  81. package/dist/ts4.2/utils/camelToSnakeCase.d.ts +8 -0
  82. package/package.json +14 -12
  83. package/src/connectionHelper/checks/publishVideo.ts +5 -0
  84. package/src/e2ee/E2eeManager.ts +94 -2
  85. package/src/e2ee/types.ts +44 -1
  86. package/src/e2ee/utils.ts +16 -0
  87. package/src/e2ee/worker/DataCryptor.test.ts +271 -0
  88. package/src/e2ee/worker/DataCryptor.ts +147 -0
  89. package/src/e2ee/worker/ParticipantKeyHandler.ts +4 -3
  90. package/src/e2ee/worker/e2ee.worker.ts +47 -0
  91. package/src/e2ee/worker/sifPayload.ts +10 -6
  92. package/src/index.ts +14 -1
  93. package/src/logger.ts +1 -0
  94. package/src/options.ts +8 -2
  95. package/src/room/PCTransport.ts +14 -5
  96. package/src/room/RTCEngine.ts +55 -6
  97. package/src/room/Room.ts +39 -17
  98. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +64 -17
  99. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +7 -0
  100. package/src/room/errors.ts +3 -0
  101. package/src/room/participant/LocalParticipant.ts +17 -29
  102. package/src/room/participant/Participant.ts +6 -1
  103. package/src/room/token-source/TokenSource.ts +285 -0
  104. package/src/room/token-source/types.ts +84 -0
  105. package/src/room/token-source/utils.ts +35 -0
  106. package/src/room/track/LocalAudioTrack.ts +1 -1
  107. package/src/room/track/LocalTrack.ts +1 -1
  108. package/src/room/track/options.ts +12 -4
  109. package/src/room/track/utils.ts +10 -2
  110. package/src/room/types.ts +1 -0
  111. package/src/room/utils.ts +8 -4
  112. package/src/utils/camelToSnakeCase.ts +16 -0
  113. /package/dist/ts4.2/{src/api → api}/SignalClient.d.ts +0 -0
  114. /package/dist/ts4.2/{src/api → api}/utils.d.ts +0 -0
  115. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/ConnectionCheck.d.ts +0 -0
  116. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/Checker.d.ts +0 -0
  117. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/cloudRegion.d.ts +0 -0
  118. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/connectionProtocol.d.ts +0 -0
  119. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/publishAudio.d.ts +0 -0
  120. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/publishVideo.d.ts +0 -0
  121. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/reconnect.d.ts +0 -0
  122. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/turn.d.ts +0 -0
  123. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/webrtc.d.ts +0 -0
  124. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/websocket.d.ts +0 -0
  125. /package/dist/ts4.2/{src/e2ee → e2ee}/KeyProvider.d.ts +0 -0
  126. /package/dist/ts4.2/{src/e2ee → e2ee}/constants.d.ts +0 -0
  127. /package/dist/ts4.2/{src/e2ee → e2ee}/errors.d.ts +0 -0
  128. /package/dist/ts4.2/{src/e2ee → e2ee}/events.d.ts +0 -0
  129. /package/dist/ts4.2/{src/e2ee → e2ee}/index.d.ts +0 -0
  130. /package/dist/ts4.2/{src/e2ee → e2ee}/worker/FrameCryptor.d.ts +0 -0
  131. /package/dist/ts4.2/{src/e2ee → e2ee}/worker/e2ee.worker.d.ts +0 -0
  132. /package/dist/ts4.2/{src/e2ee → e2ee}/worker/naluUtils.d.ts +0 -0
  133. /package/dist/ts4.2/{src/room → room}/DefaultReconnectPolicy.d.ts +0 -0
  134. /package/dist/ts4.2/{src/room → room}/DeviceManager.d.ts +0 -0
  135. /package/dist/ts4.2/{src/room → room}/PCTransport.d.ts +0 -0
  136. /package/dist/ts4.2/{src/room → room}/PCTransportManager.d.ts +0 -0
  137. /package/dist/ts4.2/{src/room → room}/ReconnectPolicy.d.ts +0 -0
  138. /package/dist/ts4.2/{src/room → room}/RegionUrlProvider.d.ts +0 -0
  139. /package/dist/ts4.2/{src/room → room}/attribute-typings.d.ts +0 -0
  140. /package/dist/ts4.2/{src/room → room}/data-stream/incoming/StreamReader.d.ts +0 -0
  141. /package/dist/ts4.2/{src/room → room}/data-stream/outgoing/OutgoingDataStreamManager.d.ts +0 -0
  142. /package/dist/ts4.2/{src/room → room}/data-stream/outgoing/StreamWriter.d.ts +0 -0
  143. /package/dist/ts4.2/{src/room → room}/defaults.d.ts +0 -0
  144. /package/dist/ts4.2/{src/room → room}/events.d.ts +0 -0
  145. /package/dist/ts4.2/{src/room → room}/participant/ParticipantTrackPermission.d.ts +0 -0
  146. /package/dist/ts4.2/{src/room → room}/participant/RemoteParticipant.d.ts +0 -0
  147. /package/dist/ts4.2/{src/room → room}/participant/publishUtils.d.ts +0 -0
  148. /package/dist/ts4.2/{src/room → room}/rpc.d.ts +0 -0
  149. /package/dist/ts4.2/{src/room → room}/stats.d.ts +0 -0
  150. /package/dist/ts4.2/{src/room → room}/timers.d.ts +0 -0
  151. /package/dist/ts4.2/{src/room → room}/track/LocalAudioTrack.d.ts +0 -0
  152. /package/dist/ts4.2/{src/room → room}/track/LocalTrackPublication.d.ts +0 -0
  153. /package/dist/ts4.2/{src/room → room}/track/LocalVideoTrack.d.ts +0 -0
  154. /package/dist/ts4.2/{src/room → room}/track/RemoteAudioTrack.d.ts +0 -0
  155. /package/dist/ts4.2/{src/room → room}/track/RemoteTrack.d.ts +0 -0
  156. /package/dist/ts4.2/{src/room → room}/track/RemoteTrackPublication.d.ts +0 -0
  157. /package/dist/ts4.2/{src/room → room}/track/RemoteVideoTrack.d.ts +0 -0
  158. /package/dist/ts4.2/{src/room → room}/track/Track.d.ts +0 -0
  159. /package/dist/ts4.2/{src/room → room}/track/TrackPublication.d.ts +0 -0
  160. /package/dist/ts4.2/{src/room → room}/track/create.d.ts +0 -0
  161. /package/dist/ts4.2/{src/room → room}/track/facingMode.d.ts +0 -0
  162. /package/dist/ts4.2/{src/room → room}/track/processor/types.d.ts +0 -0
  163. /package/dist/ts4.2/{src/room → room}/track/record.d.ts +0 -0
  164. /package/dist/ts4.2/{src/room → room}/track/types.d.ts +0 -0
  165. /package/dist/ts4.2/{src/room → room}/track/utils.d.ts +0 -0
  166. /package/dist/ts4.2/{src/test → test}/MockMediaStreamTrack.d.ts +0 -0
  167. /package/dist/ts4.2/{src/test → test}/mocks.d.ts +0 -0
  168. /package/dist/ts4.2/{src/utils → utils}/AsyncQueue.d.ts +0 -0
  169. /package/dist/ts4.2/{src/utils → utils}/browserParser.d.ts +0 -0
  170. /package/dist/ts4.2/{src/utils → utils}/cloneDeep.d.ts +0 -0
  171. /package/dist/ts4.2/{src/utils → utils}/dataPacketBuffer.d.ts +0 -0
  172. /package/dist/ts4.2/{src/utils → utils}/ttlmap.d.ts +0 -0
  173. /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
- private participantIdentity: string;
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 = [H264KeyFrame2x2SPS, H264KeyFrame2x2PPS, H264KeyFrame2x2IDR];
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 { DataPacket_Kind, DisconnectReason, SubscriptionError, TrackType } from '@livekit/protocol';
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
@@ -12,6 +12,7 @@ export enum LogLevel {
12
12
  export enum LoggerNames {
13
13
  Default = 'livekit',
14
14
  Room = 'livekit-room',
15
+ TokenSource = 'livekit-token-source',
15
16
  Participant = 'livekit-participant',
16
17
  Track = 'livekit-track',
17
18
  Publication = 'livekit-track-publication',
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
- e2ee?: E2EEOptions;
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