livekit-client 1.11.4 → 1.12.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (134) hide show
  1. package/README.md +13 -1
  2. package/dist/livekit-client.e2ee.worker.js +2 -0
  3. package/dist/livekit-client.e2ee.worker.js.map +1 -0
  4. package/dist/livekit-client.e2ee.worker.mjs +1545 -0
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -0
  6. package/dist/livekit-client.esm.mjs +4786 -4065
  7. package/dist/livekit-client.esm.mjs.map +1 -1
  8. package/dist/livekit-client.umd.js +1 -1
  9. package/dist/livekit-client.umd.js.map +1 -1
  10. package/dist/src/api/SignalClient.d.ts +4 -1
  11. package/dist/src/api/SignalClient.d.ts.map +1 -1
  12. package/dist/src/connectionHelper/checks/turn.d.ts.map +1 -1
  13. package/dist/src/connectionHelper/checks/websocket.d.ts.map +1 -1
  14. package/dist/src/e2ee/E2eeManager.d.ts +45 -0
  15. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -0
  16. package/dist/src/e2ee/KeyProvider.d.ts +42 -0
  17. package/dist/src/e2ee/KeyProvider.d.ts.map +1 -0
  18. package/dist/src/e2ee/constants.d.ts +14 -0
  19. package/dist/src/e2ee/constants.d.ts.map +1 -0
  20. package/dist/src/e2ee/errors.d.ts +11 -0
  21. package/dist/src/e2ee/errors.d.ts.map +1 -0
  22. package/dist/src/e2ee/index.d.ts +4 -0
  23. package/dist/src/e2ee/index.d.ts.map +1 -0
  24. package/dist/src/e2ee/types.d.ts +129 -0
  25. package/dist/src/e2ee/types.d.ts.map +1 -0
  26. package/dist/src/e2ee/utils.d.ts +24 -0
  27. package/dist/src/e2ee/utils.d.ts.map +1 -0
  28. package/dist/src/e2ee/worker/FrameCryptor.d.ts +174 -0
  29. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -0
  30. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +54 -0
  31. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -0
  32. package/dist/src/e2ee/worker/e2ee.worker.d.ts +2 -0
  33. package/dist/src/e2ee/worker/e2ee.worker.d.ts.map +1 -0
  34. package/dist/src/index.d.ts +1 -0
  35. package/dist/src/index.d.ts.map +1 -1
  36. package/dist/src/logger.d.ts +4 -1
  37. package/dist/src/logger.d.ts.map +1 -1
  38. package/dist/src/options.d.ts +5 -0
  39. package/dist/src/options.d.ts.map +1 -1
  40. package/dist/src/proto/livekit_models.d.ts +2 -2
  41. package/dist/src/proto/livekit_models.d.ts.map +1 -1
  42. package/dist/src/room/PCTransport.d.ts +3 -1
  43. package/dist/src/room/PCTransport.d.ts.map +1 -1
  44. package/dist/src/room/RTCEngine.d.ts +17 -3
  45. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  46. package/dist/src/room/Room.d.ts +10 -0
  47. package/dist/src/room/Room.d.ts.map +1 -1
  48. package/dist/src/room/events.d.ts +14 -2
  49. package/dist/src/room/events.d.ts.map +1 -1
  50. package/dist/src/room/participant/LocalParticipant.d.ts +7 -2
  51. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  52. package/dist/src/room/participant/Participant.d.ts +1 -0
  53. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  54. package/dist/src/room/participant/RemoteParticipant.d.ts +6 -4
  55. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  56. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  57. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  58. package/dist/src/room/track/TrackPublication.d.ts +3 -0
  59. package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
  60. package/dist/src/room/track/create.d.ts.map +1 -1
  61. package/dist/src/room/track/options.d.ts +2 -2
  62. package/dist/src/room/track/options.d.ts.map +1 -1
  63. package/dist/src/room/track/utils.d.ts +9 -0
  64. package/dist/src/room/track/utils.d.ts.map +1 -1
  65. package/dist/src/room/utils.d.ts +2 -0
  66. package/dist/src/room/utils.d.ts.map +1 -1
  67. package/dist/src/test/MockMediaStreamTrack.d.ts.map +1 -1
  68. package/dist/src/utils/browserParser.d.ts +2 -0
  69. package/dist/src/utils/browserParser.d.ts.map +1 -1
  70. package/dist/ts4.2/src/api/SignalClient.d.ts +4 -1
  71. package/dist/ts4.2/src/e2ee/E2eeManager.d.ts +45 -0
  72. package/dist/ts4.2/src/e2ee/KeyProvider.d.ts +42 -0
  73. package/dist/ts4.2/src/e2ee/constants.d.ts +14 -0
  74. package/dist/ts4.2/src/e2ee/errors.d.ts +11 -0
  75. package/dist/ts4.2/src/e2ee/index.d.ts +4 -0
  76. package/dist/ts4.2/src/e2ee/types.d.ts +129 -0
  77. package/dist/ts4.2/src/e2ee/utils.d.ts +24 -0
  78. package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +174 -0
  79. package/dist/ts4.2/src/e2ee/worker/ParticipantKeyHandler.d.ts +54 -0
  80. package/dist/ts4.2/src/e2ee/worker/e2ee.worker.d.ts +2 -0
  81. package/dist/ts4.2/src/index.d.ts +1 -0
  82. package/dist/ts4.2/src/logger.d.ts +4 -1
  83. package/dist/ts4.2/src/options.d.ts +5 -0
  84. package/dist/ts4.2/src/proto/livekit_models.d.ts +2 -2
  85. package/dist/ts4.2/src/room/PCTransport.d.ts +3 -1
  86. package/dist/ts4.2/src/room/RTCEngine.d.ts +17 -3
  87. package/dist/ts4.2/src/room/Room.d.ts +10 -0
  88. package/dist/ts4.2/src/room/events.d.ts +14 -2
  89. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +7 -2
  90. package/dist/ts4.2/src/room/participant/Participant.d.ts +1 -0
  91. package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +6 -4
  92. package/dist/ts4.2/src/room/track/TrackPublication.d.ts +3 -0
  93. package/dist/ts4.2/src/room/track/options.d.ts +6 -6
  94. package/dist/ts4.2/src/room/track/utils.d.ts +9 -0
  95. package/dist/ts4.2/src/room/utils.d.ts +2 -0
  96. package/dist/ts4.2/src/utils/browserParser.d.ts +2 -0
  97. package/package.json +17 -7
  98. package/src/api/SignalClient.ts +28 -9
  99. package/src/connectionHelper/checks/turn.ts +1 -0
  100. package/src/connectionHelper/checks/websocket.ts +1 -0
  101. package/src/e2ee/E2eeManager.ts +374 -0
  102. package/src/e2ee/KeyProvider.ts +77 -0
  103. package/src/e2ee/constants.ts +40 -0
  104. package/src/e2ee/errors.ts +16 -0
  105. package/src/e2ee/index.ts +3 -0
  106. package/src/e2ee/types.ts +160 -0
  107. package/src/e2ee/utils.ts +127 -0
  108. package/src/e2ee/worker/FrameCryptor.test.ts +21 -0
  109. package/src/e2ee/worker/FrameCryptor.ts +612 -0
  110. package/src/e2ee/worker/ParticipantKeyHandler.ts +144 -0
  111. package/src/e2ee/worker/e2ee.worker.ts +223 -0
  112. package/src/e2ee/worker/tsconfig.json +6 -0
  113. package/src/index.ts +1 -0
  114. package/src/logger.ts +10 -2
  115. package/src/options.ts +6 -0
  116. package/src/proto/livekit_models.ts +12 -12
  117. package/src/room/PCTransport.ts +39 -9
  118. package/src/room/RTCEngine.ts +127 -34
  119. package/src/room/Room.ts +94 -29
  120. package/src/room/defaults.ts +1 -1
  121. package/src/room/events.ts +14 -0
  122. package/src/room/participant/LocalParticipant.ts +52 -8
  123. package/src/room/participant/Participant.ts +4 -0
  124. package/src/room/participant/RemoteParticipant.ts +19 -15
  125. package/src/room/track/LocalTrack.ts +5 -4
  126. package/src/room/track/RemoteVideoTrack.ts +2 -2
  127. package/src/room/track/TrackPublication.ts +9 -1
  128. package/src/room/track/create.ts +9 -0
  129. package/src/room/track/options.ts +3 -2
  130. package/src/room/track/utils.ts +27 -0
  131. package/src/room/utils.ts +5 -0
  132. package/src/room/worker.d.ts +4 -0
  133. package/src/test/MockMediaStreamTrack.ts +1 -0
  134. package/src/utils/browserParser.ts +5 -0
@@ -0,0 +1,374 @@
1
+ import EventEmitter from 'eventemitter3';
2
+ import log from '../logger';
3
+ import { Encryption_Type, TrackInfo } from '../proto/livekit_models';
4
+ import type RTCEngine from '../room/RTCEngine';
5
+ import type Room from '../room/Room';
6
+ import { ConnectionState } from '../room/Room';
7
+ import { DeviceUnsupportedError } from '../room/errors';
8
+ import { EngineEvent, ParticipantEvent, RoomEvent } from '../room/events';
9
+ import LocalTrack from '../room/track/LocalTrack';
10
+ import type RemoteTrack from '../room/track/RemoteTrack';
11
+ import type { Track } from '../room/track/Track';
12
+ import type { VideoCodec } from '../room/track/options';
13
+ import type { BaseKeyProvider } from './KeyProvider';
14
+ import { E2EE_FLAG } from './constants';
15
+ import type {
16
+ E2EEManagerCallbacks,
17
+ E2EEOptions,
18
+ E2EEWorkerMessage,
19
+ EnableMessage,
20
+ EncodeMessage,
21
+ InitMessage,
22
+ KeyInfo,
23
+ RTPVideoMapMessage,
24
+ RatchetRequestMessage,
25
+ RemoveTransformMessage,
26
+ SetKeyMessage,
27
+ UpdateCodecMessage,
28
+ } from './types';
29
+ import { EncryptionEvent } from './types';
30
+ import { isE2EESupported, isScriptTransformSupported, mimeTypeToVideoCodecString } from './utils';
31
+
32
+ /**
33
+ * @experimental
34
+ */
35
+ export class E2EEManager extends EventEmitter<E2EEManagerCallbacks> {
36
+ protected worker: Worker;
37
+
38
+ protected room?: Room;
39
+
40
+ private encryptionEnabled: boolean;
41
+
42
+ private keyProvider: BaseKeyProvider;
43
+
44
+ get isEnabled() {
45
+ return this.encryptionEnabled;
46
+ }
47
+
48
+ constructor(options: E2EEOptions) {
49
+ super();
50
+ this.keyProvider = options.keyProvider;
51
+ this.worker = options.worker;
52
+ this.encryptionEnabled = false;
53
+ }
54
+
55
+ /**
56
+ * @internal
57
+ */
58
+ setup(room: Room) {
59
+ if (!isE2EESupported()) {
60
+ throw new DeviceUnsupportedError(
61
+ 'tried to setup end-to-end encryption on an unsupported browser',
62
+ );
63
+ }
64
+ log.info('setting up e2ee');
65
+ if (room !== this.room) {
66
+ this.room = room;
67
+ this.setupEventListeners(room, this.keyProvider);
68
+ // this.worker = new Worker('');
69
+ const msg: InitMessage = {
70
+ kind: 'init',
71
+ data: {
72
+ keyProviderOptions: this.keyProvider.getOptions(),
73
+ },
74
+ };
75
+ if (this.worker) {
76
+ log.info(`initializing worker`, { worker: this.worker });
77
+ this.worker.onmessage = this.onWorkerMessage;
78
+ this.worker.onerror = this.onWorkerError;
79
+ this.worker.postMessage(msg);
80
+ }
81
+ }
82
+ }
83
+
84
+ /**
85
+ * @internal
86
+ */
87
+ async setParticipantCryptorEnabled(enabled: boolean, participantId?: string) {
88
+ log.info(`set e2ee to ${enabled}`);
89
+
90
+ if (this.worker) {
91
+ const enableMsg: EnableMessage = {
92
+ kind: 'enable',
93
+ data: { enabled, participantId },
94
+ };
95
+ this.worker.postMessage(enableMsg);
96
+ } else {
97
+ throw new ReferenceError('failed to enable e2ee, worker is not ready');
98
+ }
99
+ }
100
+
101
+ private onWorkerMessage = (ev: MessageEvent<E2EEWorkerMessage>) => {
102
+ const { kind, data } = ev.data;
103
+ switch (kind) {
104
+ case 'error':
105
+ console.error('error in worker', { data });
106
+ this.emit(EncryptionEvent.Error, data.error);
107
+ break;
108
+ case 'enable':
109
+ if (this.encryptionEnabled !== data.enabled && !data.participantId) {
110
+ this.emit(
111
+ EncryptionEvent.ParticipantEncryptionStatusChanged,
112
+ data.enabled,
113
+ this.room?.localParticipant,
114
+ );
115
+ this.encryptionEnabled = data.enabled;
116
+ } else if (data.participantId) {
117
+ const participant = this.room?.getParticipantByIdentity(data.participantId);
118
+ this.emit(EncryptionEvent.ParticipantEncryptionStatusChanged, data.enabled, participant);
119
+ }
120
+ if (this.encryptionEnabled) {
121
+ this.keyProvider.getKeys().forEach((keyInfo) => {
122
+ this.postKey(keyInfo);
123
+ });
124
+ }
125
+ break;
126
+ case 'ratchetKey':
127
+ this.keyProvider.emit('keyRatcheted', data.material, data.keyIndex);
128
+ break;
129
+ default:
130
+ break;
131
+ }
132
+ };
133
+
134
+ private onWorkerError = (ev: ErrorEvent) => {
135
+ log.error('e2ee worker encountered an error:', { error: ev.error });
136
+ this.emit(EncryptionEvent.Error, ev.error);
137
+ };
138
+
139
+ public setupEngine(engine: RTCEngine) {
140
+ engine.on(EngineEvent.RTPVideoMapUpdate, (rtpMap) => {
141
+ this.postRTPMap(rtpMap);
142
+ });
143
+ }
144
+
145
+ private setupEventListeners(room: Room, keyProvider: BaseKeyProvider) {
146
+ room.on(RoomEvent.TrackPublished, (pub, participant) =>
147
+ this.setParticipantCryptorEnabled(
148
+ pub.trackInfo!.encryption !== Encryption_Type.NONE,
149
+ participant.identity,
150
+ ),
151
+ );
152
+ room.on(RoomEvent.ConnectionStateChanged, (state) => {
153
+ if (state === ConnectionState.Connected) {
154
+ room.participants.forEach((participant) => {
155
+ participant.tracks.forEach((pub) => {
156
+ this.setParticipantCryptorEnabled(
157
+ pub.trackInfo!.encryption !== Encryption_Type.NONE,
158
+ participant.identity,
159
+ );
160
+ });
161
+ });
162
+ }
163
+ });
164
+
165
+ room.on(RoomEvent.TrackUnsubscribed, (track, _, participant) => {
166
+ const msg: RemoveTransformMessage = {
167
+ kind: 'removeTransform',
168
+ data: {
169
+ participantId: participant.identity,
170
+ trackId: track.mediaStreamID,
171
+ },
172
+ };
173
+ this.worker?.postMessage(msg);
174
+ });
175
+ room.on(RoomEvent.TrackSubscribed, (track, pub, participant) => {
176
+ this.setupE2EEReceiver(track, participant.identity, pub.trackInfo);
177
+ });
178
+ room.localParticipant.on(ParticipantEvent.LocalTrackPublished, async (publication) => {
179
+ this.setupE2EESender(
180
+ publication.track!,
181
+ publication.track!.sender!,
182
+ room.localParticipant.identity,
183
+ );
184
+ });
185
+
186
+ keyProvider
187
+ .on('setKey', (keyInfo) => this.postKey(keyInfo))
188
+ .on('ratchetRequest', (participantId, keyIndex) =>
189
+ this.postRatchetRequest(participantId, keyIndex),
190
+ );
191
+ }
192
+
193
+ private postRatchetRequest(participantId?: string, keyIndex?: number) {
194
+ if (!this.worker) {
195
+ throw Error('could not ratchet key, worker is missing');
196
+ }
197
+ const msg: RatchetRequestMessage = {
198
+ kind: 'ratchetRequest',
199
+ data: {
200
+ participantId,
201
+ keyIndex,
202
+ },
203
+ };
204
+ this.worker.postMessage(msg);
205
+ }
206
+
207
+ private postKey({ key, participantId, keyIndex }: KeyInfo) {
208
+ if (!this.worker) {
209
+ throw Error('could not set key, worker is missing');
210
+ }
211
+ const msg: SetKeyMessage = {
212
+ kind: 'setKey',
213
+ data: {
214
+ participantId,
215
+ key,
216
+ keyIndex,
217
+ },
218
+ };
219
+ this.worker.postMessage(msg);
220
+ }
221
+
222
+ private postRTPMap(map: Map<number, VideoCodec>) {
223
+ if (!this.worker) {
224
+ throw Error('could not post rtp map, worker is missing');
225
+ }
226
+ const msg: RTPVideoMapMessage = {
227
+ kind: 'setRTPMap',
228
+ data: {
229
+ map,
230
+ },
231
+ };
232
+ this.worker.postMessage(msg);
233
+ }
234
+
235
+ private setupE2EEReceiver(track: RemoteTrack, remoteId: string, trackInfo?: TrackInfo) {
236
+ if (!track.receiver) {
237
+ return;
238
+ }
239
+ if (!trackInfo?.mimeType || trackInfo.mimeType === '') {
240
+ throw new TypeError('MimeType missing from trackInfo, cannot set up E2EE cryptor');
241
+ }
242
+ this.handleReceiver(
243
+ track.receiver,
244
+ track.mediaStreamID,
245
+ remoteId,
246
+ track.kind === 'video' ? mimeTypeToVideoCodecString(trackInfo.mimeType) : undefined,
247
+ );
248
+ }
249
+
250
+ private setupE2EESender(track: Track, sender: RTCRtpSender, localId: string) {
251
+ if (!(track instanceof LocalTrack) || !sender) {
252
+ if (!sender) log.warn('early return because sender is not ready');
253
+ return;
254
+ }
255
+ this.handleSender(sender, track.mediaStreamID, localId, undefined);
256
+ }
257
+
258
+ /**
259
+ * Handles the given {@code RTCRtpReceiver} by creating a {@code TransformStream} which will inject
260
+ * a frame decoder.
261
+ *
262
+ */
263
+ private async handleReceiver(
264
+ receiver: RTCRtpReceiver,
265
+ trackId: string,
266
+ participantId: string,
267
+ codec?: VideoCodec,
268
+ ) {
269
+ if (!this.worker) {
270
+ return;
271
+ }
272
+
273
+ if (isScriptTransformSupported()) {
274
+ const options = {
275
+ kind: 'decode',
276
+ participantId,
277
+ trackId,
278
+ codec,
279
+ };
280
+ // @ts-ignore
281
+ receiver.transform = new RTCRtpScriptTransform(this.worker, options);
282
+ } else {
283
+ if (E2EE_FLAG in receiver && codec) {
284
+ // only update codec
285
+ const msg: UpdateCodecMessage = {
286
+ kind: 'updateCodec',
287
+ data: {
288
+ trackId,
289
+ codec,
290
+ participantId,
291
+ },
292
+ };
293
+ this.worker.postMessage(msg);
294
+ return;
295
+ }
296
+ // @ts-ignore
297
+ let writable: WritableStream = receiver.writableStream;
298
+ // @ts-ignore
299
+ let readable: ReadableStream = receiver.readableStream;
300
+ if (!writable || !readable) {
301
+ // @ts-ignore
302
+ const receiverStreams = receiver.createEncodedStreams();
303
+ // @ts-ignore
304
+ receiver.writableStream = receiverStreams.writable;
305
+ writable = receiverStreams.writable;
306
+ // @ts-ignore
307
+ receiver.readableStream = receiverStreams.readable;
308
+ readable = receiverStreams.readable;
309
+ }
310
+
311
+ const msg: EncodeMessage = {
312
+ kind: 'decode',
313
+ data: {
314
+ readableStream: readable,
315
+ writableStream: writable,
316
+ trackId: trackId,
317
+ codec,
318
+ participantId,
319
+ },
320
+ };
321
+ this.worker.postMessage(msg, [readable, writable]);
322
+ }
323
+
324
+ // @ts-ignore
325
+ receiver[E2EE_FLAG] = true;
326
+ }
327
+
328
+ /**
329
+ * Handles the given {@code RTCRtpSender} by creating a {@code TransformStream} which will inject
330
+ * a frame encoder.
331
+ *
332
+ */
333
+ private handleSender(
334
+ sender: RTCRtpSender,
335
+ trackId: string,
336
+ participantId: string,
337
+ codec?: VideoCodec,
338
+ ) {
339
+ if (E2EE_FLAG in sender || !this.worker) {
340
+ return;
341
+ }
342
+
343
+ if (isScriptTransformSupported()) {
344
+ log.warn('initialize script transform');
345
+
346
+ const options = {
347
+ kind: 'encode',
348
+ participantId,
349
+ trackId,
350
+ codec,
351
+ };
352
+ // @ts-ignore
353
+ sender.transform = new RTCRtpScriptTransform(this.worker, options);
354
+ } else {
355
+ log.warn('initialize encoded streams');
356
+ // @ts-ignore
357
+ const senderStreams = sender.createEncodedStreams();
358
+ const msg: EncodeMessage = {
359
+ kind: 'encode',
360
+ data: {
361
+ readableStream: senderStreams.readable,
362
+ writableStream: senderStreams.writable,
363
+ codec,
364
+ trackId,
365
+ participantId,
366
+ },
367
+ };
368
+ this.worker.postMessage(msg, [senderStreams.readable, senderStreams.writable]);
369
+ }
370
+
371
+ // @ts-ignore
372
+ sender[E2EE_FLAG] = true;
373
+ }
374
+ }
@@ -0,0 +1,77 @@
1
+ import EventEmitter from 'eventemitter3';
2
+ import { KEY_PROVIDER_DEFAULTS } from './constants';
3
+ import type { KeyInfo, KeyProviderCallbacks, KeyProviderOptions } from './types';
4
+ import { createKeyMaterialFromString } from './utils';
5
+
6
+ /**
7
+ * @experimental
8
+ */
9
+ export class BaseKeyProvider extends EventEmitter<KeyProviderCallbacks> {
10
+ private keyInfoMap: Map<string, KeyInfo>;
11
+
12
+ private options: KeyProviderOptions;
13
+
14
+ constructor(options: Partial<KeyProviderOptions> = {}) {
15
+ super();
16
+ this.keyInfoMap = new Map();
17
+ this.options = { ...KEY_PROVIDER_DEFAULTS, ...options };
18
+ this.on('keyRatcheted', this.onKeyRatcheted);
19
+ }
20
+
21
+ /**
22
+ * callback to invoke once a key has been set for a participant
23
+ * @param key
24
+ * @param participantId
25
+ * @param keyIndex
26
+ */
27
+ protected onSetEncryptionKey(key: CryptoKey, participantId?: string, keyIndex?: number) {
28
+ const keyInfo: KeyInfo = { key, participantId, keyIndex };
29
+ this.keyInfoMap.set(`${participantId ?? 'shared'}-${keyIndex ?? 0}`, keyInfo);
30
+ this.emit('setKey', keyInfo);
31
+ }
32
+
33
+ /**
34
+ * callback being invoked after a ratchet request has been performed on the local participant
35
+ * that surfaces the new key material.
36
+ * @param material
37
+ * @param keyIndex
38
+ */
39
+ protected onKeyRatcheted = (material: CryptoKey, keyIndex?: number) => {
40
+ console.debug('key ratcheted event received', material, keyIndex);
41
+ };
42
+
43
+ getKeys() {
44
+ return Array.from(this.keyInfoMap.values());
45
+ }
46
+
47
+ getOptions() {
48
+ return this.options;
49
+ }
50
+
51
+ ratchetKey(participantId?: string, keyIndex?: number) {
52
+ this.emit('ratchetRequest', participantId, keyIndex);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * A basic KeyProvider implementation intended for a single shared
58
+ * passphrase between all participants
59
+ * @experimental
60
+ */
61
+ export class ExternalE2EEKeyProvider extends BaseKeyProvider {
62
+ ratchetInterval: number | undefined;
63
+
64
+ constructor(options: Partial<Omit<KeyProviderOptions, 'sharedKey'>> = {}) {
65
+ const opts: Partial<KeyProviderOptions> = { ...options, sharedKey: true };
66
+ super(opts);
67
+ }
68
+
69
+ /**
70
+ * Accepts a passphrase that's used to create the crypto keys
71
+ * @param key
72
+ */
73
+ async setKey(key: string) {
74
+ const derivedKey = await createKeyMaterialFromString(key);
75
+ this.onSetEncryptionKey(derivedKey);
76
+ }
77
+ }
@@ -0,0 +1,40 @@
1
+ import type { KeyProviderOptions } from './types';
2
+
3
+ export const ENCRYPTION_ALGORITHM = 'AES-GCM';
4
+
5
+ // We use a ringbuffer of keys so we can change them and still decode packets that were
6
+ // encrypted with an old key. We use a size of 16 which corresponds to the four bits
7
+ // in the frame trailer.
8
+ export const KEYRING_SIZE = 16;
9
+
10
+ // We copy the first bytes of the VP8 payload unencrypted.
11
+ // For keyframes this is 10 bytes, for non-keyframes (delta) 3. See
12
+ // https://tools.ietf.org/html/rfc6386#section-9.1
13
+ // This allows the bridge to continue detecting keyframes (only one byte needed in the JVB)
14
+ // and is also a bit easier for the VP8 decoder (i.e. it generates funny garbage pictures
15
+ // instead of being unable to decode).
16
+ // This is a bit for show and we might want to reduce to 1 unconditionally in the final version.
17
+ //
18
+ // For audio (where frame.type is not set) we do not encrypt the opus TOC byte:
19
+ // https://tools.ietf.org/html/rfc6716#section-3.1
20
+ export const UNENCRYPTED_BYTES = {
21
+ key: 10,
22
+ delta: 3,
23
+ audio: 1, // frame.type is not set on audio, so this is set manually
24
+ empty: 0,
25
+ } as const;
26
+
27
+ /* We use a 12 byte bit IV. This is signalled in plain together with the
28
+ packet. See https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt#parameters */
29
+ export const IV_LENGTH = 12;
30
+
31
+ // flag set to indicate that e2ee has been setup for sender/receiver;
32
+ export const E2EE_FLAG = 'lk_e2ee';
33
+
34
+ export const SALT = 'LKFrameEncryptionKey';
35
+
36
+ export const KEY_PROVIDER_DEFAULTS: KeyProviderOptions = {
37
+ sharedKey: false,
38
+ ratchetSalt: SALT,
39
+ ratchetWindowSize: 8,
40
+ } as const;
@@ -0,0 +1,16 @@
1
+ import { LivekitError } from '../room/errors';
2
+
3
+ export enum CryptorErrorReason {
4
+ InvalidKey = 0,
5
+ MissingKey = 1,
6
+ InternalError = 2,
7
+ }
8
+
9
+ export class CryptorError extends LivekitError {
10
+ reason: CryptorErrorReason;
11
+
12
+ constructor(message?: string, reason: CryptorErrorReason = CryptorErrorReason.InternalError) {
13
+ super(40, message);
14
+ this.reason = reason;
15
+ }
16
+ }
@@ -0,0 +1,3 @@
1
+ export * from './KeyProvider';
2
+ export * from './utils';
3
+ export * from './types';
@@ -0,0 +1,160 @@
1
+ import type Participant from '../room/participant/Participant';
2
+ import type { VideoCodec } from '../room/track/options';
3
+ import type { BaseKeyProvider } from './KeyProvider';
4
+ import type { CryptorError } from './errors';
5
+
6
+ export interface BaseMessage {
7
+ kind: string;
8
+ data?: unknown;
9
+ }
10
+
11
+ export interface InitMessage extends BaseMessage {
12
+ kind: 'init';
13
+ data: {
14
+ keyProviderOptions: KeyProviderOptions;
15
+ };
16
+ }
17
+
18
+ export interface SetKeyMessage extends BaseMessage {
19
+ kind: 'setKey';
20
+ data: {
21
+ participantId?: string;
22
+ key: CryptoKey;
23
+ keyIndex?: number;
24
+ };
25
+ }
26
+
27
+ export interface RTPVideoMapMessage extends BaseMessage {
28
+ kind: 'setRTPMap';
29
+ data: {
30
+ map: Map<number, VideoCodec>;
31
+ };
32
+ }
33
+
34
+ export interface EncodeMessage extends BaseMessage {
35
+ kind: 'decode' | 'encode';
36
+ data: {
37
+ participantId: string;
38
+ readableStream: ReadableStream;
39
+ writableStream: WritableStream;
40
+ trackId: string;
41
+ codec?: VideoCodec;
42
+ };
43
+ }
44
+
45
+ export interface RemoveTransformMessage extends BaseMessage {
46
+ kind: 'removeTransform';
47
+ data: {
48
+ participantId: string;
49
+ trackId: string;
50
+ };
51
+ }
52
+
53
+ export interface UpdateCodecMessage extends BaseMessage {
54
+ kind: 'updateCodec';
55
+ data: {
56
+ participantId: string;
57
+ trackId: string;
58
+ codec: VideoCodec;
59
+ };
60
+ }
61
+
62
+ export interface RatchetRequestMessage extends BaseMessage {
63
+ kind: 'ratchetRequest';
64
+ data: {
65
+ participantId: string | undefined;
66
+ keyIndex?: number;
67
+ };
68
+ }
69
+
70
+ export interface RatchetMessage extends BaseMessage {
71
+ kind: 'ratchetKey';
72
+ data: {
73
+ // participantId: string | undefined;
74
+ keyIndex?: number;
75
+ material: CryptoKey;
76
+ };
77
+ }
78
+
79
+ export interface ErrorMessage extends BaseMessage {
80
+ kind: 'error';
81
+ data: {
82
+ error: Error;
83
+ };
84
+ }
85
+
86
+ export interface EnableMessage extends BaseMessage {
87
+ kind: 'enable';
88
+ data: {
89
+ // if no participant id is set it indicates publisher encryption enable/disable
90
+ participantId?: string;
91
+ enabled: boolean;
92
+ };
93
+ }
94
+
95
+ export type E2EEWorkerMessage =
96
+ | InitMessage
97
+ | SetKeyMessage
98
+ | EncodeMessage
99
+ | ErrorMessage
100
+ | EnableMessage
101
+ | RemoveTransformMessage
102
+ | RTPVideoMapMessage
103
+ | UpdateCodecMessage
104
+ | RatchetRequestMessage
105
+ | RatchetMessage;
106
+
107
+ export type KeySet = { material: CryptoKey; encryptionKey: CryptoKey };
108
+
109
+ export type KeyProviderOptions = {
110
+ sharedKey: boolean;
111
+ ratchetSalt: string;
112
+ ratchetWindowSize: number;
113
+ };
114
+
115
+ export type KeyProviderCallbacks = {
116
+ setKey: (keyInfo: KeyInfo) => void;
117
+ ratchetRequest: (participantId?: string, keyIndex?: number) => void;
118
+ /** currently only emitted for local participant */
119
+ keyRatcheted: (material: CryptoKey, keyIndex?: number) => void;
120
+ };
121
+
122
+ export type ParticipantKeyHandlerCallbacks = {
123
+ keyRatcheted: (material: CryptoKey, keyIndex?: number, participantId?: string) => void;
124
+ };
125
+
126
+ export type E2EEManagerCallbacks = {
127
+ participantEncryptionStatusChanged: (enabled: boolean, participant?: Participant) => void;
128
+ encryptionError: (error: Error) => void;
129
+ };
130
+
131
+ export const EncryptionEvent = {
132
+ ParticipantEncryptionStatusChanged: 'participantEncryptionStatusChanged',
133
+ Error: 'encryptionError',
134
+ } as const;
135
+
136
+ export type CryptorCallbacks = {
137
+ cryptorError: (error: CryptorError) => void;
138
+ };
139
+
140
+ export const CryptorEvent = {
141
+ Error: 'cryptorError',
142
+ } as const;
143
+
144
+ export type KeyInfo = {
145
+ key: CryptoKey;
146
+ participantId?: string;
147
+ keyIndex?: number;
148
+ };
149
+
150
+ export type E2EEOptions = {
151
+ keyProvider: BaseKeyProvider;
152
+ worker: Worker;
153
+ };
154
+
155
+ export type DecodeRatchetOptions = {
156
+ /** attempts */
157
+ ratchetCount: number;
158
+ /** ratcheted key to try */
159
+ encryptionKey?: CryptoKey;
160
+ };