livekit-client 2.15.7 → 2.15.9
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 +2442 -323
- 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 +31 -2
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/api/WebSocketStream.d.ts +29 -0
- package/dist/src/api/WebSocketStream.d.ts.map +1 -0
- package/dist/src/api/utils.d.ts +2 -0
- package/dist/src/api/utils.d.ts.map +1 -1
- package/dist/src/connectionHelper/checks/publishVideo.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 +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 +10 -2
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +1 -0
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +6 -4
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +6 -3
- 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/defaults.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.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 +8 -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/api → api}/SignalClient.d.ts +31 -2
- package/dist/ts4.2/api/WebSocketStream.d.ts +29 -0
- package/dist/ts4.2/{src/api → api}/utils.d.ts +2 -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} +10 -2
- package/dist/ts4.2/{src/room → room}/PCTransport.d.ts +1 -0
- package/dist/ts4.2/{src/room → room}/PCTransportManager.d.ts +6 -4
- package/dist/ts4.2/{src/room → room}/RTCEngine.d.ts +6 -3
- 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/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 +8 -1
- package/dist/ts4.2/utils/camelToSnakeCase.d.ts +8 -0
- package/package.json +11 -10
- package/src/api/SignalClient.test.ts +688 -0
- package/src/api/SignalClient.ts +308 -161
- package/src/api/WebSocketStream.test.ts +625 -0
- package/src/api/WebSocketStream.ts +118 -0
- package/src/api/utils.ts +10 -0
- package/src/connectionHelper/checks/publishVideo.ts +5 -0
- package/src/connectionHelper/checks/turn.ts +1 -0
- package/src/connectionHelper/checks/webrtc.ts +1 -1
- package/src/connectionHelper/checks/websocket.ts +1 -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 +16 -1
- package/src/logger.ts +1 -0
- package/src/options.ts +15 -2
- package/src/room/PCTransport.ts +7 -3
- package/src/room/PCTransportManager.ts +39 -35
- package/src/room/RTCEngine.ts +109 -22
- package/src/room/Room.ts +43 -18
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +64 -17
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +7 -0
- package/src/room/defaults.ts +1 -0
- package/src/room/errors.ts +3 -0
- package/src/room/participant/LocalParticipant.ts +8 -6
- 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.test.ts +63 -0
- package/src/room/token-source/utils.ts +40 -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 +37 -4
- package/src/utils/camelToSnakeCase.ts +16 -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}/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/LocalParticipant.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,118 @@
|
|
|
1
|
+
// https://github.com/CarterLi/websocketstream-polyfill
|
|
2
|
+
import { sleep } from '../room/utils';
|
|
3
|
+
|
|
4
|
+
export interface WebSocketConnection<T extends ArrayBuffer | string = ArrayBuffer | string> {
|
|
5
|
+
readable: ReadableStream<T>;
|
|
6
|
+
writable: WritableStream<T>;
|
|
7
|
+
protocol: string;
|
|
8
|
+
extensions: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface WebSocketCloseInfo {
|
|
12
|
+
closeCode?: number;
|
|
13
|
+
reason?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface WebSocketStreamOptions {
|
|
17
|
+
protocols?: string[];
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) with [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)
|
|
23
|
+
*
|
|
24
|
+
* @see https://web.dev/websocketstream/
|
|
25
|
+
*/
|
|
26
|
+
export class WebSocketStream<T extends ArrayBuffer | string = ArrayBuffer | string> {
|
|
27
|
+
readonly url: string;
|
|
28
|
+
|
|
29
|
+
readonly opened: Promise<WebSocketConnection<T>>;
|
|
30
|
+
|
|
31
|
+
readonly closed: Promise<WebSocketCloseInfo>;
|
|
32
|
+
|
|
33
|
+
readonly close: (closeInfo?: WebSocketCloseInfo) => void;
|
|
34
|
+
|
|
35
|
+
get readyState(): number {
|
|
36
|
+
return this.ws.readyState;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private ws: WebSocket;
|
|
40
|
+
|
|
41
|
+
constructor(url: string, options: WebSocketStreamOptions = {}) {
|
|
42
|
+
if (options.signal?.aborted) {
|
|
43
|
+
throw new DOMException('This operation was aborted', 'AbortError');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.url = url;
|
|
47
|
+
|
|
48
|
+
const ws = new WebSocket(url, options.protocols ?? []);
|
|
49
|
+
ws.binaryType = 'arraybuffer';
|
|
50
|
+
this.ws = ws;
|
|
51
|
+
|
|
52
|
+
const closeWithInfo = ({ closeCode: code, reason }: WebSocketCloseInfo = {}) =>
|
|
53
|
+
ws.close(code, reason);
|
|
54
|
+
|
|
55
|
+
this.opened = new Promise((resolve, reject) => {
|
|
56
|
+
ws.onopen = () => {
|
|
57
|
+
resolve({
|
|
58
|
+
readable: new ReadableStream<T>({
|
|
59
|
+
start(controller) {
|
|
60
|
+
ws.onmessage = ({ data }) => controller.enqueue(data);
|
|
61
|
+
ws.onerror = (e) => controller.error(e);
|
|
62
|
+
},
|
|
63
|
+
cancel: closeWithInfo,
|
|
64
|
+
}),
|
|
65
|
+
writable: new WritableStream<T>({
|
|
66
|
+
write(chunk) {
|
|
67
|
+
ws.send(chunk);
|
|
68
|
+
},
|
|
69
|
+
abort() {
|
|
70
|
+
ws.close();
|
|
71
|
+
},
|
|
72
|
+
close: closeWithInfo,
|
|
73
|
+
}),
|
|
74
|
+
protocol: ws.protocol,
|
|
75
|
+
extensions: ws.extensions,
|
|
76
|
+
});
|
|
77
|
+
ws.removeEventListener('error', reject);
|
|
78
|
+
};
|
|
79
|
+
ws.addEventListener('error', reject);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
this.closed = new Promise<WebSocketCloseInfo>((resolve, reject) => {
|
|
83
|
+
const rejectHandler = async () => {
|
|
84
|
+
const closePromise = new Promise<CloseEvent>((res) => {
|
|
85
|
+
if (ws.readyState === WebSocket.CLOSED) return;
|
|
86
|
+
else {
|
|
87
|
+
ws.addEventListener(
|
|
88
|
+
'close',
|
|
89
|
+
(closeEv: CloseEvent) => {
|
|
90
|
+
res(closeEv);
|
|
91
|
+
},
|
|
92
|
+
{ once: true },
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
const reason = await Promise.race([sleep(250), closePromise]);
|
|
97
|
+
if (!reason) {
|
|
98
|
+
reject(new Error('Encountered unspecified websocket error without a timely close event'));
|
|
99
|
+
} else {
|
|
100
|
+
// if we can infer the close reason from the close event then resolve the promise, we don't need to throw
|
|
101
|
+
resolve(reason);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
ws.onclose = ({ code, reason }) => {
|
|
105
|
+
resolve({ closeCode: code, reason });
|
|
106
|
+
ws.removeEventListener('error', rejectHandler);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
ws.addEventListener('error', rejectHandler);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (options.signal) {
|
|
113
|
+
options.signal.onabort = () => ws.close();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.close = closeWithInfo;
|
|
117
|
+
}
|
|
118
|
+
}
|
package/src/api/utils.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SignalResponse } from '@livekit/protocol';
|
|
1
2
|
import { toHttpUrl, toWebsocketUrl } from '../room/utils';
|
|
2
3
|
|
|
3
4
|
export function createRtcUrl(url: string, searchParams: URLSearchParams) {
|
|
@@ -21,3 +22,12 @@ function appendUrlPath(urlObj: URL, path: string) {
|
|
|
21
22
|
urlObj.pathname = `${ensureTrailingSlash(urlObj.pathname)}${path}`;
|
|
22
23
|
return urlObj.toString();
|
|
23
24
|
}
|
|
25
|
+
|
|
26
|
+
export function parseSignalResponse(value: ArrayBuffer | string) {
|
|
27
|
+
if (typeof value === 'string') {
|
|
28
|
+
return SignalResponse.fromJson(JSON.parse(value), { ignoreUnknownFields: true });
|
|
29
|
+
} else if (value instanceof ArrayBuffer) {
|
|
30
|
+
return SignalResponse.fromBinary(new Uint8Array(value));
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`could not decode websocket message: ${typeof value}`);
|
|
33
|
+
}
|
|
@@ -46,6 +46,11 @@ export class PublishVideoCheck extends Checker {
|
|
|
46
46
|
const video = document.createElement('video');
|
|
47
47
|
video.srcObject = stream;
|
|
48
48
|
video.muted = true;
|
|
49
|
+
video.autoplay = true;
|
|
50
|
+
video.playsInline = true;
|
|
51
|
+
// For iOS Safari
|
|
52
|
+
video.setAttribute('playsinline', 'true');
|
|
53
|
+
document.body.appendChild(video);
|
|
49
54
|
|
|
50
55
|
await new Promise<void>((resolve) => {
|
|
51
56
|
video.onplay = () => {
|
|
@@ -38,7 +38,7 @@ export class WebRTCCheck extends Checker {
|
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
if (this.room.engine.pcManager) {
|
|
41
|
+
if (this.room.engine.pcManager?.subscriber) {
|
|
42
42
|
this.room.engine.pcManager.subscriber.onIceCandidateError = (ev) => {
|
|
43
43
|
if (ev instanceof RTCPeerConnectionIceErrorEvent) {
|
|
44
44
|
this.appendWarning(
|
|
@@ -18,6 +18,7 @@ export class WebSocketCheck extends Checker {
|
|
|
18
18
|
maxRetries: 0,
|
|
19
19
|
e2eeEnabled: false,
|
|
20
20
|
websocketTimeout: 15_000,
|
|
21
|
+
singlePeerConnection: false,
|
|
21
22
|
});
|
|
22
23
|
this.appendMessage(`Connected to server, version ${joinRes.serverVersion}.`);
|
|
23
24
|
if (joinRes.serverInfo?.edition === ServerInfo_Edition.Cloud && joinRes.serverInfo?.region) {
|
package/src/e2ee/E2eeManager.ts
CHANGED
|
@@ -11,15 +11,19 @@ import type RemoteTrack from '../room/track/RemoteTrack';
|
|
|
11
11
|
import type { Track } from '../room/track/Track';
|
|
12
12
|
import type { VideoCodec } from '../room/track/options';
|
|
13
13
|
import { mimeTypeToVideoCodecString } from '../room/track/utils';
|
|
14
|
-
import { isLocalTrack, isSafariBased, isVideoTrack } from '../room/utils';
|
|
14
|
+
import { Future, isLocalTrack, isSafariBased, isVideoTrack } from '../room/utils';
|
|
15
15
|
import type { BaseKeyProvider } from './KeyProvider';
|
|
16
16
|
import { E2EE_FLAG } from './constants';
|
|
17
17
|
import { type E2EEManagerCallbacks, EncryptionEvent, KeyProviderEvent } from './events';
|
|
18
18
|
import type {
|
|
19
|
+
DecryptDataRequestMessage,
|
|
20
|
+
DecryptDataResponseMessage,
|
|
19
21
|
E2EEManagerOptions,
|
|
20
22
|
E2EEWorkerMessage,
|
|
21
23
|
EnableMessage,
|
|
22
24
|
EncodeMessage,
|
|
25
|
+
EncryptDataRequestMessage,
|
|
26
|
+
EncryptDataResponseMessage,
|
|
23
27
|
InitMessage,
|
|
24
28
|
KeyInfo,
|
|
25
29
|
RTPVideoMapMessage,
|
|
@@ -35,8 +39,17 @@ import { isE2EESupported, isScriptTransformSupported } from './utils';
|
|
|
35
39
|
export interface BaseE2EEManager {
|
|
36
40
|
setup(room: Room): void;
|
|
37
41
|
setupEngine(engine: RTCEngine): void;
|
|
42
|
+
isEnabled: boolean;
|
|
43
|
+
isDataChannelEncryptionEnabled: boolean;
|
|
38
44
|
setParticipantCryptorEnabled(enabled: boolean, participantIdentity: string): void;
|
|
39
45
|
setSifTrailer(trailer: Uint8Array): void;
|
|
46
|
+
encryptData(data: Uint8Array): Promise<EncryptDataResponseMessage['data']>;
|
|
47
|
+
handleEncryptedData(
|
|
48
|
+
payload: Uint8Array,
|
|
49
|
+
iv: Uint8Array,
|
|
50
|
+
participantIdentity: string,
|
|
51
|
+
keyIndex: number,
|
|
52
|
+
): Promise<DecryptDataResponseMessage['data']>;
|
|
40
53
|
on<E extends keyof E2EEManagerCallbacks>(event: E, listener: E2EEManagerCallbacks[E]): this;
|
|
41
54
|
}
|
|
42
55
|
|
|
@@ -55,11 +68,26 @@ export class E2EEManager
|
|
|
55
68
|
|
|
56
69
|
private keyProvider: BaseKeyProvider;
|
|
57
70
|
|
|
58
|
-
|
|
71
|
+
private decryptDataRequests: Map<string, Future<DecryptDataResponseMessage['data']>> = new Map();
|
|
72
|
+
|
|
73
|
+
private encryptDataRequests: Map<string, Future<EncryptDataResponseMessage['data']>> = new Map();
|
|
74
|
+
|
|
75
|
+
private dataChannelEncryptionEnabled: boolean;
|
|
76
|
+
|
|
77
|
+
constructor(options: E2EEManagerOptions, dcEncryptionEnabled: boolean) {
|
|
59
78
|
super();
|
|
60
79
|
this.keyProvider = options.keyProvider;
|
|
61
80
|
this.worker = options.worker;
|
|
62
81
|
this.encryptionEnabled = false;
|
|
82
|
+
this.dataChannelEncryptionEnabled = dcEncryptionEnabled;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get isEnabled(): boolean {
|
|
86
|
+
return this.encryptionEnabled;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get isDataChannelEncryptionEnabled(): boolean {
|
|
90
|
+
return this.isEnabled && this.dataChannelEncryptionEnabled;
|
|
63
91
|
}
|
|
64
92
|
|
|
65
93
|
/**
|
|
@@ -160,6 +188,19 @@ export class E2EEManager
|
|
|
160
188
|
data.keyIndex,
|
|
161
189
|
);
|
|
162
190
|
break;
|
|
191
|
+
|
|
192
|
+
case 'decryptDataResponse':
|
|
193
|
+
const decryptFuture = this.decryptDataRequests.get(data.uuid);
|
|
194
|
+
if (decryptFuture?.resolve) {
|
|
195
|
+
decryptFuture.resolve(data);
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
case 'encryptDataResponse':
|
|
199
|
+
const encryptFuture = this.encryptDataRequests.get(data.uuid);
|
|
200
|
+
if (encryptFuture?.resolve) {
|
|
201
|
+
encryptFuture.resolve(data as EncryptDataResponseMessage['data']);
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
163
204
|
default:
|
|
164
205
|
break;
|
|
165
206
|
}
|
|
@@ -250,6 +291,57 @@ export class E2EEManager
|
|
|
250
291
|
);
|
|
251
292
|
}
|
|
252
293
|
|
|
294
|
+
async encryptData(data: Uint8Array): Promise<EncryptDataResponseMessage['data']> {
|
|
295
|
+
if (!this.worker) {
|
|
296
|
+
throw Error('could not encrypt data, worker is missing');
|
|
297
|
+
}
|
|
298
|
+
const uuid = crypto.randomUUID();
|
|
299
|
+
const msg: EncryptDataRequestMessage = {
|
|
300
|
+
kind: 'encryptDataRequest',
|
|
301
|
+
data: {
|
|
302
|
+
uuid,
|
|
303
|
+
payload: data,
|
|
304
|
+
participantIdentity: this.room!.localParticipant.identity,
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
const future = new Future<EncryptDataResponseMessage['data']>();
|
|
308
|
+
future.onFinally = () => {
|
|
309
|
+
this.encryptDataRequests.delete(uuid);
|
|
310
|
+
};
|
|
311
|
+
this.encryptDataRequests.set(uuid, future);
|
|
312
|
+
this.worker.postMessage(msg);
|
|
313
|
+
return future!.promise!;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
handleEncryptedData(
|
|
317
|
+
payload: Uint8Array,
|
|
318
|
+
iv: Uint8Array,
|
|
319
|
+
participantIdentity: string,
|
|
320
|
+
keyIndex: number,
|
|
321
|
+
) {
|
|
322
|
+
if (!this.worker) {
|
|
323
|
+
throw Error('could not handle encrypted data, worker is missing');
|
|
324
|
+
}
|
|
325
|
+
const uuid = crypto.randomUUID();
|
|
326
|
+
const msg: DecryptDataRequestMessage = {
|
|
327
|
+
kind: 'decryptDataRequest',
|
|
328
|
+
data: {
|
|
329
|
+
uuid,
|
|
330
|
+
payload,
|
|
331
|
+
iv,
|
|
332
|
+
participantIdentity,
|
|
333
|
+
keyIndex,
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
const future = new Future<DecryptDataResponseMessage['data']>();
|
|
337
|
+
future.onFinally = () => {
|
|
338
|
+
this.decryptDataRequests.delete(uuid);
|
|
339
|
+
};
|
|
340
|
+
this.decryptDataRequests.set(uuid, future);
|
|
341
|
+
this.worker.postMessage(msg);
|
|
342
|
+
return future.promise;
|
|
343
|
+
}
|
|
344
|
+
|
|
253
345
|
private postRatchetRequest(participantIdentity?: string, keyIndex?: number) {
|
|
254
346
|
if (!this.worker) {
|
|
255
347
|
throw Error('could not ratchet key, worker is missing');
|
package/src/e2ee/types.ts
CHANGED
|
@@ -109,6 +109,44 @@ export interface InitAck extends BaseMessage {
|
|
|
109
109
|
};
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
export interface DecryptDataRequestMessage extends BaseMessage {
|
|
113
|
+
kind: 'decryptDataRequest';
|
|
114
|
+
data: {
|
|
115
|
+
uuid: string;
|
|
116
|
+
payload: Uint8Array;
|
|
117
|
+
iv: Uint8Array;
|
|
118
|
+
participantIdentity: string;
|
|
119
|
+
keyIndex: number;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface DecryptDataResponseMessage extends BaseMessage {
|
|
124
|
+
kind: 'decryptDataResponse';
|
|
125
|
+
data: {
|
|
126
|
+
uuid: string;
|
|
127
|
+
payload: Uint8Array;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface EncryptDataRequestMessage extends BaseMessage {
|
|
132
|
+
kind: 'encryptDataRequest';
|
|
133
|
+
data: {
|
|
134
|
+
uuid: string;
|
|
135
|
+
payload: Uint8Array;
|
|
136
|
+
participantIdentity: string;
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface EncryptDataResponseMessage extends BaseMessage {
|
|
141
|
+
kind: 'encryptDataResponse';
|
|
142
|
+
data: {
|
|
143
|
+
uuid: string;
|
|
144
|
+
payload: Uint8Array;
|
|
145
|
+
iv: Uint8Array;
|
|
146
|
+
keyIndex: number;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
112
150
|
export type E2EEWorkerMessage =
|
|
113
151
|
| InitMessage
|
|
114
152
|
| SetKeyMessage
|
|
@@ -121,7 +159,11 @@ export type E2EEWorkerMessage =
|
|
|
121
159
|
| RatchetRequestMessage
|
|
122
160
|
| RatchetMessage
|
|
123
161
|
| SifTrailerMessage
|
|
124
|
-
| InitAck
|
|
162
|
+
| InitAck
|
|
163
|
+
| DecryptDataRequestMessage
|
|
164
|
+
| DecryptDataResponseMessage
|
|
165
|
+
| EncryptDataRequestMessage
|
|
166
|
+
| EncryptDataResponseMessage;
|
|
125
167
|
|
|
126
168
|
export type KeySet = { material: CryptoKey; encryptionKey: CryptoKey };
|
|
127
169
|
|
|
@@ -150,6 +192,7 @@ export type E2EEManagerOptions = {
|
|
|
150
192
|
keyProvider: BaseKeyProvider;
|
|
151
193
|
worker: Worker;
|
|
152
194
|
};
|
|
195
|
+
|
|
153
196
|
export type E2EEOptions =
|
|
154
197
|
| E2EEManagerOptions
|
|
155
198
|
| {
|
package/src/e2ee/utils.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type DataPacket, EncryptedPacketPayload } from '@livekit/protocol';
|
|
1
2
|
import { ENCRYPTION_ALGORITHM } from './constants';
|
|
2
3
|
|
|
3
4
|
export function isE2EESupported() {
|
|
@@ -176,3 +177,18 @@ export function writeRbsp(data_in: Uint8Array): Uint8Array {
|
|
|
176
177
|
}
|
|
177
178
|
return new Uint8Array(dataOut);
|
|
178
179
|
}
|
|
180
|
+
|
|
181
|
+
export function asEncryptablePacket(packet: DataPacket): EncryptedPacketPayload | undefined {
|
|
182
|
+
if (
|
|
183
|
+
packet.value?.case !== 'sipDtmf' &&
|
|
184
|
+
packet.value?.case !== 'metrics' &&
|
|
185
|
+
packet.value?.case !== 'speaker' &&
|
|
186
|
+
packet.value?.case !== 'transcription' &&
|
|
187
|
+
packet.value?.case !== 'encryptedPacket'
|
|
188
|
+
) {
|
|
189
|
+
return new EncryptedPacketPayload({
|
|
190
|
+
value: packet.value,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
@@ -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
|
+
});
|