Переглянути джерело

e2ee: move context to separate file

to allow writing tests
dev1
Philipp Hancke 5 роки тому
джерело
коміт
687a82e5a1
2 змінених файлів з 325 додано та 321 видалено
  1. 323
    0
      modules/e2ee/Context.js
  2. 2
    321
      modules/e2ee/Worker.js

+ 323
- 0
modules/e2ee/Context.js Переглянути файл

@@ -0,0 +1,323 @@
1
+/* eslint-disable no-bitwise */
2
+
3
+import { deriveKeys, importKey, ratchet } from './crypto-utils';
4
+import { isArrayEqual } from './utils';
5
+
6
+// We use a ringbuffer of keys so we can change them and still decode packets that were
7
+// encrypted with an old key.
8
+const keyRingSize = 3;
9
+
10
+// We copy the first bytes of the VP8 payload unencrypted.
11
+// For keyframes this is 10 bytes, for non-keyframes (delta) 3. See
12
+//   https://tools.ietf.org/html/rfc6386#section-9.1
13
+// This allows the bridge to continue detecting keyframes (only one byte needed in the JVB)
14
+// and is also a bit easier for the VP8 decoder (i.e. it generates funny garbage pictures
15
+// instead of being unable to decode).
16
+// This is a bit for show and we might want to reduce to 1 unconditionally in the final version.
17
+//
18
+// For audio (where frame.type is not set) we do not encrypt the opus TOC byte:
19
+//   https://tools.ietf.org/html/rfc6716#section-3.1
20
+const unencryptedBytes = {
21
+    key: 10,
22
+    delta: 3,
23
+    undefined: 1 // frame.type is not set on audio
24
+};
25
+
26
+// Use truncated SHA-256 hashes, 80 bіts for video, 32 bits for audio.
27
+// This follows the same principles as DTLS-SRTP.
28
+const authenticationTagOptions = {
29
+    name: 'HMAC',
30
+    hash: 'SHA-256'
31
+};
32
+const digestLength = {
33
+    key: 10,
34
+    delta: 10,
35
+    undefined: 4 // frame.type is not set on audio
36
+};
37
+
38
+// Maximum number of forward ratchets to attempt when the authentication
39
+// tag on a remote packet does not match the current key.
40
+const ratchetWindow = 8;
41
+
42
+/**
43
+ * Per-participant context holding the cryptographic keys and
44
+ * encode/decode functions
45
+ */
46
+export class Context {
47
+    /**
48
+     * @param {string} id - local muc resourcepart
49
+     */
50
+    constructor(id) {
51
+        // An array (ring) of keys that we use for sending and receiving.
52
+        this._cryptoKeyRing = new Array(keyRingSize);
53
+
54
+        // A pointer to the currently used key.
55
+        this._currentKeyIndex = -1;
56
+
57
+        // A per-sender counter that is used create the AES CTR.
58
+        // Must be incremented on every frame that is sent, can be reset on
59
+        // key changes.
60
+        this._sendCount = 0n;
61
+
62
+        this._id = id;
63
+    }
64
+
65
+    /**
66
+     * Derives the different subkeys and starts using them for encryption or
67
+     * decryption.
68
+     * @param {Uint8Array|false} key bytes. Pass false to disable.
69
+     * @param {Number} keyIndex
70
+     */
71
+    async setKey(keyBytes, keyIndex) {
72
+        let newKey;
73
+
74
+        if (keyBytes) {
75
+            const material = await importKey(keyBytes);
76
+
77
+            newKey = await deriveKeys(material);
78
+        } else {
79
+            newKey = false;
80
+        }
81
+        this._currentKeyIndex = keyIndex % this._cryptoKeyRing.length;
82
+        this._setKeys(newKey);
83
+    }
84
+
85
+    /**
86
+     * Sets a set of keys and resets the sendCount.
87
+     * decryption.
88
+     * @param {Object} keys set of keys.
89
+     * @private
90
+     */
91
+    _setKeys(keys) {
92
+        this._cryptoKeyRing[this._currentKeyIndex] = keys;
93
+        this._sendCount = 0n; // Reset the send count (bigint).
94
+    }
95
+
96
+    /**
97
+     * Function that will be injected in a stream and will encrypt the given encoded frames.
98
+     *
99
+     * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
100
+     * @param {TransformStreamDefaultController} controller - TransportStreamController.
101
+     *
102
+     * The packet format is a variant of
103
+     *   https://tools.ietf.org/html/draft-omara-sframe-00
104
+     * using a trailer instead of a header. One of the design goals was to not require
105
+     * changes to the SFU which for video requires not encrypting the keyframe bit of VP8
106
+     * as SFUs need to detect a keyframe (framemarking or the generic frame descriptor will
107
+     * solve this eventually). This also "hides" that a client is using E2EE a bit.
108
+     *
109
+     * Note that this operates on the full frame, i.e. for VP8 the data described in
110
+     *   https://tools.ietf.org/html/rfc6386#section-9.1
111
+     *
112
+     * The VP8 payload descriptor described in
113
+     *   https://tools.ietf.org/html/rfc7741#section-4.2
114
+     * is part of the RTP packet and not part of the encoded frame and is therefore not
115
+     * controllable by us. This is fine as the SFU keeps having access to it for routing.
116
+     */
117
+    encodeFunction(encodedFrame, controller) {
118
+        const keyIndex = this._currentKeyIndex;
119
+
120
+        if (this._cryptoKeyRing[keyIndex]) {
121
+            this._sendCount++;
122
+
123
+            // Thіs is not encrypted and contains the VP8 payload descriptor or the Opus TOC byte.
124
+            const frameHeader = new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type]);
125
+
126
+            // Construct frame trailer. Similar to the frame header described in
127
+            // https://tools.ietf.org/html/draft-omara-sframe-00#section-4.2
128
+            // but we put it at the end.
129
+            //                                             0 1 2 3 4 5 6 7
130
+            // ---------+---------------------------------+-+-+-+-+-+-+-+-+
131
+            // payload  |    CTR... (length=LEN)          |S|LEN  |0| KID |
132
+            // ---------+---------------------------------+-+-+-+-+-+-+-+-+
133
+            const counter = new Uint8Array(16);
134
+            const counterView = new DataView(counter.buffer);
135
+
136
+            // The counter is encoded as a variable-length field.
137
+            counterView.setBigUint64(8, this._sendCount);
138
+            let counterLength = 8;
139
+
140
+            for (let i = 8; i < counter.byteLength; i++ && counterLength--) {
141
+                if (counterView.getUint8(i) !== 0) {
142
+                    break;
143
+                }
144
+            }
145
+
146
+            const frameTrailer = new Uint8Array(counterLength + 1);
147
+
148
+            frameTrailer.set(new Uint8Array(counter.buffer, counter.byteLength - counterLength));
149
+
150
+            // Since we never send a counter of 0 we send counterLength - 1 on the wire.
151
+            // This is different from the sframe draft, increases the key space and lets us
152
+            // ignore the case of a zero-length counter at the receiver.
153
+            frameTrailer[frameTrailer.byteLength - 1] = keyIndex | ((counterLength - 1) << 4);
154
+
155
+            // XOR the counter with the saltKey to construct the AES CTR.
156
+            const saltKey = new DataView(this._cryptoKeyRing[keyIndex].saltKey);
157
+
158
+            for (let i = 0; i < counter.byteLength; i++) {
159
+                counterView.setUint8(i, counterView.getUint8(i) ^ saltKey.getUint8(i));
160
+            }
161
+
162
+            return crypto.subtle.encrypt({
163
+                name: 'AES-CTR',
164
+                counter,
165
+                length: 64
166
+            }, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
167
+                unencryptedBytes[encodedFrame.type]))
168
+            .then(cipherText => {
169
+                const newData = new ArrayBuffer(frameHeader.byteLength + cipherText.byteLength
170
+                    + digestLength[encodedFrame.type] + frameTrailer.byteLength);
171
+                const newUint8 = new Uint8Array(newData);
172
+
173
+                newUint8.set(frameHeader); // copy first bytes.
174
+                newUint8.set(new Uint8Array(cipherText), unencryptedBytes[encodedFrame.type]); // add ciphertext.
175
+                // Leave some space for the authentication tag. This is filled with 0s initially, similar to
176
+                // STUN message-integrity described in https://tools.ietf.org/html/rfc5389#section-15.4
177
+                newUint8.set(frameTrailer, frameHeader.byteLength + cipherText.byteLength
178
+                    + digestLength[encodedFrame.type]); // append trailer.
179
+
180
+                return crypto.subtle.sign(authenticationTagOptions, this._cryptoKeyRing[keyIndex].authenticationKey,
181
+                    new Uint8Array(newData)).then(authTag => {
182
+                    // Set the truncated authentication tag.
183
+                    newUint8.set(new Uint8Array(authTag, 0, digestLength[encodedFrame.type]),
184
+                        unencryptedBytes[encodedFrame.type] + cipherText.byteLength);
185
+                    encodedFrame.data = newData;
186
+
187
+                    return controller.enqueue(encodedFrame);
188
+                });
189
+            }, e => {
190
+                // TODO: surface this to the app.
191
+                console.error(e);
192
+
193
+                // We are not enqueuing the frame here on purpose.
194
+            });
195
+        }
196
+
197
+        /* NOTE WELL:
198
+         * This will send unencrypted data (only protected by DTLS transport encryption) when no key is configured.
199
+         * This is ok for demo purposes but should not be done once this becomes more relied upon.
200
+         */
201
+        controller.enqueue(encodedFrame);
202
+    }
203
+
204
+    /**
205
+     * Function that will be injected in a stream and will decrypt the given encoded frames.
206
+     *
207
+     * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
208
+     * @param {TransformStreamDefaultController} controller - TransportStreamController.
209
+     */
210
+    async decodeFunction(encodedFrame, controller) {
211
+        const data = new Uint8Array(encodedFrame.data);
212
+        const keyIndex = data[encodedFrame.data.byteLength - 1] & 0x7;
213
+
214
+        if (this._cryptoKeyRing[keyIndex]) {
215
+            const counterLength = 1 + ((data[encodedFrame.data.byteLength - 1] >> 4) & 0x7);
216
+            const frameHeader = new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type]);
217
+
218
+            // Extract the truncated authentication tag.
219
+            const authTagOffset = encodedFrame.data.byteLength - (digestLength[encodedFrame.type]
220
+                + counterLength + 1);
221
+            const authTag = encodedFrame.data.slice(authTagOffset, authTagOffset
222
+                + digestLength[encodedFrame.type]);
223
+
224
+            // Set authentication tag bytes to 0.
225
+            const zeros = new Uint8Array(digestLength[encodedFrame.type]);
226
+
227
+            data.set(zeros, encodedFrame.data.byteLength - (digestLength[encodedFrame.type] + counterLength + 1));
228
+
229
+            // Do truncated hash comparison. If the hash does not match we might have to advance the
230
+            // ratchet a limited number of times. See (even though the description there is odd)
231
+            // https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.5.1
232
+            let { authenticationKey, material } = this._cryptoKeyRing[keyIndex];
233
+            let valid = false;
234
+            let newKeys = null;
235
+
236
+            for (let distance = 0; distance < ratchetWindow; distance++) {
237
+                const calculatedTag = await crypto.subtle.sign(authenticationTagOptions,
238
+                    authenticationKey, encodedFrame.data);
239
+
240
+                if (isArrayEqual(new Uint8Array(authTag),
241
+                        new Uint8Array(calculatedTag.slice(0, digestLength[encodedFrame.type])))) {
242
+                    valid = true;
243
+                    if (distance > 0) {
244
+                        this._setKeys(newKeys);
245
+                    }
246
+                    break;
247
+                }
248
+
249
+                // Attempt to ratchet and generate the next set of keys.
250
+                material = await importKey(await ratchet(material));
251
+                newKeys = await deriveKeys(material);
252
+                authenticationKey = newKeys.authenticationKey;
253
+            }
254
+
255
+            // Check whether we found a valid signature.
256
+            if (!valid) {
257
+                // TODO: return an error to the app.
258
+
259
+                console.error('Authentication tag mismatch');
260
+
261
+                return;
262
+            }
263
+
264
+            // Extract the counter.
265
+            const counter = new Uint8Array(16);
266
+
267
+            counter.set(data.slice(encodedFrame.data.byteLength - (counterLength + 1),
268
+                encodedFrame.data.byteLength - 1), 16 - counterLength);
269
+            const counterView = new DataView(counter.buffer);
270
+
271
+            // XOR the counter with the saltKey to construct the AES CTR.
272
+            const saltKey = new DataView(this._cryptoKeyRing[keyIndex].saltKey);
273
+
274
+            for (let i = 0; i < counter.byteLength; i++) {
275
+                counterView.setUint8(i,
276
+                    counterView.getUint8(i) ^ saltKey.getUint8(i));
277
+            }
278
+
279
+            return crypto.subtle.decrypt({
280
+                name: 'AES-CTR',
281
+                counter,
282
+                length: 64
283
+            }, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
284
+                    unencryptedBytes[encodedFrame.type],
285
+                    encodedFrame.data.byteLength - (unencryptedBytes[encodedFrame.type]
286
+                    + digestLength[encodedFrame.type] + counterLength + 1))
287
+            ).then(plainText => {
288
+                const newData = new ArrayBuffer(unencryptedBytes[encodedFrame.type] + plainText.byteLength);
289
+                const newUint8 = new Uint8Array(newData);
290
+
291
+                newUint8.set(frameHeader);
292
+                newUint8.set(new Uint8Array(plainText), unencryptedBytes[encodedFrame.type]);
293
+                encodedFrame.data = newData;
294
+
295
+                return controller.enqueue(encodedFrame);
296
+            }, e => {
297
+                console.error(e);
298
+
299
+                // TODO: notify the application about error status.
300
+                // TODO: For video we need a better strategy since we do not want to based any
301
+                // non-error frames on a garbage keyframe.
302
+                if (encodedFrame.type === undefined) { // audio, replace with silence.
303
+                    const newData = new ArrayBuffer(3);
304
+                    const newUint8 = new Uint8Array(newData);
305
+
306
+                    newUint8.set([ 0xd8, 0xff, 0xfe ]); // opus silence frame.
307
+                    encodedFrame.data = newData;
308
+                    controller.enqueue(encodedFrame);
309
+                }
310
+            });
311
+        } else if (keyIndex >= this._cryptoKeyRing.length && this._cryptoKeyRing[this._currentKeyIndex]) {
312
+            // If we are encrypting but don't have a key for the remote drop the frame.
313
+            // This is a heuristic since we don't know whether a packet is encrypted,
314
+            // do not have a checksum and do not have signaling for whether a remote participant does
315
+            // encrypt or not.
316
+            return;
317
+        }
318
+
319
+        // TODO: this just passes through to the decoder. Is that ok? If we don't know the key yet
320
+        // we might want to buffer a bit but it is still unclear how to do that (and for how long etc).
321
+        controller.enqueue(encodedFrame);
322
+    }
323
+}

+ 2
- 321
modules/e2ee/Worker.js Переглянути файл

@@ -4,327 +4,8 @@
4 4
 // Worker for E2EE/Insertable streams.
5 5
 //
6 6
 
7
-import { deriveKeys, importKey, ratchet } from './crypto-utils';
8
-import { polyFillEncodedFrameMetadata, isArrayEqual } from './utils';
9
-
10
-// We use a ringbuffer of keys so we can change them and still decode packets that were
11
-// encrypted with an old key.
12
-const keyRingSize = 3;
13
-
14
-// We copy the first bytes of the VP8 payload unencrypted.
15
-// For keyframes this is 10 bytes, for non-keyframes (delta) 3. See
16
-//   https://tools.ietf.org/html/rfc6386#section-9.1
17
-// This allows the bridge to continue detecting keyframes (only one byte needed in the JVB)
18
-// and is also a bit easier for the VP8 decoder (i.e. it generates funny garbage pictures
19
-// instead of being unable to decode).
20
-// This is a bit for show and we might want to reduce to 1 unconditionally in the final version.
21
-//
22
-// For audio (where frame.type is not set) we do not encrypt the opus TOC byte:
23
-//   https://tools.ietf.org/html/rfc6716#section-3.1
24
-const unencryptedBytes = {
25
-    key: 10,
26
-    delta: 3,
27
-    undefined: 1 // frame.type is not set on audio
28
-};
29
-
30
-// Use truncated SHA-256 hashes, 80 bіts for video, 32 bits for audio.
31
-// This follows the same principles as DTLS-SRTP.
32
-const authenticationTagOptions = {
33
-    name: 'HMAC',
34
-    hash: 'SHA-256'
35
-};
36
-const digestLength = {
37
-    key: 10,
38
-    delta: 10,
39
-    undefined: 4 // frame.type is not set on audio
40
-};
41
-
42
-// Maximum number of forward ratchets to attempt when the authentication
43
-// tag on a remote packet does not match the current key.
44
-const ratchetWindow = 8;
45
-
46
-/**
47
- * Per-participant context holding the cryptographic keys and
48
- * encode/decode functions
49
- */
50
-class Context {
51
-    /**
52
-     * @param {string} id - local muc resourcepart
53
-     */
54
-    constructor(id) {
55
-        // An array (ring) of keys that we use for sending and receiving.
56
-        this._cryptoKeyRing = new Array(keyRingSize);
57
-
58
-        // A pointer to the currently used key.
59
-        this._currentKeyIndex = -1;
60
-
61
-        // A per-sender counter that is used create the AES CTR.
62
-        // Must be incremented on every frame that is sent, can be reset on
63
-        // key changes.
64
-        this._sendCount = 0n;
65
-
66
-        this._id = id;
67
-    }
68
-
69
-    /**
70
-     * Derives the different subkeys and starts using them for encryption or
71
-     * decryption.
72
-     * @param {Uint8Array|false} key bytes. Pass false to disable.
73
-     * @param {Number} keyIndex
74
-     */
75
-    async setKey(keyBytes, keyIndex) {
76
-        let newKey;
77
-
78
-        if (keyBytes) {
79
-            const material = await importKey(keyBytes);
80
-
81
-            newKey = await deriveKeys(material);
82
-        } else {
83
-            newKey = false;
84
-        }
85
-        this._currentKeyIndex = keyIndex % this._cryptoKeyRing.length;
86
-        this._setKeys(newKey);
87
-    }
88
-
89
-    /**
90
-     * Sets a set of keys and resets the sendCount.
91
-     * decryption.
92
-     * @param {Object} keys set of keys.
93
-     * @private
94
-     */
95
-    _setKeys(keys) {
96
-        this._cryptoKeyRing[this._currentKeyIndex] = keys;
97
-        this._sendCount = 0n; // Reset the send count (bigint).
98
-    }
99
-
100
-    /**
101
-     * Function that will be injected in a stream and will encrypt the given encoded frames.
102
-     *
103
-     * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
104
-     * @param {TransformStreamDefaultController} controller - TransportStreamController.
105
-     *
106
-     * The packet format is a variant of
107
-     *   https://tools.ietf.org/html/draft-omara-sframe-00
108
-     * using a trailer instead of a header. One of the design goals was to not require
109
-     * changes to the SFU which for video requires not encrypting the keyframe bit of VP8
110
-     * as SFUs need to detect a keyframe (framemarking or the generic frame descriptor will
111
-     * solve this eventually). This also "hides" that a client is using E2EE a bit.
112
-     *
113
-     * Note that this operates on the full frame, i.e. for VP8 the data described in
114
-     *   https://tools.ietf.org/html/rfc6386#section-9.1
115
-     *
116
-     * The VP8 payload descriptor described in
117
-     *   https://tools.ietf.org/html/rfc7741#section-4.2
118
-     * is part of the RTP packet and not part of the encoded frame and is therefore not
119
-     * controllable by us. This is fine as the SFU keeps having access to it for routing.
120
-     */
121
-    encodeFunction(encodedFrame, controller) {
122
-        const keyIndex = this._currentKeyIndex;
123
-
124
-        if (this._cryptoKeyRing[keyIndex]) {
125
-            this._sendCount++;
126
-
127
-            // Thіs is not encrypted and contains the VP8 payload descriptor or the Opus TOC byte.
128
-            const frameHeader = new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type]);
129
-
130
-            // Construct frame trailer. Similar to the frame header described in
131
-            // https://tools.ietf.org/html/draft-omara-sframe-00#section-4.2
132
-            // but we put it at the end.
133
-            //                                             0 1 2 3 4 5 6 7
134
-            // ---------+---------------------------------+-+-+-+-+-+-+-+-+
135
-            // payload  |    CTR... (length=LEN)          |S|LEN  |0| KID |
136
-            // ---------+---------------------------------+-+-+-+-+-+-+-+-+
137
-            const counter = new Uint8Array(16);
138
-            const counterView = new DataView(counter.buffer);
139
-
140
-            // The counter is encoded as a variable-length field.
141
-            counterView.setBigUint64(8, this._sendCount);
142
-            let counterLength = 8;
143
-
144
-            for (let i = 8; i < counter.byteLength; i++ && counterLength--) {
145
-                if (counterView.getUint8(i) !== 0) {
146
-                    break;
147
-                }
148
-            }
149
-
150
-            const frameTrailer = new Uint8Array(counterLength + 1);
151
-
152
-            frameTrailer.set(new Uint8Array(counter.buffer, counter.byteLength - counterLength));
153
-
154
-            // Since we never send a counter of 0 we send counterLength - 1 on the wire.
155
-            // This is different from the sframe draft, increases the key space and lets us
156
-            // ignore the case of a zero-length counter at the receiver.
157
-            frameTrailer[frameTrailer.byteLength - 1] = keyIndex | ((counterLength - 1) << 4);
158
-
159
-            // XOR the counter with the saltKey to construct the AES CTR.
160
-            const saltKey = new DataView(this._cryptoKeyRing[keyIndex].saltKey);
161
-
162
-            for (let i = 0; i < counter.byteLength; i++) {
163
-                counterView.setUint8(i, counterView.getUint8(i) ^ saltKey.getUint8(i));
164
-            }
165
-
166
-            return crypto.subtle.encrypt({
167
-                name: 'AES-CTR',
168
-                counter,
169
-                length: 64
170
-            }, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
171
-                unencryptedBytes[encodedFrame.type]))
172
-            .then(cipherText => {
173
-                const newData = new ArrayBuffer(frameHeader.byteLength + cipherText.byteLength
174
-                    + digestLength[encodedFrame.type] + frameTrailer.byteLength);
175
-                const newUint8 = new Uint8Array(newData);
176
-
177
-                newUint8.set(frameHeader); // copy first bytes.
178
-                newUint8.set(new Uint8Array(cipherText), unencryptedBytes[encodedFrame.type]); // add ciphertext.
179
-                // Leave some space for the authentication tag. This is filled with 0s initially, similar to
180
-                // STUN message-integrity described in https://tools.ietf.org/html/rfc5389#section-15.4
181
-                newUint8.set(frameTrailer, frameHeader.byteLength + cipherText.byteLength
182
-                    + digestLength[encodedFrame.type]); // append trailer.
183
-
184
-                return crypto.subtle.sign(authenticationTagOptions, this._cryptoKeyRing[keyIndex].authenticationKey,
185
-                    new Uint8Array(newData)).then(authTag => {
186
-                    // Set the truncated authentication tag.
187
-                    newUint8.set(new Uint8Array(authTag, 0, digestLength[encodedFrame.type]),
188
-                        unencryptedBytes[encodedFrame.type] + cipherText.byteLength);
189
-                    encodedFrame.data = newData;
190
-
191
-                    return controller.enqueue(encodedFrame);
192
-                });
193
-            }, e => {
194
-                // TODO: surface this to the app.
195
-                console.error(e);
196
-
197
-                // We are not enqueuing the frame here on purpose.
198
-            });
199
-        }
200
-
201
-        /* NOTE WELL:
202
-         * This will send unencrypted data (only protected by DTLS transport encryption) when no key is configured.
203
-         * This is ok for demo purposes but should not be done once this becomes more relied upon.
204
-         */
205
-        controller.enqueue(encodedFrame);
206
-    }
207
-
208
-    /**
209
-     * Function that will be injected in a stream and will decrypt the given encoded frames.
210
-     *
211
-     * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
212
-     * @param {TransformStreamDefaultController} controller - TransportStreamController.
213
-     */
214
-    async decodeFunction(encodedFrame, controller) {
215
-        const data = new Uint8Array(encodedFrame.data);
216
-        const keyIndex = data[encodedFrame.data.byteLength - 1] & 0x7;
217
-
218
-        if (this._cryptoKeyRing[keyIndex]) {
219
-            const counterLength = 1 + ((data[encodedFrame.data.byteLength - 1] >> 4) & 0x7);
220
-            const frameHeader = new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type]);
221
-
222
-            // Extract the truncated authentication tag.
223
-            const authTagOffset = encodedFrame.data.byteLength - (digestLength[encodedFrame.type]
224
-                + counterLength + 1);
225
-            const authTag = encodedFrame.data.slice(authTagOffset, authTagOffset
226
-                + digestLength[encodedFrame.type]);
227
-
228
-            // Set authentication tag bytes to 0.
229
-            const zeros = new Uint8Array(digestLength[encodedFrame.type]);
230
-
231
-            data.set(zeros, encodedFrame.data.byteLength - (digestLength[encodedFrame.type] + counterLength + 1));
232
-
233
-            // Do truncated hash comparison. If the hash does not match we might have to advance the
234
-            // ratchet a limited number of times. See (even though the description there is odd)
235
-            // https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.5.1
236
-            let { authenticationKey, material } = this._cryptoKeyRing[keyIndex];
237
-            let valid = false;
238
-            let newKeys = null;
239
-
240
-            for (let distance = 0; distance < ratchetWindow; distance++) {
241
-                const calculatedTag = await crypto.subtle.sign(authenticationTagOptions,
242
-                    authenticationKey, encodedFrame.data);
243
-
244
-                if (isArrayEqual(new Uint8Array(authTag),
245
-                        new Uint8Array(calculatedTag.slice(0, digestLength[encodedFrame.type])))) {
246
-                    valid = true;
247
-                    if (distance > 0) {
248
-                        this._setKeys(newKeys);
249
-                    }
250
-                    break;
251
-                }
252
-
253
-                // Attempt to ratchet and generate the next set of keys.
254
-                material = await importKey(await ratchet(material));
255
-                newKeys = await deriveKeys(material);
256
-                authenticationKey = newKeys.authenticationKey;
257
-            }
258
-
259
-            // Check whether we found a valid signature.
260
-            if (!valid) {
261
-                // TODO: return an error to the app.
262
-
263
-                console.error('Authentication tag mismatch');
264
-
265
-                return;
266
-            }
267
-
268
-            // Extract the counter.
269
-            const counter = new Uint8Array(16);
270
-
271
-            counter.set(data.slice(encodedFrame.data.byteLength - (counterLength + 1),
272
-                encodedFrame.data.byteLength - 1), 16 - counterLength);
273
-            const counterView = new DataView(counter.buffer);
274
-
275
-            // XOR the counter with the saltKey to construct the AES CTR.
276
-            const saltKey = new DataView(this._cryptoKeyRing[keyIndex].saltKey);
277
-
278
-            for (let i = 0; i < counter.byteLength; i++) {
279
-                counterView.setUint8(i,
280
-                    counterView.getUint8(i) ^ saltKey.getUint8(i));
281
-            }
282
-
283
-            return crypto.subtle.decrypt({
284
-                name: 'AES-CTR',
285
-                counter,
286
-                length: 64
287
-            }, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
288
-                    unencryptedBytes[encodedFrame.type],
289
-                    encodedFrame.data.byteLength - (unencryptedBytes[encodedFrame.type]
290
-                    + digestLength[encodedFrame.type] + counterLength + 1))
291
-            ).then(plainText => {
292
-                const newData = new ArrayBuffer(unencryptedBytes[encodedFrame.type] + plainText.byteLength);
293
-                const newUint8 = new Uint8Array(newData);
294
-
295
-                newUint8.set(frameHeader);
296
-                newUint8.set(new Uint8Array(plainText), unencryptedBytes[encodedFrame.type]);
297
-                encodedFrame.data = newData;
298
-
299
-                return controller.enqueue(encodedFrame);
300
-            }, e => {
301
-                console.error(e);
302
-
303
-                // TODO: notify the application about error status.
304
-                // TODO: For video we need a better strategy since we do not want to based any
305
-                // non-error frames on a garbage keyframe.
306
-                if (encodedFrame.type === undefined) { // audio, replace with silence.
307
-                    const newData = new ArrayBuffer(3);
308
-                    const newUint8 = new Uint8Array(newData);
309
-
310
-                    newUint8.set([ 0xd8, 0xff, 0xfe ]); // opus silence frame.
311
-                    encodedFrame.data = newData;
312
-                    controller.enqueue(encodedFrame);
313
-                }
314
-            });
315
-        } else if (keyIndex >= this._cryptoKeyRing.length && this._cryptoKeyRing[this._currentKeyIndex]) {
316
-            // If we are encrypting but don't have a key for the remote drop the frame.
317
-            // This is a heuristic since we don't know whether a packet is encrypted,
318
-            // do not have a checksum and do not have signaling for whether a remote participant does
319
-            // encrypt or not.
320
-            return;
321
-        }
322
-
323
-        // TODO: this just passes through to the decoder. Is that ok? If we don't know the key yet
324
-        // we might want to buffer a bit but it is still unclear how to do that (and for how long etc).
325
-        controller.enqueue(encodedFrame);
326
-    }
327
-}
7
+import { Context } from './Context';
8
+import { polyFillEncodedFrameMetadata } from './utils';
328 9
 
329 10
 const contexts = new Map(); // Map participant id => context
330 11
 

Завантаження…
Відмінити
Зберегти