|
@@ -1,4 +1,5 @@
|
1
|
1
|
/* global TransformStream */
|
|
2
|
+/* eslint-disable no-bitwise */
|
2
|
3
|
|
3
|
4
|
// Worker for E2EE/Insertable streams.
|
4
|
5
|
//
|
|
@@ -21,20 +22,26 @@ function polyFillEncodedFrameMetadata(encodedFrame, controller) {
|
21
|
22
|
controller.enqueue(encodedFrame);
|
22
|
23
|
}
|
23
|
24
|
|
|
25
|
+/**
|
|
26
|
+ * Compares two byteArrays for equality.
|
|
27
|
+ */
|
|
28
|
+function isArrayEqual(a1, a2) {
|
|
29
|
+ if (a1.byteLength !== a2.byteLength) {
|
|
30
|
+ return false;
|
|
31
|
+ }
|
|
32
|
+ for (let i = 0; i < a1.byteLength; i++) {
|
|
33
|
+ if (a1[i] !== a2[i]) {
|
|
34
|
+ return false;
|
|
35
|
+ }
|
|
36
|
+ }
|
|
37
|
+
|
|
38
|
+ return true;
|
|
39
|
+}
|
|
40
|
+
|
24
|
41
|
// We use a ringbuffer of keys so we can change them and still decode packets that were
|
25
|
42
|
// encrypted with an old key.
|
26
|
43
|
const keyRingSize = 3;
|
27
|
44
|
|
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 ivLength = 12;
|
31
|
|
-
|
32
|
|
-// We use a 128 bit key for AES GCM.
|
33
|
|
-const keyGenParameters = {
|
34
|
|
- name: 'AES-GCM',
|
35
|
|
- length: 128
|
36
|
|
-};
|
37
|
|
-
|
38
|
45
|
// We copy the first bytes of the VP8 payload unencrypted.
|
39
|
46
|
// For keyframes this is 10 bytes, for non-keyframes (delta) 3. See
|
40
|
47
|
// https://tools.ietf.org/html/rfc6386#section-9.1
|
|
@@ -51,32 +58,65 @@ const unencryptedBytes = {
|
51
|
58
|
undefined: 1 // frame.type is not set on audio
|
52
|
59
|
};
|
53
|
60
|
|
54
|
|
-// Salt used in key derivation
|
55
|
|
-// FIXME: We currently use the MUC room name for this which has the same lifetime
|
56
|
|
-// as this worker. While not (pseudo)random as recommended in
|
57
|
|
-// https://developer.mozilla.org/en-US/docs/Web/API/Pbkdf2Params
|
58
|
|
-// this is easily available and the same for all participants.
|
59
|
|
-// We currently do not enforce a minimum length of 16 bytes either.
|
60
|
|
-let _keySalt;
|
|
61
|
+// Use truncated SHA-256 hashes, 80 bіts for video, 32 bits for audio.
|
|
62
|
+// This follows the same principles as DTLS-SRTP.
|
|
63
|
+const authenticationTagOptions = {
|
|
64
|
+ name: 'HMAC',
|
|
65
|
+ hash: 'SHA-256'
|
|
66
|
+};
|
|
67
|
+const digestLength = {
|
|
68
|
+ key: 10,
|
|
69
|
+ delta: 10,
|
|
70
|
+ undefined: 4 // frame.type is not set on audio
|
|
71
|
+};
|
61
|
72
|
|
62
|
73
|
/**
|
63
|
|
- * Derives a AES-GCM key from the input using PBKDF2
|
64
|
|
- * The key length can be configured above and should be either 128 or 256 bits.
|
|
74
|
+ * Derives a set of keys from the master key.
|
65
|
75
|
* @param {Uint8Array} keyBytes - Value to derive key from
|
66
|
76
|
* @param {Uint8Array} salt - Salt used in key derivation
|
|
77
|
+ *
|
|
78
|
+ * See https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.1
|
67
|
79
|
*/
|
68
|
|
-async function deriveKey(keyBytes, salt) {
|
|
80
|
+async function deriveKeys(keyBytes) {
|
69
|
81
|
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
|
70
|
82
|
const material = await crypto.subtle.importKey('raw', keyBytes,
|
71
|
|
- 'PBKDF2', false, [ 'deriveBits', 'deriveKey' ]);
|
72
|
|
-
|
73
|
|
- // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#PBKDF2
|
74
|
|
- return crypto.subtle.deriveKey({
|
75
|
|
- name: 'PBKDF2',
|
76
|
|
- salt,
|
77
|
|
- iterations: 100000,
|
|
83
|
+ 'HKDF', false, [ 'deriveBits', 'deriveKey' ]);
|
|
84
|
+
|
|
85
|
+ const info = new ArrayBuffer();
|
|
86
|
+ const textEncoder = new TextEncoder();
|
|
87
|
+
|
|
88
|
+ // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#HKDF
|
|
89
|
+ // https://developer.mozilla.org/en-US/docs/Web/API/HkdfParams
|
|
90
|
+ const encryptionKey = await crypto.subtle.deriveKey({
|
|
91
|
+ name: 'HKDF',
|
|
92
|
+ salt: textEncoder.encode('JFrameEncryptionKey'),
|
|
93
|
+ hash: 'SHA-256',
|
|
94
|
+ info
|
|
95
|
+ }, material, {
|
|
96
|
+ name: 'AES-CTR',
|
|
97
|
+ length: 128
|
|
98
|
+ }, false, [ 'encrypt', 'decrypt' ]);
|
|
99
|
+ const authenticationKey = await crypto.subtle.deriveKey({
|
|
100
|
+ name: 'HKDF',
|
|
101
|
+ salt: textEncoder.encode('JFrameAuthenticationKey'),
|
|
102
|
+ hash: 'SHA-256',
|
|
103
|
+ info
|
|
104
|
+ }, material, {
|
|
105
|
+ name: 'HMAC',
|
78
|
106
|
hash: 'SHA-256'
|
79
|
|
- }, material, keyGenParameters, false, [ 'encrypt', 'decrypt' ]);
|
|
107
|
+ }, false, [ 'sign' ]);
|
|
108
|
+ const saltKey = await crypto.subtle.deriveBits({
|
|
109
|
+ name: 'HKDF',
|
|
110
|
+ salt: textEncoder.encode('JFrameSaltKey'),
|
|
111
|
+ hash: 'SHA-256',
|
|
112
|
+ info
|
|
113
|
+ }, material, 128);
|
|
114
|
+
|
|
115
|
+ return {
|
|
116
|
+ encryptionKey,
|
|
117
|
+ authenticationKey,
|
|
118
|
+ saltKey
|
|
119
|
+ };
|
80
|
120
|
}
|
81
|
121
|
|
82
|
122
|
|
|
@@ -95,78 +135,28 @@ class Context {
|
95
|
135
|
// A pointer to the currently used key.
|
96
|
136
|
this._currentKeyIndex = -1;
|
97
|
137
|
|
98
|
|
- // We keep track of how many frames we have sent per ssrc.
|
99
|
|
- // Starts with a random offset similar to the RTP sequence number.
|
100
|
|
- this._sendCounts = new Map();
|
|
138
|
+ // A per-sender counter that is used create the AES CTR.
|
|
139
|
+ // Must be incremented on every frame that is sent, can be reset on
|
|
140
|
+ // key changes.
|
|
141
|
+ this._sendCount = 0n;
|
101
|
142
|
|
102
|
143
|
this._id = id;
|
103
|
144
|
}
|
104
|
145
|
|
105
|
146
|
/**
|
106
|
|
- * Derives a per-participant key.
|
107
|
|
- * @param {Uint8Array} keyBytes - Value to derive key from
|
108
|
|
- * @param {Uint8Array} salt - Salt used in key derivation
|
109
|
|
- */
|
110
|
|
- async deriveKey(keyBytes, salt) {
|
111
|
|
- const encoder = new TextEncoder();
|
112
|
|
- const idBytes = encoder.encode(this._id);
|
113
|
|
-
|
114
|
|
- // Separate both parts by a null byte to avoid ambiguity attacks.
|
115
|
|
- const participantSalt = new Uint8Array(salt.byteLength + idBytes.byteLength + 1);
|
116
|
|
-
|
117
|
|
- participantSalt.set(salt);
|
118
|
|
- participantSalt.set(idBytes, salt.byteLength + 1);
|
119
|
|
-
|
120
|
|
- return deriveKey(keyBytes, participantSalt);
|
121
|
|
- }
|
122
|
|
-
|
123
|
|
- /**
|
124
|
|
- * Sets a key and starts using it for encrypting.
|
|
147
|
+ * Sets a key, derives the different subkeys and starts using them for encryption or
|
|
148
|
+ * decryption.
|
125
|
149
|
* @param {CryptoKey} key
|
126
|
150
|
* @param {Number} keyIndex
|
127
|
151
|
*/
|
128
|
|
- setKey(key, keyIndex) {
|
|
152
|
+ async setKey(key, keyIndex) {
|
129
|
153
|
this._currentKeyIndex = keyIndex % this._cryptoKeyRing.length;
|
130
|
|
- this._cryptoKeyRing[this._currentKeyIndex] = key;
|
131
|
|
- }
|
132
|
|
-
|
133
|
|
- /**
|
134
|
|
- * Construct the IV used for AES-GCM and sent (in plain) with the packet similar to
|
135
|
|
- * https://tools.ietf.org/html/rfc7714#section-8.1
|
136
|
|
- * It concatenates
|
137
|
|
- * - the 32 bit synchronization source (SSRC) given on the encoded frame,
|
138
|
|
- * - the 32 bit rtp timestamp given on the encoded frame,
|
139
|
|
- * - a send counter that is specific to the SSRC. Starts at a random number.
|
140
|
|
- * The send counter is essentially the pictureId but we currently have to implement this ourselves.
|
141
|
|
- * There is no XOR with a salt. Note that this IV leaks the SSRC to the receiver but since this is
|
142
|
|
- * randomly generated and SFUs may not rewrite this is considered acceptable.
|
143
|
|
- * The SSRC is used to allow demultiplexing multiple streams with the same key, as described in
|
144
|
|
- * https://tools.ietf.org/html/rfc3711#section-4.1.1
|
145
|
|
- * The RTP timestamp is 32 bits and advances by the codec clock rate (90khz for video, 48khz for
|
146
|
|
- * opus audio) every second. For video it rolls over roughly every 13 hours.
|
147
|
|
- * The send counter will advance at the frame rate (30fps for video, 50fps for 20ms opus audio)
|
148
|
|
- * every second. It will take a long time to roll over.
|
149
|
|
- *
|
150
|
|
- * See also https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams
|
151
|
|
- */
|
152
|
|
- makeIV(synchronizationSource, timestamp) {
|
153
|
|
- const iv = new ArrayBuffer(ivLength);
|
154
|
|
- const ivView = new DataView(iv);
|
155
|
|
-
|
156
|
|
- // having to keep our own send count (similar to a picture id) is not ideal.
|
157
|
|
- if (!this._sendCounts.has(synchronizationSource)) {
|
158
|
|
- // Initialize with a random offset, similar to the RTP sequence number.
|
159
|
|
- this._sendCounts.set(synchronizationSource, Math.floor(Math.random() * 0xFFFF));
|
|
154
|
+ if (key) {
|
|
155
|
+ this._cryptoKeyRing[this._currentKeyIndex] = await deriveKeys(key);
|
|
156
|
+ } else {
|
|
157
|
+ this._cryptoKeyRing[this._currentKeyIndex] = false;
|
160
|
158
|
}
|
161
|
|
- const sendCount = this._sendCounts.get(synchronizationSource);
|
162
|
|
-
|
163
|
|
- ivView.setUint32(0, synchronizationSource);
|
164
|
|
- ivView.setUint32(4, timestamp);
|
165
|
|
- ivView.setUint32(8, sendCount % 0xFFFF);
|
166
|
|
-
|
167
|
|
- this._sendCounts.set(synchronizationSource, sendCount + 1);
|
168
|
|
-
|
169
|
|
- return iv;
|
|
159
|
+ this._sendCount = 0n; // Reset the send count (bigint).
|
170
|
160
|
}
|
171
|
161
|
|
172
|
162
|
/**
|
|
@@ -175,7 +165,9 @@ class Context {
|
175
|
165
|
* @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
|
176
|
166
|
* @param {TransformStreamDefaultController} controller - TransportStreamController.
|
177
|
167
|
*
|
178
|
|
- * The packet format is described below. One of the design goals was to not require
|
|
168
|
+ * The packet format is a variant of
|
|
169
|
+ * https://tools.ietf.org/html/draft-omara-sframe-00
|
|
170
|
+ * using a trailer instead of a header. One of the design goals was to not require
|
179
|
171
|
* changes to the SFU which for video requires not encrypting the keyframe bit of VP8
|
180
|
172
|
* as SFUs need to detect a keyframe (framemarking or the generic frame descriptor will
|
181
|
173
|
* solve this eventually). This also "hides" that a client is using E2EE a bit.
|
|
@@ -185,50 +177,83 @@ class Context {
|
185
|
177
|
*
|
186
|
178
|
* The VP8 payload descriptor described in
|
187
|
179
|
* https://tools.ietf.org/html/rfc7741#section-4.2
|
188
|
|
- * is part of the RTP packet and not part of the frame and is not controllable by us.
|
189
|
|
- * This is fine as the SFU keeps having access to it for routing.
|
190
|
|
- *
|
191
|
|
- * The encrypted frame is formed as follows:
|
192
|
|
- * 1) Leave the first (10, 3, 1) bytes unencrypted, depending on the frame type and kind.
|
193
|
|
- * 2) Form the GCM IV for the frame as described above.
|
194
|
|
- * 3) Encrypt the rest of the frame using AES-GCM.
|
195
|
|
- * 4) Allocate space for the encrypted frame.
|
196
|
|
- * 5) Copy the unencrypted bytes to the start of the encrypted frame.
|
197
|
|
- * 6) Append the ciphertext to the encrypted frame.
|
198
|
|
- * 7) Append the IV.
|
199
|
|
- * 8) Append a single byte for the key identifier. TODO: we don't need all the bits.
|
200
|
|
- * 9) Enqueue the encrypted frame for sending.
|
|
180
|
+ * is part of the RTP packet and not part of the encoded frame and is therefore not
|
|
181
|
+ * controllable by us. This is fine as the SFU keeps having access to it for routing.
|
201
|
182
|
*/
|
202
|
183
|
encodeFunction(encodedFrame, controller) {
|
203
|
184
|
const keyIndex = this._currentKeyIndex;
|
204
|
185
|
|
205
|
186
|
if (this._cryptoKeyRing[keyIndex]) {
|
206
|
|
- const iv = this.makeIV(encodedFrame.getMetadata().synchronizationSource, encodedFrame.timestamp);
|
|
187
|
+ this._sendCount++;
|
|
188
|
+
|
|
189
|
+ // Thіs is not encrypted and contains the VP8 payload descriptor or the Opus TOC byte.
|
|
190
|
+ const frameHeader = new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type]);
|
|
191
|
+
|
|
192
|
+ // Construct frame trailer. Similar to the frame header described in
|
|
193
|
+ // https://tools.ietf.org/html/draft-omara-sframe-00#section-4.2
|
|
194
|
+ // but we put it at the end.
|
|
195
|
+ // 0 1 2 3 4 5 6 7
|
|
196
|
+ // ---------+---------------------------------+-+-+-+-+-+-+-+-+
|
|
197
|
+ // payload | CTR... (length=LEN) |S|LEN |0| KID |
|
|
198
|
+ // ---------+---------------------------------+-+-+-+-+-+-+-+-+
|
|
199
|
+ const counter = new Uint8Array(16);
|
|
200
|
+ const counterView = new DataView(counter.buffer);
|
|
201
|
+
|
|
202
|
+ // The counter is encoded as a variable-length field.
|
|
203
|
+ counterView.setBigUint64(8, this._sendCount);
|
|
204
|
+ let counterLength = 8;
|
|
205
|
+
|
|
206
|
+ for (let i = 8; i < counter.byteLength; i++ && counterLength--) {
|
|
207
|
+ if (counterView.getUint8(i) !== 0) {
|
|
208
|
+ break;
|
|
209
|
+ }
|
|
210
|
+ }
|
|
211
|
+
|
|
212
|
+ const frameTrailer = new Uint8Array(counterLength + 1);
|
|
213
|
+
|
|
214
|
+ frameTrailer.set(new Uint8Array(counter.buffer, counter.byteLength - counterLength));
|
|
215
|
+
|
|
216
|
+ // Since we never send a counter of 0 we send counterLength - 1 on the wire.
|
|
217
|
+ // This is different from the sframe draft, increases the key space and lets us
|
|
218
|
+ // ignore the case of a zero-length counter at the receiver.
|
|
219
|
+ frameTrailer[frameTrailer.byteLength - 1] = keyIndex | ((counterLength - 1) << 4);
|
|
220
|
+
|
|
221
|
+ // XOR the counter with the saltKey to construct the AES CTR.
|
|
222
|
+ const saltKey = new DataView(this._cryptoKeyRing[keyIndex].saltKey);
|
|
223
|
+
|
|
224
|
+ for (let i = 0; i < counter.byteLength; i++) {
|
|
225
|
+ counterView.setUint8(i, counterView.getUint8(i) ^ saltKey.getUint8(i));
|
|
226
|
+ }
|
207
|
227
|
|
208
|
228
|
return crypto.subtle.encrypt({
|
209
|
|
- name: 'AES-GCM',
|
210
|
|
- iv,
|
211
|
|
- additionalData: new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type])
|
212
|
|
- }, this._cryptoKeyRing[keyIndex], new Uint8Array(encodedFrame.data,
|
|
229
|
+ name: 'AES-CTR',
|
|
230
|
+ counter,
|
|
231
|
+ length: 64
|
|
232
|
+ }, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
|
213
|
233
|
unencryptedBytes[encodedFrame.type]))
|
214
|
234
|
.then(cipherText => {
|
215
|
|
- const newData = new ArrayBuffer(unencryptedBytes[encodedFrame.type] + cipherText.byteLength
|
216
|
|
- + iv.byteLength + 1);
|
|
235
|
+ const newData = new ArrayBuffer(frameHeader.byteLength + cipherText.byteLength
|
|
236
|
+ + digestLength[encodedFrame.type] + frameTrailer.byteLength);
|
217
|
237
|
const newUint8 = new Uint8Array(newData);
|
218
|
238
|
|
219
|
|
- newUint8.set(
|
220
|
|
- new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type])); // copy first bytes.
|
221
|
|
- newUint8.set(
|
222
|
|
- new Uint8Array(cipherText), unencryptedBytes[encodedFrame.type]); // add ciphertext.
|
223
|
|
- newUint8.set(
|
224
|
|
- new Uint8Array(iv), unencryptedBytes[encodedFrame.type] + cipherText.byteLength); // append IV.
|
225
|
|
- newUint8[unencryptedBytes[encodedFrame.type] + cipherText.byteLength + ivLength]
|
226
|
|
- = keyIndex; // set key index.
|
227
|
|
-
|
228
|
|
- encodedFrame.data = newData;
|
|
239
|
+ newUint8.set(frameHeader); // copy first bytes.
|
|
240
|
+ newUint8.set(new Uint8Array(cipherText), unencryptedBytes[encodedFrame.type]); // add ciphertext.
|
|
241
|
+ // Leave some space for the authentication tag. This is filled with 0s initially, similar to
|
|
242
|
+ // STUN message-integrity described in https://tools.ietf.org/html/rfc5389#section-15.4
|
|
243
|
+ newUint8.set(frameTrailer, frameHeader.byteLength + cipherText.byteLength
|
|
244
|
+ + digestLength[encodedFrame.type]); // append trailer.
|
|
245
|
+
|
|
246
|
+ return crypto.subtle.sign(authenticationTagOptions, this._cryptoKeyRing[keyIndex].authenticationKey,
|
|
247
|
+ new Uint8Array(newData)).then(authTag => {
|
|
248
|
+ // Set the truncated authentication tag.
|
|
249
|
+ newUint8.set(new Uint8Array(authTag, 0, digestLength[encodedFrame.type]),
|
|
250
|
+ unencryptedBytes[encodedFrame.type] + cipherText.byteLength);
|
|
251
|
+ encodedFrame.data = newData;
|
229
|
252
|
|
230
|
|
- return controller.enqueue(encodedFrame);
|
|
253
|
+ return controller.enqueue(encodedFrame);
|
|
254
|
+ });
|
231
|
255
|
}, e => {
|
|
256
|
+ // TODO: surface this to the app.
|
232
|
257
|
console.error(e);
|
233
|
258
|
|
234
|
259
|
// We are not enqueuing the frame here on purpose.
|
|
@@ -247,61 +272,84 @@ class Context {
|
247
|
272
|
*
|
248
|
273
|
* @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
|
249
|
274
|
* @param {TransformStreamDefaultController} controller - TransportStreamController.
|
250
|
|
- *
|
251
|
|
- * The decrypted frame is formed as follows:
|
252
|
|
- * 1) Extract the key index from the last byte of the encrypted frame.
|
253
|
|
- * If there is no key associated with the key index, the frame is enqueued for decoding
|
254
|
|
- * and these steps terminate.
|
255
|
|
- * 2) Determine the frame type in order to look up the number of unencrypted header bytes.
|
256
|
|
- * 2) Extract the 12-byte IV from its position near the end of the packet.
|
257
|
|
- * Note: the IV is treated as opaque and not reconstructed from the input.
|
258
|
|
- * 3) Decrypt the encrypted frame content after the unencrypted bytes using AES-GCM.
|
259
|
|
- * 4) Allocate space for the decrypted frame.
|
260
|
|
- * 5) Copy the unencrypted bytes from the start of the encrypted frame.
|
261
|
|
- * 6) Append the plaintext to the decrypted frame.
|
262
|
|
- * 7) Enqueue the decrypted frame for decoding.
|
263
|
275
|
*/
|
264
|
276
|
decodeFunction(encodedFrame, controller) {
|
265
|
277
|
const data = new Uint8Array(encodedFrame.data);
|
266
|
|
- const keyIndex = data[encodedFrame.data.byteLength - 1];
|
|
278
|
+ const keyIndex = data[encodedFrame.data.byteLength - 1] & 0x7;
|
267
|
279
|
|
268
|
280
|
if (this._cryptoKeyRing[keyIndex]) {
|
269
|
|
- const iv = new Uint8Array(encodedFrame.data, encodedFrame.data.byteLength - ivLength - 1, ivLength);
|
270
|
|
- const cipherTextStart = unencryptedBytes[encodedFrame.type];
|
271
|
|
- const cipherTextLength = encodedFrame.data.byteLength - (unencryptedBytes[encodedFrame.type]
|
272
|
|
- + ivLength + 1);
|
273
|
|
-
|
274
|
|
- return crypto.subtle.decrypt({
|
275
|
|
- name: 'AES-GCM',
|
276
|
|
- iv,
|
277
|
|
- additionalData: new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type])
|
278
|
|
- }, this._cryptoKeyRing[keyIndex], new Uint8Array(encodedFrame.data, cipherTextStart, cipherTextLength))
|
279
|
|
- .then(plainText => {
|
280
|
|
- const newData = new ArrayBuffer(unencryptedBytes[encodedFrame.type] + plainText.byteLength);
|
281
|
|
- const newUint8 = new Uint8Array(newData);
|
|
281
|
+ const counterLength = 1 + ((data[encodedFrame.data.byteLength - 1] >> 4) & 0x7);
|
|
282
|
+ const frameHeader = new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type]);
|
282
|
283
|
|
283
|
|
- newUint8.set(new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type]));
|
284
|
|
- newUint8.set(new Uint8Array(plainText), unencryptedBytes[encodedFrame.type]);
|
|
284
|
+ // Extract the truncated authentication tag.
|
|
285
|
+ const authTagOffset = encodedFrame.data.byteLength - (digestLength[encodedFrame.type]
|
|
286
|
+ + counterLength + 1);
|
|
287
|
+ const authTag = encodedFrame.data.slice(authTagOffset, authTagOffset
|
|
288
|
+ + digestLength[encodedFrame.type]);
|
285
|
289
|
|
286
|
|
- encodedFrame.data = newData;
|
|
290
|
+ // Set authentication tag bytes to 0.
|
|
291
|
+ const zeros = new Uint8Array(digestLength[encodedFrame.type]);
|
287
|
292
|
|
288
|
|
- return controller.enqueue(encodedFrame);
|
289
|
|
- }, e => {
|
290
|
|
- console.error(e);
|
|
293
|
+ data.set(zeros, encodedFrame.data.byteLength - (digestLength[encodedFrame.type] + counterLength + 1));
|
|
294
|
+
|
|
295
|
+ return crypto.subtle.sign(authenticationTagOptions, this._cryptoKeyRing[keyIndex].authenticationKey,
|
|
296
|
+ encodedFrame.data).then(calculatedTag => {
|
|
297
|
+ // Do truncated hash comparison.
|
|
298
|
+ if (!isArrayEqual(authTag, calculatedTag.slice(0, digestLength[encodedFrame.type]))) {
|
|
299
|
+ // TODO: surface this to the app.
|
|
300
|
+ console.error('Authentication tag mismatch', new Uint8Array(authTag), new Uint8Array(calculatedTag,
|
|
301
|
+ 0, digestLength[encodedFrame.type]));
|
|
302
|
+
|
|
303
|
+ return;
|
|
304
|
+ }
|
291
|
305
|
|
292
|
|
- // TODO: notify the application about error status.
|
|
306
|
+ // Extract the counter.
|
|
307
|
+ const counter = new Uint8Array(16);
|
293
|
308
|
|
294
|
|
- // TODO: For video we need a better strategy since we do not want to based any
|
295
|
|
- // non-error frames on a garbage keyframe.
|
296
|
|
- if (encodedFrame.type === undefined) { // audio, replace with silence.
|
297
|
|
- // audio, replace with silence.
|
298
|
|
- const newData = new ArrayBuffer(3);
|
|
309
|
+ counter.set(data.slice(encodedFrame.data.byteLength - (counterLength + 1),
|
|
310
|
+ encodedFrame.data.byteLength - 1), 16 - counterLength);
|
|
311
|
+ const counterView = new DataView(counter.buffer);
|
|
312
|
+
|
|
313
|
+ // XOR the counter with the saltKey to construct the AES CTR.
|
|
314
|
+ const saltKey = new DataView(this._cryptoKeyRing[keyIndex].saltKey);
|
|
315
|
+
|
|
316
|
+ for (let i = 0; i < counter.byteLength; i++) {
|
|
317
|
+ counterView.setUint8(i,
|
|
318
|
+ counterView.getUint8(i) ^ saltKey.getUint8(i));
|
|
319
|
+ }
|
|
320
|
+
|
|
321
|
+ return crypto.subtle.decrypt({
|
|
322
|
+ name: 'AES-CTR',
|
|
323
|
+ counter,
|
|
324
|
+ length: 64
|
|
325
|
+ }, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
|
|
326
|
+ unencryptedBytes[encodedFrame.type],
|
|
327
|
+ encodedFrame.data.byteLength - (unencryptedBytes[encodedFrame.type]
|
|
328
|
+ + digestLength[encodedFrame.type] + counterLength + 1))
|
|
329
|
+ ).then(plainText => {
|
|
330
|
+ const newData = new ArrayBuffer(unencryptedBytes[encodedFrame.type] + plainText.byteLength);
|
299
|
331
|
const newUint8 = new Uint8Array(newData);
|
300
|
332
|
|
301
|
|
- newUint8.set([ 0xd8, 0xff, 0xfe ]); // opus silence frame.
|
|
333
|
+ newUint8.set(frameHeader);
|
|
334
|
+ newUint8.set(new Uint8Array(plainText), unencryptedBytes[encodedFrame.type]);
|
302
|
335
|
encodedFrame.data = newData;
|
303
|
|
- controller.enqueue(encodedFrame);
|
304
|
|
- }
|
|
336
|
+
|
|
337
|
+ return controller.enqueue(encodedFrame);
|
|
338
|
+ }, e => {
|
|
339
|
+ console.error(e);
|
|
340
|
+
|
|
341
|
+ // TODO: notify the application about error status.
|
|
342
|
+ // TODO: For video we need a better strategy since we do not want to based any
|
|
343
|
+ // non-error frames on a garbage keyframe.
|
|
344
|
+ if (encodedFrame.type === undefined) { // audio, replace with silence.
|
|
345
|
+ const newData = new ArrayBuffer(3);
|
|
346
|
+ const newUint8 = new Uint8Array(newData);
|
|
347
|
+
|
|
348
|
+ newUint8.set([ 0xd8, 0xff, 0xfe ]); // opus silence frame.
|
|
349
|
+ encodedFrame.data = newData;
|
|
350
|
+ controller.enqueue(encodedFrame);
|
|
351
|
+ }
|
|
352
|
+ });
|
305
|
353
|
});
|
306
|
354
|
} else if (keyIndex >= this._cryptoKeyRing.length && this._cryptoKeyRing[this._currentKeyIndex]) {
|
307
|
355
|
// If we are encrypting but don't have a key for the remote drop the frame.
|
|
@@ -322,9 +370,7 @@ const contexts = new Map(); // Map participant id => context
|
322
|
370
|
onmessage = async event => {
|
323
|
371
|
const { operation } = event.data;
|
324
|
372
|
|
325
|
|
- if (operation === 'initialize') {
|
326
|
|
- _keySalt = event.data.salt;
|
327
|
|
- } else if (operation === 'encode') {
|
|
373
|
+ if (operation === 'encode') {
|
328
|
374
|
const { readableStream, writableStream, participantId } = event.data;
|
329
|
375
|
|
330
|
376
|
if (!contexts.has(participantId)) {
|
|
@@ -367,7 +413,7 @@ onmessage = async event => {
|
367
|
413
|
const context = contexts.get(participantId);
|
368
|
414
|
|
369
|
415
|
if (key) {
|
370
|
|
- context.setKey(await context.deriveKey(key, _keySalt), keyIndex);
|
|
416
|
+ context.setKey(key, keyIndex);
|
371
|
417
|
} else {
|
372
|
418
|
context.setKey(false, keyIndex);
|
373
|
419
|
}
|