|
@@ -2,7 +2,6 @@
|
2
|
2
|
/* global BigInt */
|
3
|
3
|
|
4
|
4
|
import { deriveKeys, importKey, ratchet } from './crypto-utils';
|
5
|
|
-import { isArrayEqual } from './utils';
|
6
|
5
|
|
7
|
6
|
// We use a ringbuffer of keys so we can change them and still decode packets that were
|
8
|
7
|
// encrypted with an old key. We use a size of 16 which corresponds to the four bits
|
|
@@ -24,26 +23,12 @@ const UNENCRYPTED_BYTES = {
|
24
|
23
|
delta: 3,
|
25
|
24
|
undefined: 1 // frame.type is not set on audio
|
26
|
25
|
};
|
|
26
|
+const ENCRYPTION_ALGORITHM = 'AES-GCM';
|
27
|
27
|
|
28
|
|
-// Use truncated SHA-256 hashes, 80 bіts for video, 32 bits for audio.
|
29
|
|
-// This follows the same principles as DTLS-SRTP.
|
30
|
|
-const AUTHENTICATIONTAG_OPTIONS = {
|
31
|
|
- name: 'HMAC',
|
32
|
|
- hash: 'SHA-256'
|
33
|
|
-};
|
34
|
|
-const ENCRYPTION_ALGORITHM = 'AES-CTR';
|
35
|
|
-
|
36
|
|
-// https://developer.mozilla.org/en-US/docs/Web/API/AesCtrParams
|
37
|
|
-const CTR_LENGTH = 64;
|
38
|
|
-
|
39
|
|
-const DIGEST_LENGTH = {
|
40
|
|
- key: 10,
|
41
|
|
- delta: 10,
|
42
|
|
- undefined: 4 // frame.type is not set on audio
|
43
|
|
-};
|
|
28
|
+/* We use a 96 bit IV for AES GCM. This is signalled in plain together with the
|
|
29
|
+ packet. See https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams */
|
|
30
|
+const IV_LENGTH = 12;
|
44
|
31
|
|
45
|
|
-// Maximum number of forward ratchets to attempt when the authentication
|
46
|
|
-// tag on a remote packet does not match the current key.
|
47
|
32
|
const RATCHET_WINDOW_SIZE = 8;
|
48
|
33
|
|
49
|
34
|
/**
|
|
@@ -61,10 +46,7 @@ export class Context {
|
61
|
46
|
// A pointer to the currently used key.
|
62
|
47
|
this._currentKeyIndex = -1;
|
63
|
48
|
|
64
|
|
- // A per-sender counter that is used create the AES CTR.
|
65
|
|
- // Must be incremented on every frame that is sent, can be reset on
|
66
|
|
- // key changes.
|
67
|
|
- this._sendCount = BigInt(0); // eslint-disable-line new-cap
|
|
49
|
+ this._sendCounts = new Map();
|
68
|
50
|
|
69
|
51
|
this._id = id;
|
70
|
52
|
}
|
|
@@ -111,96 +93,68 @@ export class Context {
|
111
|
93
|
* @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
|
112
|
94
|
* @param {TransformStreamDefaultController} controller - TransportStreamController.
|
113
|
95
|
*
|
114
|
|
- * The packet format is a variant of
|
115
|
|
- * https://tools.ietf.org/html/draft-omara-sframe-00
|
116
|
|
- * using a trailer instead of a header. One of the design goals was to not require
|
117
|
|
- * changes to the SFU which for video requires not encrypting the keyframe bit of VP8
|
118
|
|
- * as SFUs need to detect a keyframe (framemarking or the generic frame descriptor will
|
119
|
|
- * solve this eventually). This also "hides" that a client is using E2EE a bit.
|
120
|
|
- *
|
121
|
|
- * Note that this operates on the full frame, i.e. for VP8 the data described in
|
122
|
|
- * https://tools.ietf.org/html/rfc6386#section-9.1
|
123
|
|
- *
|
124
|
96
|
* The VP8 payload descriptor described in
|
125
|
|
- * https://tools.ietf.org/html/rfc7741#section-4.2
|
126
|
|
- * is part of the RTP packet and not part of the encoded frame and is therefore not
|
127
|
|
- * controllable by us. This is fine as the SFU keeps having access to it for routing.
|
|
97
|
+ * https://tools.ietf.org/html/rfc7741#section-4.2
|
|
98
|
+ * is part of the RTP packet and not part of the frame and is not controllable by us.
|
|
99
|
+ * This is fine as the SFU keeps having access to it for routing.
|
|
100
|
+ *
|
|
101
|
+ * The encrypted frame is formed as follows:
|
|
102
|
+ * 1) Leave the first (10, 3, 1) bytes unencrypted, depending on the frame type and kind.
|
|
103
|
+ * 2) Form the GCM IV for the frame as described above.
|
|
104
|
+ * 3) Encrypt the rest of the frame using AES-GCM.
|
|
105
|
+ * 4) Allocate space for the encrypted frame.
|
|
106
|
+ * 5) Copy the unencrypted bytes to the start of the encrypted frame.
|
|
107
|
+ * 6) Append the ciphertext to the encrypted frame.
|
|
108
|
+ * 7) Append the IV.
|
|
109
|
+ * 8) Append a single byte for the key identifier.
|
|
110
|
+ * 9) Enqueue the encrypted frame for sending.
|
128
|
111
|
*/
|
129
|
112
|
encodeFunction(encodedFrame, controller) {
|
130
|
113
|
const keyIndex = this._currentKeyIndex;
|
131
|
114
|
|
132
|
115
|
if (this._cryptoKeyRing[keyIndex]) {
|
133
|
|
- this._sendCount++;
|
|
116
|
+ const iv = this._makeIV(encodedFrame.getMetadata().synchronizationSource, encodedFrame.timestamp);
|
134
|
117
|
|
135
|
118
|
// Thіs is not encrypted and contains the VP8 payload descriptor or the Opus TOC byte.
|
136
|
119
|
const frameHeader = new Uint8Array(encodedFrame.data, 0, UNENCRYPTED_BYTES[encodedFrame.type]);
|
137
|
120
|
|
|
121
|
+ // Frame trailer contains the R|IV_LENGTH and key index
|
|
122
|
+ const frameTrailer = new Uint8Array(2);
|
|
123
|
+
|
|
124
|
+ frameTrailer[0] = IV_LENGTH;
|
|
125
|
+ frameTrailer[1] = keyIndex;
|
|
126
|
+
|
138
|
127
|
// Construct frame trailer. Similar to the frame header described in
|
139
|
128
|
// https://tools.ietf.org/html/draft-omara-sframe-00#section-4.2
|
140
|
129
|
// but we put it at the end.
|
141
|
|
- // 0 1 2 3 4 5 6 7
|
142
|
|
- // ---------+---------------------------------+-+-+-+-+-+-+-+-+
|
143
|
|
- // payload | CTR... (length=LEN) |S|LEN |KID |
|
144
|
|
- // ---------+---------------------------------+-+-+-+-+-+-+-+-+
|
145
|
|
- const counter = new Uint8Array(16);
|
146
|
|
- const counterView = new DataView(counter.buffer);
|
147
|
|
-
|
148
|
|
- // The counter is encoded as a variable-length field.
|
149
|
|
- counterView.setBigUint64(8, this._sendCount);
|
150
|
|
- let counterLength = 8;
|
151
|
|
-
|
152
|
|
- for (let i = 8; i < counter.byteLength; i++ && counterLength--) {
|
153
|
|
- if (counterView.getUint8(i) !== 0) {
|
154
|
|
- break;
|
155
|
|
- }
|
156
|
|
- }
|
157
|
|
-
|
158
|
|
- const frameTrailer = new Uint8Array(counterLength + 1);
|
159
|
|
-
|
160
|
|
- frameTrailer.set(new Uint8Array(counter.buffer, counter.byteLength - counterLength));
|
161
|
|
-
|
162
|
|
- // Since we never send a counter of 0 we send counterLength - 1 on the wire.
|
163
|
|
- // This is different from the sframe draft, increases the key space and lets us
|
164
|
|
- // ignore the case of a zero-length counter at the receiver.
|
165
|
|
- frameTrailer[frameTrailer.byteLength - 1] = keyIndex | ((counterLength - 1) << 4);
|
166
|
|
-
|
167
|
|
- // XOR the counter with the saltKey to construct the AES CTR.
|
168
|
|
- const saltKey = new DataView(this._cryptoKeyRing[keyIndex].saltKey);
|
169
|
|
-
|
170
|
|
- for (let i = 0; i < counter.byteLength; i++) {
|
171
|
|
- counterView.setUint8(i, counterView.getUint8(i) ^ saltKey.getUint8(i));
|
172
|
|
- }
|
|
130
|
+ //
|
|
131
|
+ // ---------+-------------------------+-+---------+----
|
|
132
|
+ // payload |IV...(length = IV_LENGTH)|R|IV_LENGTH|KID |
|
|
133
|
+ // ---------+-------------------------+-+---------+----
|
173
|
134
|
|
174
|
135
|
return crypto.subtle.encrypt({
|
175
|
136
|
name: ENCRYPTION_ALGORITHM,
|
176
|
|
- counter,
|
177
|
|
- length: CTR_LENGTH
|
|
137
|
+ iv,
|
|
138
|
+ additionalData: new Uint8Array(encodedFrame.data, 0, frameHeader.byteLength)
|
178
|
139
|
}, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
|
179
|
140
|
UNENCRYPTED_BYTES[encodedFrame.type]))
|
180
|
141
|
.then(cipherText => {
|
181
|
142
|
const newData = new ArrayBuffer(frameHeader.byteLength + cipherText.byteLength
|
182
|
|
- + DIGEST_LENGTH[encodedFrame.type] + frameTrailer.byteLength);
|
|
143
|
+ + iv.byteLength + frameTrailer.byteLength);
|
183
|
144
|
const newUint8 = new Uint8Array(newData);
|
184
|
145
|
|
185
|
146
|
newUint8.set(frameHeader); // copy first bytes.
|
186
|
|
- newUint8.set(new Uint8Array(cipherText), UNENCRYPTED_BYTES[encodedFrame.type]); // add ciphertext.
|
187
|
|
- // Leave some space for the authentication tag. This is filled with 0s initially, similar to
|
188
|
|
- // STUN message-integrity described in https://tools.ietf.org/html/rfc5389#section-15.4
|
189
|
|
- newUint8.set(frameTrailer, frameHeader.byteLength + cipherText.byteLength
|
190
|
|
- + DIGEST_LENGTH[encodedFrame.type]); // append trailer.
|
191
|
|
-
|
192
|
|
- return crypto.subtle.sign(AUTHENTICATIONTAG_OPTIONS, this._cryptoKeyRing[keyIndex].authenticationKey,
|
193
|
|
- new Uint8Array(newData)).then(async authTag => {
|
194
|
|
- const truncatedAuthTag = new Uint8Array(authTag, 0, DIGEST_LENGTH[encodedFrame.type]);
|
195
|
|
-
|
|
147
|
+ newUint8.set(
|
|
148
|
+ new Uint8Array(cipherText), frameHeader.byteLength); // add ciphertext.
|
|
149
|
+ newUint8.set(
|
|
150
|
+ new Uint8Array(iv), frameHeader.byteLength + cipherText.byteLength); // append IV.
|
|
151
|
+ newUint8.set(
|
|
152
|
+ frameTrailer,
|
|
153
|
+ frameHeader.byteLength + cipherText.byteLength + iv.byteLength); // append frame trailer.
|
196
|
154
|
|
197
|
|
- // Set the truncated authentication tag.
|
198
|
|
- newUint8.set(truncatedAuthTag, UNENCRYPTED_BYTES[encodedFrame.type] + cipherText.byteLength);
|
199
|
|
-
|
200
|
|
- encodedFrame.data = newData;
|
|
155
|
+ encodedFrame.data = newData;
|
201
|
156
|
|
202
|
|
- return controller.enqueue(encodedFrame);
|
203
|
|
- });
|
|
157
|
+ return controller.enqueue(encodedFrame);
|
204
|
158
|
}, e => {
|
205
|
159
|
// TODO: surface this to the app.
|
206
|
160
|
console.error(e);
|
|
@@ -224,109 +178,147 @@ export class Context {
|
224
|
178
|
*/
|
225
|
179
|
async decodeFunction(encodedFrame, controller) {
|
226
|
180
|
const data = new Uint8Array(encodedFrame.data);
|
227
|
|
- const keyIndex = data[encodedFrame.data.byteLength - 1] & 0xf; // lower four bits.
|
|
181
|
+ const keyIndex = data[encodedFrame.data.byteLength - 1];
|
|
182
|
+
|
|
183
|
+ if (this._cryptoKeyRing[keyIndex]) {
|
|
184
|
+
|
|
185
|
+ const decodedFrame = await this._decryptFrame(
|
|
186
|
+ encodedFrame,
|
|
187
|
+ keyIndex);
|
|
188
|
+
|
|
189
|
+ return controller.enqueue(decodedFrame);
|
|
190
|
+ }
|
|
191
|
+
|
|
192
|
+ // TODO: this just passes through to the decoder. Is that ok? If we don't know the key yet
|
|
193
|
+ // we might want to buffer a bit but it is still unclear how to do that (and for how long etc).
|
|
194
|
+ controller.enqueue(encodedFrame);
|
|
195
|
+ }
|
228
|
196
|
|
229
|
|
- if (this._cryptoKeyRing[this._currentKeyIndex] && this._cryptoKeyRing[keyIndex]) {
|
230
|
|
- const counterLength = 1 + ((data[encodedFrame.data.byteLength - 1] >> 4) & 0x7);
|
|
197
|
+ /**
|
|
198
|
+ * Function that will decrypt the given encoded frame. If the decryption fails, it will
|
|
199
|
+ * ratchet the key for up to RATCHET_WINDOW_SIZE times.
|
|
200
|
+ *
|
|
201
|
+ * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
|
|
202
|
+ * @param {number} keyIndex - the index of the decryption data in _cryptoKeyRing array.
|
|
203
|
+ * @param {number} ratchetCount - the number of retries after ratcheting the key.
|
|
204
|
+ * @returns {RTCEncodedVideoFrame|RTCEncodedAudioFrame} - The decrypted frame.
|
|
205
|
+ * @private
|
|
206
|
+ */
|
|
207
|
+ async _decryptFrame(
|
|
208
|
+ encodedFrame,
|
|
209
|
+ keyIndex,
|
|
210
|
+ ratchetCount = 0) {
|
|
211
|
+
|
|
212
|
+ const { encryptionKey } = this._cryptoKeyRing[keyIndex];
|
|
213
|
+ let { material } = this._cryptoKeyRing[keyIndex];
|
|
214
|
+
|
|
215
|
+ // Construct frame trailer. Similar to the frame header described in
|
|
216
|
+ // https://tools.ietf.org/html/draft-omara-sframe-00#section-4.2
|
|
217
|
+ // but we put it at the end.
|
|
218
|
+ //
|
|
219
|
+ // ---------+-------------------------+-+---------+----
|
|
220
|
+ // payload |IV...(length = IV_LENGTH)|R|IV_LENGTH|KID |
|
|
221
|
+ // ---------+-------------------------+-+---------+----
|
|
222
|
+
|
|
223
|
+ try {
|
231
|
224
|
const frameHeader = new Uint8Array(encodedFrame.data, 0, UNENCRYPTED_BYTES[encodedFrame.type]);
|
|
225
|
+ const frameTrailer = new Uint8Array(encodedFrame.data, encodedFrame.data.byteLength - 2, 2);
|
232
|
226
|
|
233
|
|
- // Extract the truncated authentication tag.
|
234
|
|
- const authTagOffset = encodedFrame.data.byteLength - (DIGEST_LENGTH[encodedFrame.type]
|
235
|
|
- + counterLength + 1);
|
236
|
|
- const authTag = encodedFrame.data.slice(authTagOffset, authTagOffset
|
237
|
|
- + DIGEST_LENGTH[encodedFrame.type]);
|
238
|
|
-
|
239
|
|
- // Set authentication tag bytes to 0.
|
240
|
|
- data.set(new Uint8Array(DIGEST_LENGTH[encodedFrame.type]), encodedFrame.data.byteLength
|
241
|
|
- - (DIGEST_LENGTH[encodedFrame.type] + counterLength + 1));
|
242
|
|
-
|
243
|
|
- // Do truncated hash comparison of the authentication tag.
|
244
|
|
- // If the hash does not match we might have to advance the ratchet a limited number
|
245
|
|
- // of times. See (even though the description there is odd)
|
246
|
|
- // https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.5.1
|
247
|
|
- let { authenticationKey, material } = this._cryptoKeyRing[keyIndex];
|
248
|
|
- let validAuthTag = false;
|
249
|
|
- let newKeys = null;
|
250
|
|
-
|
251
|
|
- for (let distance = 0; distance < RATCHET_WINDOW_SIZE; distance++) {
|
252
|
|
- const calculatedTag = await crypto.subtle.sign(AUTHENTICATIONTAG_OPTIONS,
|
253
|
|
- authenticationKey, encodedFrame.data);
|
254
|
|
-
|
255
|
|
- if (isArrayEqual(new Uint8Array(authTag),
|
256
|
|
- new Uint8Array(calculatedTag.slice(0, DIGEST_LENGTH[encodedFrame.type])))) {
|
257
|
|
- validAuthTag = true;
|
258
|
|
- if (distance > 0) {
|
259
|
|
- this._setKeys(newKeys, keyIndex);
|
260
|
|
- }
|
261
|
|
- break;
|
262
|
|
- }
|
263
|
|
-
|
264
|
|
- // Attempt to ratchet and generate the next set of keys.
|
265
|
|
- material = await importKey(await ratchet(material));
|
266
|
|
- newKeys = await deriveKeys(material);
|
267
|
|
- authenticationKey = newKeys.authenticationKey;
|
268
|
|
- }
|
|
227
|
+ const ivLength = frameTrailer[0];
|
|
228
|
+ const iv = new Uint8Array(
|
|
229
|
+ encodedFrame.data,
|
|
230
|
+ encodedFrame.data.byteLength - ivLength - frameTrailer.byteLength,
|
|
231
|
+ ivLength);
|
269
|
232
|
|
270
|
|
- // Check whether we found a valid authentication tag.
|
271
|
|
- if (!validAuthTag) {
|
272
|
|
- // TODO: return an error to the app.
|
|
233
|
+ const cipherTextStart = frameHeader.byteLength;
|
|
234
|
+ const cipherTextLength = encodedFrame.data.byteLength
|
|
235
|
+ - (frameHeader.byteLength + ivLength + frameTrailer.byteLength);
|
273
|
236
|
|
274
|
|
- console.error('Authentication tag mismatch');
|
|
237
|
+ const plainText = await crypto.subtle.decrypt({
|
|
238
|
+ name: 'AES-GCM',
|
|
239
|
+ iv,
|
|
240
|
+ additionalData: new Uint8Array(encodedFrame.data, 0, frameHeader.byteLength)
|
|
241
|
+ },
|
|
242
|
+ encryptionKey,
|
|
243
|
+ new Uint8Array(encodedFrame.data, cipherTextStart, cipherTextLength));
|
275
|
244
|
|
276
|
|
- return;
|
277
|
|
- }
|
|
245
|
+ const newData = new ArrayBuffer(frameHeader.byteLength + plainText.byteLength);
|
|
246
|
+ const newUint8 = new Uint8Array(newData);
|
|
247
|
+
|
|
248
|
+ newUint8.set(new Uint8Array(encodedFrame.data, 0, frameHeader.byteLength));
|
|
249
|
+ newUint8.set(new Uint8Array(plainText), frameHeader.byteLength);
|
278
|
250
|
|
279
|
|
- // Extract the counter.
|
280
|
|
- const counter = new Uint8Array(16);
|
|
251
|
+ encodedFrame.data = newData;
|
|
252
|
+ } catch (error) {
|
|
253
|
+ console.error(error);
|
|
254
|
+
|
|
255
|
+ if (ratchetCount < RATCHET_WINDOW_SIZE) {
|
|
256
|
+ material = await importKey(await ratchet(material));
|
281
|
257
|
|
282
|
|
- counter.set(data.slice(encodedFrame.data.byteLength - (counterLength + 1),
|
283
|
|
- encodedFrame.data.byteLength - 1), 16 - counterLength);
|
284
|
|
- const counterView = new DataView(counter.buffer);
|
|
258
|
+ const newKey = await deriveKeys(material);
|
285
|
259
|
|
286
|
|
- // XOR the counter with the saltKey to construct the AES CTR.
|
287
|
|
- const saltKey = new DataView(this._cryptoKeyRing[keyIndex].saltKey);
|
|
260
|
+ this._setKeys(newKey);
|
288
|
261
|
|
289
|
|
- for (let i = 0; i < counter.byteLength; i++) {
|
290
|
|
- counterView.setUint8(i,
|
291
|
|
- counterView.getUint8(i) ^ saltKey.getUint8(i));
|
|
262
|
+ return await this._decryptFrame(
|
|
263
|
+ encodedFrame,
|
|
264
|
+ keyIndex,
|
|
265
|
+ ratchetCount + 1);
|
292
|
266
|
}
|
293
|
267
|
|
294
|
|
- return crypto.subtle.decrypt({
|
295
|
|
- name: ENCRYPTION_ALGORITHM,
|
296
|
|
- counter,
|
297
|
|
- length: CTR_LENGTH
|
298
|
|
- }, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
|
299
|
|
- UNENCRYPTED_BYTES[encodedFrame.type],
|
300
|
|
- encodedFrame.data.byteLength - (UNENCRYPTED_BYTES[encodedFrame.type]
|
301
|
|
- + DIGEST_LENGTH[encodedFrame.type] + counterLength + 1))
|
302
|
|
- ).then(plainText => {
|
303
|
|
- const newData = new ArrayBuffer(UNENCRYPTED_BYTES[encodedFrame.type] + plainText.byteLength);
|
|
268
|
+ // TODO: notify the application about error status.
|
|
269
|
+
|
|
270
|
+ // TODO: For video we need a better strategy since we do not want to based any
|
|
271
|
+ // non-error frames on a garbage keyframe.
|
|
272
|
+ if (encodedFrame.type === undefined) { // audio, replace with silence.
|
|
273
|
+ const newData = new ArrayBuffer(3);
|
304
|
274
|
const newUint8 = new Uint8Array(newData);
|
305
|
275
|
|
306
|
|
- newUint8.set(frameHeader);
|
307
|
|
- newUint8.set(new Uint8Array(plainText), UNENCRYPTED_BYTES[encodedFrame.type]);
|
|
276
|
+ newUint8.set([ 0xd8, 0xff, 0xfe ]); // opus silence frame.
|
308
|
277
|
encodedFrame.data = newData;
|
|
278
|
+ }
|
|
279
|
+ }
|
309
|
280
|
|
310
|
|
- return controller.enqueue(encodedFrame);
|
311
|
|
- }, e => {
|
312
|
|
- console.error(e);
|
|
281
|
+ return encodedFrame;
|
|
282
|
+ }
|
313
|
283
|
|
314
|
|
- // TODO: notify the application about error status.
|
315
|
|
- // TODO: For video we need a better strategy since we do not want to based any
|
316
|
|
- // non-error frames on a garbage keyframe.
|
317
|
|
- if (encodedFrame.type === undefined) { // audio, replace with silence.
|
318
|
|
- const newData = new ArrayBuffer(3);
|
319
|
|
- const newUint8 = new Uint8Array(newData);
|
320
|
|
-
|
321
|
|
- newUint8.set([ 0xd8, 0xff, 0xfe ]); // opus silence frame.
|
322
|
|
- encodedFrame.data = newData;
|
323
|
|
- controller.enqueue(encodedFrame);
|
324
|
|
- }
|
325
|
|
- });
|
|
284
|
+
|
|
285
|
+ /**
|
|
286
|
+ * Construct the IV used for AES-GCM and sent (in plain) with the packet similar to
|
|
287
|
+ * https://tools.ietf.org/html/rfc7714#section-8.1
|
|
288
|
+ * It concatenates
|
|
289
|
+ * - the 32 bit synchronization source (SSRC) given on the encoded frame,
|
|
290
|
+ * - the 32 bit rtp timestamp given on the encoded frame,
|
|
291
|
+ * - a send counter that is specific to the SSRC. Starts at a random number.
|
|
292
|
+ * The send counter is essentially the pictureId but we currently have to implement this ourselves.
|
|
293
|
+ * There is no XOR with a salt. Note that this IV leaks the SSRC to the receiver but since this is
|
|
294
|
+ * randomly generated and SFUs may not rewrite this is considered acceptable.
|
|
295
|
+ * The SSRC is used to allow demultiplexing multiple streams with the same key, as described in
|
|
296
|
+ * https://tools.ietf.org/html/rfc3711#section-4.1.1
|
|
297
|
+ * The RTP timestamp is 32 bits and advances by the codec clock rate (90khz for video, 48khz for
|
|
298
|
+ * opus audio) every second. For video it rolls over roughly every 13 hours.
|
|
299
|
+ * The send counter will advance at the frame rate (30fps for video, 50fps for 20ms opus audio)
|
|
300
|
+ * every second. It will take a long time to roll over.
|
|
301
|
+ *
|
|
302
|
+ * See also https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams
|
|
303
|
+ */
|
|
304
|
+ _makeIV(synchronizationSource, timestamp) {
|
|
305
|
+ const iv = new ArrayBuffer(IV_LENGTH);
|
|
306
|
+ const ivView = new DataView(iv);
|
|
307
|
+
|
|
308
|
+ // having to keep our own send count (similar to a picture id) is not ideal.
|
|
309
|
+ if (!this._sendCounts.has(synchronizationSource)) {
|
|
310
|
+ // Initialize with a random offset, similar to the RTP sequence number.
|
|
311
|
+ this._sendCounts.set(synchronizationSource, Math.floor(Math.random() * 0xFFFF));
|
326
|
312
|
}
|
327
|
313
|
|
328
|
|
- // TODO: this just passes through to the decoder. Is that ok? If we don't know the key yet
|
329
|
|
- // we might want to buffer a bit but it is still unclear how to do that (and for how long etc).
|
330
|
|
- controller.enqueue(encodedFrame);
|
|
314
|
+ const sendCount = this._sendCounts.get(synchronizationSource);
|
|
315
|
+
|
|
316
|
+ ivView.setUint32(0, synchronizationSource);
|
|
317
|
+ ivView.setUint32(4, timestamp);
|
|
318
|
+ ivView.setUint32(8, sendCount % 0xFFFF);
|
|
319
|
+
|
|
320
|
+ this._sendCounts.set(synchronizationSource, sendCount + 1);
|
|
321
|
+
|
|
322
|
+ return iv;
|
331
|
323
|
}
|
332
|
324
|
}
|