livekit-client 1.12.3 → 1.13.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (121) hide show
  1. package/dist/livekit-client.e2ee.worker.js +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs +198 -107
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +515 -192
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.umd.js +1 -1
  8. package/dist/livekit-client.umd.js.map +1 -1
  9. package/dist/src/api/SignalClient.d.ts +2 -5
  10. package/dist/src/api/SignalClient.d.ts.map +1 -1
  11. package/dist/src/connectionHelper/checks/turn.d.ts.map +1 -1
  12. package/dist/src/connectionHelper/checks/webrtc.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 +9 -3
  15. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  16. package/dist/src/e2ee/KeyProvider.d.ts +10 -7
  17. package/dist/src/e2ee/KeyProvider.d.ts.map +1 -1
  18. package/dist/src/e2ee/constants.d.ts +2 -0
  19. package/dist/src/e2ee/constants.d.ts.map +1 -1
  20. package/dist/src/e2ee/events.d.ts +34 -0
  21. package/dist/src/e2ee/events.d.ts.map +1 -0
  22. package/dist/src/e2ee/index.d.ts +1 -0
  23. package/dist/src/e2ee/index.d.ts.map +1 -1
  24. package/dist/src/e2ee/types.d.ts +23 -33
  25. package/dist/src/e2ee/types.d.ts.map +1 -1
  26. package/dist/src/e2ee/utils.d.ts +1 -0
  27. package/dist/src/e2ee/utils.d.ts.map +1 -1
  28. package/dist/src/e2ee/worker/FrameCryptor.d.ts +18 -13
  29. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  30. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +6 -8
  31. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
  32. package/dist/src/e2ee/worker/SifGuard.d.ts +11 -0
  33. package/dist/src/e2ee/worker/SifGuard.d.ts.map +1 -0
  34. package/dist/src/options.d.ts +5 -0
  35. package/dist/src/options.d.ts.map +1 -1
  36. package/dist/src/proto/livekit_models_pb.d.ts.map +1 -1
  37. package/dist/src/proto/livekit_rtc_pb.d.ts.map +1 -1
  38. package/dist/src/room/DeviceManager.d.ts +1 -0
  39. package/dist/src/room/DeviceManager.d.ts.map +1 -1
  40. package/dist/src/room/PCTransport.d.ts.map +1 -1
  41. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  42. package/dist/src/room/Room.d.ts +1 -1
  43. package/dist/src/room/Room.d.ts.map +1 -1
  44. package/dist/src/room/defaults.d.ts.map +1 -1
  45. package/dist/src/room/participant/LocalParticipant.d.ts +1 -0
  46. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  47. package/dist/src/room/participant/Participant.d.ts +5 -0
  48. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  49. package/dist/src/room/participant/RemoteParticipant.d.ts +0 -5
  50. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  51. package/dist/src/room/timers.d.ts +2 -2
  52. package/dist/src/room/timers.d.ts.map +1 -1
  53. package/dist/src/room/track/LocalAudioTrack.d.ts +9 -1
  54. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  55. package/dist/src/room/track/LocalTrack.d.ts +3 -3
  56. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  57. package/dist/src/room/track/LocalVideoTrack.d.ts +6 -0
  58. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  59. package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -1
  60. package/dist/src/room/track/processor/types.d.ts +14 -2
  61. package/dist/src/room/track/processor/types.d.ts.map +1 -1
  62. package/dist/src/room/types.d.ts +1 -1
  63. package/dist/src/room/types.d.ts.map +1 -1
  64. package/dist/ts4.2/src/api/SignalClient.d.ts +2 -5
  65. package/dist/ts4.2/src/e2ee/E2eeManager.d.ts +9 -3
  66. package/dist/ts4.2/src/e2ee/KeyProvider.d.ts +10 -7
  67. package/dist/ts4.2/src/e2ee/constants.d.ts +2 -0
  68. package/dist/ts4.2/src/e2ee/events.d.ts +34 -0
  69. package/dist/ts4.2/src/e2ee/index.d.ts +1 -0
  70. package/dist/ts4.2/src/e2ee/types.d.ts +23 -33
  71. package/dist/ts4.2/src/e2ee/utils.d.ts +1 -0
  72. package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +18 -13
  73. package/dist/ts4.2/src/e2ee/worker/ParticipantKeyHandler.d.ts +6 -8
  74. package/dist/ts4.2/src/e2ee/worker/SifGuard.d.ts +11 -0
  75. package/dist/ts4.2/src/options.d.ts +5 -0
  76. package/dist/ts4.2/src/room/DeviceManager.d.ts +1 -0
  77. package/dist/ts4.2/src/room/Room.d.ts +1 -1
  78. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +1 -0
  79. package/dist/ts4.2/src/room/participant/Participant.d.ts +5 -0
  80. package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +0 -5
  81. package/dist/ts4.2/src/room/timers.d.ts +2 -2
  82. package/dist/ts4.2/src/room/track/LocalAudioTrack.d.ts +9 -1
  83. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +3 -3
  84. package/dist/ts4.2/src/room/track/LocalVideoTrack.d.ts +6 -0
  85. package/dist/ts4.2/src/room/track/processor/types.d.ts +14 -2
  86. package/dist/ts4.2/src/room/types.d.ts +1 -1
  87. package/package.json +15 -16
  88. package/src/api/SignalClient.ts +13 -9
  89. package/src/connectionHelper/checks/turn.ts +1 -0
  90. package/src/connectionHelper/checks/webrtc.ts +9 -7
  91. package/src/connectionHelper/checks/websocket.ts +1 -0
  92. package/src/e2ee/E2eeManager.ts +129 -76
  93. package/src/e2ee/KeyProvider.ts +31 -16
  94. package/src/e2ee/constants.ts +3 -0
  95. package/src/e2ee/events.ts +48 -0
  96. package/src/e2ee/index.ts +1 -0
  97. package/src/e2ee/types.ts +27 -41
  98. package/src/e2ee/utils.ts +9 -0
  99. package/src/e2ee/worker/FrameCryptor.ts +90 -47
  100. package/src/e2ee/worker/ParticipantKeyHandler.ts +25 -26
  101. package/src/e2ee/worker/SifGuard.ts +47 -0
  102. package/src/e2ee/worker/e2ee.worker.ts +75 -68
  103. package/src/options.ts +6 -0
  104. package/src/proto/livekit_models_pb.ts +14 -0
  105. package/src/proto/livekit_rtc_pb.ts +14 -0
  106. package/src/room/DeviceManager.ts +7 -2
  107. package/src/room/PCTransport.ts +12 -2
  108. package/src/room/RTCEngine.ts +3 -2
  109. package/src/room/Room.ts +47 -22
  110. package/src/room/defaults.ts +1 -0
  111. package/src/room/participant/LocalParticipant.ts +18 -2
  112. package/src/room/participant/Participant.ts +16 -0
  113. package/src/room/participant/RemoteParticipant.ts +0 -12
  114. package/src/room/track/LocalAudioTrack.ts +45 -0
  115. package/src/room/track/LocalTrack.ts +22 -14
  116. package/src/room/track/LocalVideoTrack.ts +39 -0
  117. package/src/room/track/RemoteAudioTrack.ts +9 -1
  118. package/src/room/track/RemoteTrackPublication.ts +2 -2
  119. package/src/room/track/facingMode.ts +1 -1
  120. package/src/room/track/processor/types.ts +18 -2
  121. package/src/room/types.ts +5 -1
@@ -1,8 +1,10 @@
1
1
  import { EventEmitter } from 'events';
2
2
  import type TypedEventEmitter from 'typed-emitter';
3
+ import log from '../logger';
3
4
  import { KEY_PROVIDER_DEFAULTS } from './constants';
4
- import type { KeyInfo, KeyProviderCallbacks, KeyProviderOptions } from './types';
5
- import { createKeyMaterialFromString } from './utils';
5
+ import { type KeyProviderCallbacks, KeyProviderEvent } from './events';
6
+ import type { KeyInfo, KeyProviderOptions } from './types';
7
+ import { createKeyMaterialFromBuffer, createKeyMaterialFromString } from './utils';
6
8
 
7
9
  /**
8
10
  * @experimental
@@ -16,29 +18,29 @@ export class BaseKeyProvider extends (EventEmitter as new () => TypedEventEmitte
16
18
  super();
17
19
  this.keyInfoMap = new Map();
18
20
  this.options = { ...KEY_PROVIDER_DEFAULTS, ...options };
19
- this.on('keyRatcheted', this.onKeyRatcheted);
21
+ this.on(KeyProviderEvent.KeyRatcheted, this.onKeyRatcheted);
20
22
  }
21
23
 
22
24
  /**
23
25
  * callback to invoke once a key has been set for a participant
24
26
  * @param key
25
- * @param participantId
27
+ * @param participantIdentity
26
28
  * @param keyIndex
27
29
  */
28
- protected onSetEncryptionKey(key: CryptoKey, participantId?: string, keyIndex?: number) {
29
- const keyInfo: KeyInfo = { key, participantId, keyIndex };
30
- this.keyInfoMap.set(`${participantId ?? 'shared'}-${keyIndex ?? 0}`, keyInfo);
31
- this.emit('setKey', keyInfo);
30
+ protected onSetEncryptionKey(key: CryptoKey, participantIdentity?: string, keyIndex?: number) {
31
+ const keyInfo: KeyInfo = { key, participantIdentity, keyIndex };
32
+ this.keyInfoMap.set(`${participantIdentity ?? 'shared'}-${keyIndex ?? 0}`, keyInfo);
33
+ this.emit(KeyProviderEvent.SetKey, keyInfo);
32
34
  }
33
35
 
34
36
  /**
35
- * callback being invoked after a ratchet request has been performed on the local participant
37
+ * callback being invoked after a ratchet request has been performed on a participant
36
38
  * that surfaces the new key material.
37
39
  * @param material
38
40
  * @param keyIndex
39
41
  */
40
42
  protected onKeyRatcheted = (material: CryptoKey, keyIndex?: number) => {
41
- console.debug('key ratcheted event received', material, keyIndex);
43
+ log.debug('key ratcheted event received', { material, keyIndex });
42
44
  };
43
45
 
44
46
  getKeys() {
@@ -49,8 +51,8 @@ export class BaseKeyProvider extends (EventEmitter as new () => TypedEventEmitte
49
51
  return this.options;
50
52
  }
51
53
 
52
- ratchetKey(participantId?: string, keyIndex?: number) {
53
- this.emit('ratchetRequest', participantId, keyIndex);
54
+ ratchetKey(participantIdentity?: string, keyIndex?: number) {
55
+ this.emit(KeyProviderEvent.RatchetRequest, participantIdentity, keyIndex);
54
56
  }
55
57
  }
56
58
 
@@ -63,16 +65,29 @@ export class ExternalE2EEKeyProvider extends BaseKeyProvider {
63
65
  ratchetInterval: number | undefined;
64
66
 
65
67
  constructor(options: Partial<Omit<KeyProviderOptions, 'sharedKey'>> = {}) {
66
- const opts: Partial<KeyProviderOptions> = { ...options, sharedKey: true };
68
+ const opts: Partial<KeyProviderOptions> = {
69
+ ...options,
70
+ sharedKey: true,
71
+ // for a shared key provider failing to decrypt for a specific participant
72
+ // should not mark the key as invalid, so we accept wrong keys forever
73
+ // and won't try to auto-ratchet
74
+ ratchetWindowSize: 0,
75
+ failureTolerance: -1,
76
+ };
67
77
  super(opts);
68
78
  }
69
79
 
70
80
  /**
71
- * Accepts a passphrase that's used to create the crypto keys
81
+ * Accepts a passphrase that's used to create the crypto keys.
82
+ * When passing in a string, PBKDF2 is used.
83
+ * When passing in an Array buffer of cryptographically random numbers, HKDF is being used. (recommended)
72
84
  * @param key
73
85
  */
74
- async setKey(key: string) {
75
- const derivedKey = await createKeyMaterialFromString(key);
86
+ async setKey(key: string | ArrayBuffer) {
87
+ const derivedKey =
88
+ typeof key === 'string'
89
+ ? await createKeyMaterialFromString(key)
90
+ : await createKeyMaterialFromBuffer(key);
76
91
  this.onSetEncryptionKey(derivedKey);
77
92
  }
78
93
  }
@@ -42,3 +42,6 @@ export const KEY_PROVIDER_DEFAULTS: KeyProviderOptions = {
42
42
  ratchetWindowSize: 8,
43
43
  failureTolerance: DECRYPTION_FAILURE_TOLERANCE,
44
44
  } as const;
45
+
46
+ export const MAX_SIF_COUNT = 100;
47
+ export const MAX_SIF_DURATION = 2000;
@@ -0,0 +1,48 @@
1
+ import type Participant from '../room/participant/Participant';
2
+ import type { CryptorError } from './errors';
3
+ import type { KeyInfo } from './types';
4
+
5
+ export enum KeyProviderEvent {
6
+ SetKey = 'setKey',
7
+ RatchetRequest = 'ratchetRequest',
8
+ KeyRatcheted = 'keyRatcheted',
9
+ }
10
+
11
+ export type KeyProviderCallbacks = {
12
+ [KeyProviderEvent.SetKey]: (keyInfo: KeyInfo) => void;
13
+ [KeyProviderEvent.RatchetRequest]: (participantIdentity?: string, keyIndex?: number) => void;
14
+ [KeyProviderEvent.KeyRatcheted]: (material: CryptoKey, keyIndex?: number) => void;
15
+ };
16
+
17
+ export enum KeyHandlerEvent {
18
+ KeyRatcheted = 'keyRatcheted',
19
+ }
20
+
21
+ export type ParticipantKeyHandlerCallbacks = {
22
+ [KeyHandlerEvent.KeyRatcheted]: (
23
+ material: CryptoKey,
24
+ participantIdentity: string,
25
+ keyIndex?: number,
26
+ ) => void;
27
+ };
28
+
29
+ export enum EncryptionEvent {
30
+ ParticipantEncryptionStatusChanged = 'participantEncryptionStatusChanged',
31
+ EncryptionError = 'encryptionError',
32
+ }
33
+
34
+ export type E2EEManagerCallbacks = {
35
+ [EncryptionEvent.ParticipantEncryptionStatusChanged]: (
36
+ enabled: boolean,
37
+ participant: Participant,
38
+ ) => void;
39
+ [EncryptionEvent.EncryptionError]: (error: Error) => void;
40
+ };
41
+
42
+ export type CryptorCallbacks = {
43
+ [CryptorEvent.Error]: (error: CryptorError) => void;
44
+ };
45
+
46
+ export enum CryptorEvent {
47
+ Error = 'cryptorError',
48
+ }
package/src/e2ee/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './KeyProvider';
2
2
  export * from './utils';
3
3
  export * from './types';
4
+ export * from './events';
package/src/e2ee/types.ts CHANGED
@@ -1,7 +1,5 @@
1
- import type Participant from '../room/participant/Participant';
2
1
  import type { VideoCodec } from '../room/track/options';
3
2
  import type { BaseKeyProvider } from './KeyProvider';
4
- import type { CryptorError } from './errors';
5
3
 
6
4
  export interface BaseMessage {
7
5
  kind: string;
@@ -18,7 +16,8 @@ export interface InitMessage extends BaseMessage {
18
16
  export interface SetKeyMessage extends BaseMessage {
19
17
  kind: 'setKey';
20
18
  data: {
21
- participantId?: string;
19
+ participantIdentity?: string;
20
+ isPublisher: boolean;
22
21
  key: CryptoKey;
23
22
  keyIndex?: number;
24
23
  };
@@ -28,13 +27,21 @@ export interface RTPVideoMapMessage extends BaseMessage {
28
27
  kind: 'setRTPMap';
29
28
  data: {
30
29
  map: Map<number, VideoCodec>;
30
+ participantIdentity: string;
31
+ };
32
+ }
33
+
34
+ export interface SifTrailerMessage extends BaseMessage {
35
+ kind: 'setSifTrailer';
36
+ data: {
37
+ trailer: Uint8Array;
31
38
  };
32
39
  }
33
40
 
34
41
  export interface EncodeMessage extends BaseMessage {
35
42
  kind: 'decode' | 'encode';
36
43
  data: {
37
- participantId: string;
44
+ participantIdentity: string;
38
45
  readableStream: ReadableStream;
39
46
  writableStream: WritableStream;
40
47
  trackId: string;
@@ -45,7 +52,7 @@ export interface EncodeMessage extends BaseMessage {
45
52
  export interface RemoveTransformMessage extends BaseMessage {
46
53
  kind: 'removeTransform';
47
54
  data: {
48
- participantId: string;
55
+ participantIdentity: string;
49
56
  trackId: string;
50
57
  };
51
58
  }
@@ -53,7 +60,7 @@ export interface RemoveTransformMessage extends BaseMessage {
53
60
  export interface UpdateCodecMessage extends BaseMessage {
54
61
  kind: 'updateCodec';
55
62
  data: {
56
- participantId: string;
63
+ participantIdentity: string;
57
64
  trackId: string;
58
65
  codec: VideoCodec;
59
66
  };
@@ -62,7 +69,7 @@ export interface UpdateCodecMessage extends BaseMessage {
62
69
  export interface RatchetRequestMessage extends BaseMessage {
63
70
  kind: 'ratchetRequest';
64
71
  data: {
65
- participantId: string | undefined;
72
+ participantIdentity?: string;
66
73
  keyIndex?: number;
67
74
  };
68
75
  }
@@ -70,7 +77,7 @@ export interface RatchetRequestMessage extends BaseMessage {
70
77
  export interface RatchetMessage extends BaseMessage {
71
78
  kind: 'ratchetKey';
72
79
  data: {
73
- // participantId: string | undefined;
80
+ participantIdentity: string;
74
81
  keyIndex?: number;
75
82
  material: CryptoKey;
76
83
  };
@@ -86,8 +93,14 @@ export interface ErrorMessage extends BaseMessage {
86
93
  export interface EnableMessage extends BaseMessage {
87
94
  kind: 'enable';
88
95
  data: {
89
- // if no participant id is set it indicates publisher encryption enable/disable
90
- participantId?: string;
96
+ participantIdentity: string;
97
+ enabled: boolean;
98
+ };
99
+ }
100
+
101
+ export interface InitAck extends BaseMessage {
102
+ kind: 'initAck';
103
+ data: {
91
104
  enabled: boolean;
92
105
  };
93
106
  }
@@ -102,7 +115,9 @@ export type E2EEWorkerMessage =
102
115
  | RTPVideoMapMessage
103
116
  | UpdateCodecMessage
104
117
  | RatchetRequestMessage
105
- | RatchetMessage;
118
+ | RatchetMessage
119
+ | SifTrailerMessage
120
+ | InitAck;
106
121
 
107
122
  export type KeySet = { material: CryptoKey; encryptionKey: CryptoKey };
108
123
 
@@ -113,38 +128,9 @@ export type KeyProviderOptions = {
113
128
  failureTolerance: number;
114
129
  };
115
130
 
116
- export type KeyProviderCallbacks = {
117
- setKey: (keyInfo: KeyInfo) => void;
118
- ratchetRequest: (participantId?: string, keyIndex?: number) => void;
119
- /** currently only emitted for local participant */
120
- keyRatcheted: (material: CryptoKey, keyIndex?: number) => void;
121
- };
122
-
123
- export type ParticipantKeyHandlerCallbacks = {
124
- keyRatcheted: (material: CryptoKey, keyIndex?: number, participantId?: string) => void;
125
- };
126
-
127
- export type E2EEManagerCallbacks = {
128
- participantEncryptionStatusChanged: (enabled: boolean, participant?: Participant) => void;
129
- encryptionError: (error: Error) => void;
130
- };
131
-
132
- export const EncryptionEvent = {
133
- ParticipantEncryptionStatusChanged: 'participantEncryptionStatusChanged',
134
- Error: 'encryptionError',
135
- } as const;
136
-
137
- export type CryptorCallbacks = {
138
- cryptorError: (error: CryptorError) => void;
139
- };
140
-
141
- export const CryptorEvent = {
142
- Error: 'cryptorError',
143
- } as const;
144
-
145
131
  export type KeyInfo = {
146
132
  key: CryptoKey;
147
- participantId?: string;
133
+ participantIdentity?: string;
148
134
  keyIndex?: number;
149
135
  };
150
136
 
package/src/e2ee/utils.ts CHANGED
@@ -56,6 +56,15 @@ export async function createKeyMaterialFromString(password: string) {
56
56
  return keyMaterial;
57
57
  }
58
58
 
59
+ export async function createKeyMaterialFromBuffer(cryptoBuffer: ArrayBuffer) {
60
+ const keyMaterial = await crypto.subtle.importKey('raw', cryptoBuffer, 'HKDF', false, [
61
+ 'deriveBits',
62
+ 'deriveKey',
63
+ ]);
64
+
65
+ return keyMaterial;
66
+ }
67
+
59
68
  function getAlgoOptions(algorithmName: string, salt: string) {
60
69
  const textEncoder = new TextEncoder();
61
70
  const encodedSalt = textEncoder.encode(salt);
@@ -6,15 +6,13 @@ import { workerLogger } from '../../logger';
6
6
  import type { VideoCodec } from '../../room/track/options';
7
7
  import { ENCRYPTION_ALGORITHM, IV_LENGTH, UNENCRYPTED_BYTES } from '../constants';
8
8
  import { CryptorError, CryptorErrorReason } from '../errors';
9
- import {
10
- CryptorCallbacks,
11
- CryptorEvent,
12
- DecodeRatchetOptions,
13
- KeyProviderOptions,
14
- KeySet,
15
- } from '../types';
9
+ import { CryptorCallbacks, CryptorEvent } from '../events';
10
+ import type { DecodeRatchetOptions, KeyProviderOptions, KeySet } from '../types';
16
11
  import { deriveKeys, isVideoFrame } from '../utils';
17
12
  import type { ParticipantKeyHandler } from './ParticipantKeyHandler';
13
+ import { SifGuard } from './SifGuard';
14
+
15
+ export const encryptionEnabledMap: Map<string, boolean> = new Map();
18
16
 
19
17
  export interface FrameCryptorConstructor {
20
18
  new (opts?: unknown): BaseFrameCryptor;
@@ -28,14 +26,14 @@ export interface TransformerInfo {
28
26
  }
29
27
 
30
28
  export class BaseFrameCryptor extends (EventEmitter as new () => TypedEventEmitter<CryptorCallbacks>) {
31
- encodeFunction(
29
+ protected encodeFunction(
32
30
  encodedFrame: RTCEncodedVideoFrame | RTCEncodedAudioFrame,
33
31
  controller: TransformStreamDefaultController,
34
32
  ): Promise<any> {
35
33
  throw Error('not implemented for subclass');
36
34
  }
37
35
 
38
- decodeFunction(
36
+ protected decodeFunction(
39
37
  encodedFrame: RTCEncodedVideoFrame | RTCEncodedAudioFrame,
40
38
  controller: TransformStreamDefaultController,
41
39
  ): Promise<any> {
@@ -50,7 +48,7 @@ export class BaseFrameCryptor extends (EventEmitter as new () => TypedEventEmitt
50
48
  export class FrameCryptor extends BaseFrameCryptor {
51
49
  private sendCounts: Map<number, number>;
52
50
 
53
- private participantId: string | undefined;
51
+ private participantIdentity: string | undefined;
54
52
 
55
53
  private trackId: string | undefined;
56
54
 
@@ -65,22 +63,24 @@ export class FrameCryptor extends BaseFrameCryptor {
65
63
  /**
66
64
  * used for detecting server injected unencrypted frames
67
65
  */
68
- private unencryptedFrameByteTrailer: Uint8Array;
66
+ private sifTrailer: Uint8Array;
67
+
68
+ private sifGuard: SifGuard;
69
69
 
70
70
  constructor(opts: {
71
71
  keys: ParticipantKeyHandler;
72
- participantId: string;
72
+ participantIdentity: string;
73
73
  keyProviderOptions: KeyProviderOptions;
74
- unencryptedFrameBytes?: Uint8Array;
74
+ sifTrailer?: Uint8Array;
75
75
  }) {
76
76
  super();
77
77
  this.sendCounts = new Map();
78
78
  this.keys = opts.keys;
79
- this.participantId = opts.participantId;
79
+ this.participantIdentity = opts.participantIdentity;
80
80
  this.rtpMap = new Map();
81
81
  this.keyProviderOptions = opts.keyProviderOptions;
82
- this.unencryptedFrameByteTrailer =
83
- opts.unencryptedFrameBytes ?? new TextEncoder().encode('LKROCKS');
82
+ this.sifTrailer = opts.sifTrailer ?? Uint8Array.from([]);
83
+ this.sifGuard = new SifGuard();
84
84
  }
85
85
 
86
86
  /**
@@ -90,16 +90,25 @@ export class FrameCryptor extends BaseFrameCryptor {
90
90
  * @param keys
91
91
  */
92
92
  setParticipant(id: string, keys: ParticipantKeyHandler) {
93
- this.participantId = id;
93
+ this.participantIdentity = id;
94
94
  this.keys = keys;
95
+ this.sifGuard.reset();
95
96
  }
96
97
 
97
98
  unsetParticipant() {
98
- this.participantId = undefined;
99
+ this.participantIdentity = undefined;
100
+ }
101
+
102
+ isEnabled() {
103
+ if (this.participantIdentity) {
104
+ return encryptionEnabledMap.get(this.participantIdentity);
105
+ } else {
106
+ return undefined;
107
+ }
99
108
  }
100
109
 
101
- getParticipantId() {
102
- return this.participantId;
110
+ getParticipantIdentity() {
111
+ return this.participantIdentity;
103
112
  }
104
113
 
105
114
  getTrackId() {
@@ -130,9 +139,10 @@ export class FrameCryptor extends BaseFrameCryptor {
130
139
  codec?: VideoCodec,
131
140
  ) {
132
141
  if (codec) {
133
- console.info('setting codec on cryptor to', codec);
142
+ workerLogger.info('setting codec on cryptor to', { codec });
134
143
  this.videoCodec = codec;
135
144
  }
145
+
136
146
  const transformFn = operation === 'encode' ? this.encodeFunction : this.decodeFunction;
137
147
  const transformStream = new TransformStream({
138
148
  transform: transformFn.bind(this),
@@ -142,12 +152,16 @@ export class FrameCryptor extends BaseFrameCryptor {
142
152
  .pipeThrough(transformStream)
143
153
  .pipeTo(writable)
144
154
  .catch((e) => {
145
- console.error(e);
146
- this.emit('cryptorError', e instanceof CryptorError ? e : new CryptorError(e.message));
155
+ workerLogger.warn(e);
156
+ this.emit(CryptorEvent.Error, e instanceof CryptorError ? e : new CryptorError(e.message));
147
157
  });
148
158
  this.trackId = trackId;
149
159
  }
150
160
 
161
+ setSifTrailer(trailer: Uint8Array) {
162
+ this.sifTrailer = trailer;
163
+ }
164
+
151
165
  /**
152
166
  * Function that will be injected in a stream and will encrypt the given encoded frames.
153
167
  *
@@ -170,19 +184,26 @@ export class FrameCryptor extends BaseFrameCryptor {
170
184
  * 8) Append a single byte for the key identifier.
171
185
  * 9) Enqueue the encrypted frame for sending.
172
186
  */
173
- async encodeFunction(
187
+ protected async encodeFunction(
174
188
  encodedFrame: RTCEncodedVideoFrame | RTCEncodedAudioFrame,
175
189
  controller: TransformStreamDefaultController,
176
190
  ) {
177
191
  if (
178
- !this.keys.isEnabled() ||
192
+ !this.isEnabled() ||
179
193
  // skip for encryption for empty dtx frames
180
194
  encodedFrame.data.byteLength === 0
181
195
  ) {
182
196
  return controller.enqueue(encodedFrame);
183
197
  }
184
-
185
- const { encryptionKey } = this.keys.getKeySet();
198
+ const keySet = this.keys.getKeySet();
199
+ if (!keySet) {
200
+ throw new TypeError(
201
+ `key set not found for ${
202
+ this.participantIdentity
203
+ } at index ${this.keys.getCurrentKeyIndex()}`,
204
+ );
205
+ }
206
+ const { encryptionKey } = keySet;
186
207
  const keyIndex = this.keys.getCurrentKeyIndex();
187
208
 
188
209
  if (encryptionKey) {
@@ -253,19 +274,31 @@ export class FrameCryptor extends BaseFrameCryptor {
253
274
  * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
254
275
  * @param {TransformStreamDefaultController} controller - TransportStreamController.
255
276
  */
256
- async decodeFunction(
277
+ protected async decodeFunction(
257
278
  encodedFrame: RTCEncodedVideoFrame | RTCEncodedAudioFrame,
258
279
  controller: TransformStreamDefaultController,
259
280
  ) {
260
281
  if (
261
- !this.keys.isEnabled() ||
282
+ !this.isEnabled() ||
262
283
  // skip for decryption for empty dtx frames
263
- encodedFrame.data.byteLength === 0 ||
264
- // skip decryption if frame is server injected
265
- isFrameServerInjected(encodedFrame.data, this.unencryptedFrameByteTrailer)
284
+ encodedFrame.data.byteLength === 0
266
285
  ) {
286
+ this.sifGuard.recordUserFrame();
267
287
  return controller.enqueue(encodedFrame);
268
288
  }
289
+
290
+ if (isFrameServerInjected(encodedFrame.data, this.sifTrailer)) {
291
+ this.sifGuard.recordSif();
292
+
293
+ if (this.sifGuard.isSifAllowed()) {
294
+ return controller.enqueue(encodedFrame);
295
+ } else {
296
+ workerLogger.warn('SIF limit reached, dropping frame');
297
+ return;
298
+ }
299
+ } else {
300
+ this.sifGuard.recordUserFrame();
301
+ }
269
302
  const data = new Uint8Array(encodedFrame.data);
270
303
  const keyIndex = data[encodedFrame.data.byteLength - 1];
271
304
 
@@ -279,11 +312,10 @@ export class FrameCryptor extends BaseFrameCryptor {
279
312
  } catch (error) {
280
313
  if (error instanceof CryptorError && error.reason === CryptorErrorReason.InvalidKey) {
281
314
  if (this.keys.hasValidKey) {
282
- workerLogger.warn('invalid key');
283
315
  this.emit(
284
316
  CryptorEvent.Error,
285
317
  new CryptorError(
286
- `invalid key for participant ${this.participantId}`,
318
+ `invalid key for participant ${this.participantIdentity}`,
287
319
  CryptorErrorReason.InvalidKey,
288
320
  ),
289
321
  );
@@ -293,22 +325,33 @@ export class FrameCryptor extends BaseFrameCryptor {
293
325
  workerLogger.warn('decoding frame failed', { error });
294
326
  }
295
327
  }
328
+ } else if (!this.keys.getKeySet(keyIndex) && this.keys.hasValidKey) {
329
+ // emit an error in case the key index is out of bounds but the key handler thinks we still have a valid key
330
+ workerLogger.warn('skipping decryption due to missing key at index');
331
+ this.emit(
332
+ CryptorEvent.Error,
333
+ new CryptorError(
334
+ `missing key at index for participant ${this.participantIdentity}`,
335
+ CryptorErrorReason.MissingKey,
336
+ ),
337
+ );
296
338
  }
297
-
298
- return controller.enqueue(encodedFrame);
299
339
  }
300
340
 
301
341
  /**
302
342
  * Function that will decrypt the given encoded frame. If the decryption fails, it will
303
343
  * ratchet the key for up to RATCHET_WINDOW_SIZE times.
304
344
  */
305
- async decryptFrame(
345
+ private async decryptFrame(
306
346
  encodedFrame: RTCEncodedVideoFrame | RTCEncodedAudioFrame,
307
347
  keyIndex: number,
308
348
  initialMaterial: KeySet | undefined = undefined,
309
349
  ratchetOpts: DecodeRatchetOptions = { ratchetCount: 0 },
310
350
  ): Promise<RTCEncodedVideoFrame | RTCEncodedAudioFrame | undefined> {
311
351
  const keySet = this.keys.getKeySet(keyIndex);
352
+ if (!ratchetOpts.encryptionKey && !keySet) {
353
+ throw new TypeError(`no encryption key found for decryption of ${this.participantIdentity}`);
354
+ }
312
355
 
313
356
  // Construct frame trailer. Similar to the frame header described in
314
357
  // https://tools.ietf.org/html/draft-omara-sframe-00#section-4.2
@@ -344,7 +387,7 @@ export class FrameCryptor extends BaseFrameCryptor {
344
387
  iv,
345
388
  additionalData: new Uint8Array(encodedFrame.data, 0, frameHeader.byteLength),
346
389
  },
347
- ratchetOpts.encryptionKey ?? keySet.encryptionKey,
390
+ ratchetOpts.encryptionKey ?? keySet!.encryptionKey,
348
391
  new Uint8Array(encodedFrame.data, cipherTextStart, cipherTextLength),
349
392
  );
350
393
 
@@ -397,13 +440,10 @@ export class FrameCryptor extends BaseFrameCryptor {
397
440
  this.keys.setKeyFromMaterial(initialMaterial.material, keyIndex);
398
441
  }
399
442
 
400
- workerLogger.warn('maximum ratchet attempts exceeded, resetting key');
401
- this.emit(
402
- CryptorEvent.Error,
403
- new CryptorError(
404
- `valid key missing for participant ${this.participantId}`,
405
- CryptorErrorReason.MissingKey,
406
- ),
443
+ workerLogger.warn('maximum ratchet attempts exceeded');
444
+ throw new CryptorError(
445
+ `valid key missing for participant ${this.participantIdentity}`,
446
+ CryptorErrorReason.InvalidKey,
407
447
  );
408
448
  }
409
449
  } else {
@@ -455,7 +495,7 @@ export class FrameCryptor extends BaseFrameCryptor {
455
495
  return iv;
456
496
  }
457
497
 
458
- getUnencryptedBytes(frame: RTCEncodedVideoFrame | RTCEncodedAudioFrame): number {
498
+ private getUnencryptedBytes(frame: RTCEncodedVideoFrame | RTCEncodedAudioFrame): number {
459
499
  if (isVideoFrame(frame)) {
460
500
  let detectedCodec = this.getVideoCodec(frame) ?? this.videoCodec;
461
501
 
@@ -504,7 +544,7 @@ export class FrameCryptor extends BaseFrameCryptor {
504
544
  /**
505
545
  * inspects frame payloadtype if available and maps it to the codec specified in rtpMap
506
546
  */
507
- getVideoCodec(frame: RTCEncodedVideoFrame): VideoCodec | undefined {
547
+ private getVideoCodec(frame: RTCEncodedVideoFrame): VideoCodec | undefined {
508
548
  if (this.rtpMap.size === 0) {
509
549
  return undefined;
510
550
  }
@@ -605,6 +645,9 @@ export enum NALUType {
605
645
  * @internal
606
646
  */
607
647
  export function isFrameServerInjected(frameData: ArrayBuffer, trailerBytes: Uint8Array): boolean {
648
+ if (trailerBytes.byteLength === 0) {
649
+ return false;
650
+ }
608
651
  const frameTrailer = new Uint8Array(
609
652
  frameData.slice(frameData.byteLength - trailerBytes.byteLength),
610
653
  );