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.
- package/README.md +13 -1
- package/dist/livekit-client.e2ee.worker.js +2 -0
- package/dist/livekit-client.e2ee.worker.js.map +1 -0
- package/dist/livekit-client.e2ee.worker.mjs +1545 -0
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -0
- package/dist/livekit-client.esm.mjs +4786 -4065
- 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/api/SignalClient.d.ts +4 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/connectionHelper/checks/turn.d.ts.map +1 -1
- package/dist/src/connectionHelper/checks/websocket.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts +45 -0
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -0
- package/dist/src/e2ee/KeyProvider.d.ts +42 -0
- package/dist/src/e2ee/KeyProvider.d.ts.map +1 -0
- package/dist/src/e2ee/constants.d.ts +14 -0
- package/dist/src/e2ee/constants.d.ts.map +1 -0
- package/dist/src/e2ee/errors.d.ts +11 -0
- package/dist/src/e2ee/errors.d.ts.map +1 -0
- package/dist/src/e2ee/index.d.ts +4 -0
- package/dist/src/e2ee/index.d.ts.map +1 -0
- package/dist/src/e2ee/types.d.ts +129 -0
- package/dist/src/e2ee/types.d.ts.map +1 -0
- package/dist/src/e2ee/utils.d.ts +24 -0
- package/dist/src/e2ee/utils.d.ts.map +1 -0
- package/dist/src/e2ee/worker/FrameCryptor.d.ts +174 -0
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -0
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +54 -0
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -0
- package/dist/src/e2ee/worker/e2ee.worker.d.ts +2 -0
- package/dist/src/e2ee/worker/e2ee.worker.d.ts.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logger.d.ts +4 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/options.d.ts +5 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/proto/livekit_models.d.ts +2 -2
- package/dist/src/proto/livekit_models.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +3 -1
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +17 -3
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +10 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +14 -2
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +7 -2
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +1 -0
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts +6 -4
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/TrackPublication.d.ts +3 -0
- package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/create.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +2 -2
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/track/utils.d.ts +9 -0
- package/dist/src/room/track/utils.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +2 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/MockMediaStreamTrack.d.ts.map +1 -1
- package/dist/src/utils/browserParser.d.ts +2 -0
- package/dist/src/utils/browserParser.d.ts.map +1 -1
- package/dist/ts4.2/src/api/SignalClient.d.ts +4 -1
- package/dist/ts4.2/src/e2ee/E2eeManager.d.ts +45 -0
- package/dist/ts4.2/src/e2ee/KeyProvider.d.ts +42 -0
- package/dist/ts4.2/src/e2ee/constants.d.ts +14 -0
- package/dist/ts4.2/src/e2ee/errors.d.ts +11 -0
- package/dist/ts4.2/src/e2ee/index.d.ts +4 -0
- package/dist/ts4.2/src/e2ee/types.d.ts +129 -0
- package/dist/ts4.2/src/e2ee/utils.d.ts +24 -0
- package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +174 -0
- package/dist/ts4.2/src/e2ee/worker/ParticipantKeyHandler.d.ts +54 -0
- package/dist/ts4.2/src/e2ee/worker/e2ee.worker.d.ts +2 -0
- package/dist/ts4.2/src/index.d.ts +1 -0
- package/dist/ts4.2/src/logger.d.ts +4 -1
- package/dist/ts4.2/src/options.d.ts +5 -0
- package/dist/ts4.2/src/proto/livekit_models.d.ts +2 -2
- package/dist/ts4.2/src/room/PCTransport.d.ts +3 -1
- package/dist/ts4.2/src/room/RTCEngine.d.ts +17 -3
- package/dist/ts4.2/src/room/Room.d.ts +10 -0
- package/dist/ts4.2/src/room/events.d.ts +14 -2
- package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +7 -2
- package/dist/ts4.2/src/room/participant/Participant.d.ts +1 -0
- package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +6 -4
- package/dist/ts4.2/src/room/track/TrackPublication.d.ts +3 -0
- package/dist/ts4.2/src/room/track/options.d.ts +6 -6
- package/dist/ts4.2/src/room/track/utils.d.ts +9 -0
- package/dist/ts4.2/src/room/utils.d.ts +2 -0
- package/dist/ts4.2/src/utils/browserParser.d.ts +2 -0
- package/package.json +17 -7
- package/src/api/SignalClient.ts +28 -9
- package/src/connectionHelper/checks/turn.ts +1 -0
- package/src/connectionHelper/checks/websocket.ts +1 -0
- package/src/e2ee/E2eeManager.ts +374 -0
- package/src/e2ee/KeyProvider.ts +77 -0
- package/src/e2ee/constants.ts +40 -0
- package/src/e2ee/errors.ts +16 -0
- package/src/e2ee/index.ts +3 -0
- package/src/e2ee/types.ts +160 -0
- package/src/e2ee/utils.ts +127 -0
- package/src/e2ee/worker/FrameCryptor.test.ts +21 -0
- package/src/e2ee/worker/FrameCryptor.ts +612 -0
- package/src/e2ee/worker/ParticipantKeyHandler.ts +144 -0
- package/src/e2ee/worker/e2ee.worker.ts +223 -0
- package/src/e2ee/worker/tsconfig.json +6 -0
- package/src/index.ts +1 -0
- package/src/logger.ts +10 -2
- package/src/options.ts +6 -0
- package/src/proto/livekit_models.ts +12 -12
- package/src/room/PCTransport.ts +39 -9
- package/src/room/RTCEngine.ts +127 -34
- package/src/room/Room.ts +94 -29
- package/src/room/defaults.ts +1 -1
- package/src/room/events.ts +14 -0
- package/src/room/participant/LocalParticipant.ts +52 -8
- package/src/room/participant/Participant.ts +4 -0
- package/src/room/participant/RemoteParticipant.ts +19 -15
- package/src/room/track/LocalTrack.ts +5 -4
- package/src/room/track/RemoteVideoTrack.ts +2 -2
- package/src/room/track/TrackPublication.ts +9 -1
- package/src/room/track/create.ts +9 -0
- package/src/room/track/options.ts +3 -2
- package/src/room/track/utils.ts +27 -0
- package/src/room/utils.ts +5 -0
- package/src/room/worker.d.ts +4 -0
- package/src/test/MockMediaStreamTrack.ts +1 -0
- 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,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
|
+
};
|