Sfoglia il codice sorgente

e2ee: move to a worker (#1112)

Moveѕ e2ee operations to a worker that is included as text/blob for now
to simplify deployment.
dev1
Philipp Hancke 5 anni fa
parent
commit
ba0777f0cf
Nessun account collegato all'indirizzo email del committer
3 ha cambiato i file con 326 aggiunte e 266 eliminazioni
  1. 6
    1
      doc/e2ee.md
  2. 30
    265
      modules/e2ee/E2EEContext.js
  3. 290
    0
      modules/e2ee/Worker.js

+ 6
- 1
doc/e2ee.md Vedi File

@@ -44,4 +44,9 @@ nor the Opus TOC byte
44 44
 This allows the decoder to understand the frame a bit more and makes it generate the fun looking garbage we see in the video.
45 45
 This also means the SFU does not know (ideally) that the content is end-to-end encrypted and there are no changes in the SFU required at all.
46 46
 
47
-Decryption errors are currently handled by inserting silence or a black frame.
47
+## Using workers
48
+
49
+Insertable Streams are transferable and can be sent from the main javascript context to a Worker
50
+  https://developer.mozilla.org/en-US/docs/Web/API/Worker
51
+We are using a named worker (E2EEworker) which allows very easy inspection in Chrome Devtools.
52
+It also makes the keys very self-contained.

+ 30
- 265
modules/e2ee/E2EEContext.js Vedi File

@@ -1,36 +1,10 @@
1
-/* global __filename, TransformStream */
1
+/* global __filename */
2 2
 
3
+import { e2eeWorkerScript } from './Worker';
3 4
 import { getLogger } from 'jitsi-meet-logger';
4 5
 
5 6
 const logger = getLogger(__filename);
6 7
 
7
-// We use a ringbuffer of keys so we can change them and still decode packets that were
8
-// encrypted with an old key.
9
-// In the future when we dont rely on a globally shared key we will actually use it. For
10
-// now set the size to 1 which means there is only a single key. This causes some
11
-// glitches when changing the key but its ok.
12
-const keyRingSize = 1;
13
-
14
-// We use a 96 bit IV for AES GCM. This is signalled in plain together with the
15
-// packet. See https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams
16
-const ivLength = 12;
17
-
18
-// We copy the first bytes of the VP8 payload unencrypted.
19
-// For keyframes this is 10 bytes, for non-keyframes (delta) 3. See
20
-//   https://tools.ietf.org/html/rfc6386#section-9.1
21
-// This allows the bridge to continue detecting keyframes (only one byte needed in the JVB)
22
-// and is also a bit easier for the VP8 decoder (i.e. it generates funny garbage pictures
23
-// instead of being unable to decode).
24
-// This is a bit for show and we might want to reduce to 1 unconditionally in the final version.
25
-//
26
-// For audio (where frame.type is not set) we do not encrypt the opus TOC byte:
27
-//   https://tools.ietf.org/html/rfc6716#section-3.1
28
-const unencryptedBytes = {
29
-    key: 10,
30
-    delta: 3,
31
-    undefined: 1 // frame.type is not set on audio
32
-};
33
-
34 8
 // Flag to set on senders / receivers to avoid setting up the encryption transform
35 9
 // more than once.
36 10
 const kJitsiE2EE = Symbol('kJitsiE2EE');
@@ -62,20 +36,20 @@ export default class E2EEcontext {
62 36
     constructor(options) {
63 37
         this._options = options;
64 38
 
65
-        // An array (ring) of keys that we use for sending and receiving.
66
-        this._cryptoKeyRing = new Array(keyRingSize);
67
-
68
-        // A pointer to the currently used key.
69
-        this._currentKeyIndex = -1;
70
-
71
-        // We keep track of how many frames we have sent per ssrc.
72
-        // Starts with a random offset similar to the RTP sequence number.
73
-        this._sendCounts = new Map();
39
+        // Initialize the E2EE worker.
40
+        this._worker = new Worker(e2eeWorkerScript, {
41
+            name: 'E2EE Worker'
42
+        });
43
+        this._worker.onerror = e => logger.onerror(e);
74 44
 
75 45
         // Initialize the salt and convert it once.
76 46
         const encoder = new TextEncoder();
77 47
 
78
-        this._salt = encoder.encode(options.salt);
48
+        // Send initial options to worker.
49
+        this._worker.postMessage({
50
+            operation: 'initialize',
51
+            salt: encoder.encode(options.salt)
52
+        });
79 53
     }
80 54
 
81 55
     /**
@@ -89,18 +63,16 @@ export default class E2EEcontext {
89 63
         if (receiver[kJitsiE2EE]) {
90 64
             return;
91 65
         }
66
+        receiver[kJitsiE2EE] = true;
92 67
 
93 68
         const receiverStreams
94 69
             = kind === 'video' ? receiver.createEncodedVideoStreams() : receiver.createEncodedAudioStreams();
95
-        const transform = new TransformStream({
96
-            transform: this._decodeFunction.bind(this)
97
-        });
98
-
99
-        receiverStreams.readableStream
100
-            .pipeThrough(transform)
101
-            .pipeTo(receiverStreams.writableStream);
102 70
 
103
-        receiver[kJitsiE2EE] = true;
71
+        this._worker.postMessage({
72
+            operation: 'decode',
73
+            readableStream: receiverStreams.readableStream,
74
+            writableStream: receiverStreams.writableStream
75
+        }, [ receiverStreams.readableStream, receiverStreams.writableStream ]);
104 76
     }
105 77
 
106 78
     /**
@@ -114,18 +86,16 @@ export default class E2EEcontext {
114 86
         if (sender[kJitsiE2EE]) {
115 87
             return;
116 88
         }
89
+        sender[kJitsiE2EE] = true;
117 90
 
118 91
         const senderStreams
119 92
             = kind === 'video' ? sender.createEncodedVideoStreams() : sender.createEncodedAudioStreams();
120
-        const transform = new TransformStream({
121
-            transform: this._encodeFunction.bind(this)
122
-        });
123
-
124
-        senderStreams.readableStream
125
-            .pipeThrough(transform)
126
-            .pipeTo(senderStreams.writableStream);
127 93
 
128
-        sender[kJitsiE2EE] = true;
94
+        this._worker.postMessage({
95
+            operation: 'encode',
96
+            readableStream: senderStreams.readableStream,
97
+            writableStream: senderStreams.writableStream
98
+        }, [ senderStreams.readableStream, senderStreams.writableStream ]);
129 99
     }
130 100
 
131 101
     /**
@@ -133,225 +103,20 @@ export default class E2EEcontext {
133 103
      *
134 104
      * @param {string} value - Value to be used as the new key. May be falsy to disable end-to-end encryption.
135 105
      */
136
-    async setKey(value) {
106
+    setKey(value) {
137 107
         let key;
138 108
 
139 109
         if (value) {
140 110
             const encoder = new TextEncoder();
141 111
 
142
-            key = await this._deriveKey(encoder.encode(value));
112
+            key = encoder.encode(value);
143 113
         } else {
144 114
             key = false;
145 115
         }
146
-        this._currentKeyIndex++;
147
-        this._cryptoKeyRing[this._currentKeyIndex % this._cryptoKeyRing.length] = key;
148
-    }
149
-
150
-    /**
151
-     * Derives a AES-GCM key with 128 bits from the input using PBKDF2
152
-     * The salt is configured in the constructor of this class.
153
-     * @param {Uint8Array} keyBytes - Value to derive key from
154
-     */
155
-    async _deriveKey(keyBytes) {
156
-        // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
157
-        const material = await crypto.subtle.importKey('raw', keyBytes,
158
-            'PBKDF2', false, [ 'deriveBits', 'deriveKey' ]);
159
-
160
-        // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#PBKDF2
161
-        return crypto.subtle.deriveKey({
162
-            name: 'PBKDF2',
163
-            salt: this._salt,
164
-            iterations: 100000,
165
-            hash: 'SHA-256'
166
-        }, material, {
167
-            name: 'AES-GCM',
168
-            length: 128
169
-        }, false, [ 'encrypt', 'decrypt' ]);
170
-    }
171
-
172
-    /**
173
-     * Construct the IV used for AES-GCM and sent (in plain) with the packet similar to
174
-     * https://tools.ietf.org/html/rfc7714#section-8.1
175
-     * It concatenates
176
-     * - the 32 bit synchronization source (SSRC) given on the encoded frame,
177
-     * - the 32 bit rtp timestamp given on the encoded frame,
178
-     * - a send counter that is specific to the SSRC. Starts at a random number.
179
-     * The send counter is essentially the pictureId but we currently have to implement this ourselves.
180
-     * There is no XOR with a salt. Note that this IV leaks the SSRC to the receiver but since this is
181
-     * randomly generated and SFUs may not rewrite this is considered acceptable.
182
-     * The SSRC is used to allow demultiplexing multiple streams with the same key, as described in
183
-     *   https://tools.ietf.org/html/rfc3711#section-4.1.1
184
-     * The RTP timestamp is 32 bits and advances by the codec clock rate (90khz for video, 48khz for
185
-     * opus audio) every second. For video it rolls over roughly every 13 hours.
186
-     * The send counter will advance at the frame rate (30fps for video, 50fps for 20ms opus audio)
187
-     * every second. It will take a long time to roll over.
188
-     *
189
-     * See also https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams
190
-     */
191
-    _makeIV(synchronizationSource, timestamp) {
192
-        const iv = new ArrayBuffer(ivLength);
193
-        const ivView = new DataView(iv);
194
-
195
-        // having to keep our own send count (similar to a picture id) is not ideal.
196
-        if (!this._sendCounts.has(synchronizationSource)) {
197
-            // Initialize with a random offset, similar to the RTP sequence number.
198
-            this._sendCounts.set(synchronizationSource, Math.floor(Math.random() * 0xFFFF));
199
-        }
200
-        const sendCount = this._sendCounts.get(synchronizationSource);
201
-
202
-        ivView.setUint32(0, synchronizationSource);
203
-        ivView.setUint32(4, timestamp);
204
-        ivView.setUint32(8, sendCount % 0xFFFF);
205
-
206
-        this._sendCounts.set(synchronizationSource, sendCount + 1);
207
-
208
-        return iv;
209
-    }
210
-
211
-    /**
212
-     * Function that will be injected in a stream and will encrypt the given encoded frames.
213
-     *
214
-     * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
215
-     * @param {TransformStreamDefaultController} controller - TransportStreamController.
216
-     *
217
-     * The packet format is described below. One of the design goals was to not require
218
-     * changes to the SFU which for video requires not encrypting the keyframe bit of VP8
219
-     * as SFUs need to detect a keyframe (framemarking or the generic frame descriptor will
220
-     * solve this eventually). This also "hides" that a client is using E2EE a bit.
221
-     *
222
-     * Note that this operates on the full frame, i.e. for VP8 the data described in
223
-     *   https://tools.ietf.org/html/rfc6386#section-9.1
224
-     *
225
-     * The VP8 payload descriptor described in
226
-     *   https://tools.ietf.org/html/rfc7741#section-4.2
227
-     * is part of the RTP packet and not part of the frame and is not controllable by us.
228
-     * This is fine as the SFU keeps having access to it for routing.
229
-     *
230
-     * The encrypted frame is formed as follows:
231
-     * 1) Leave the first (10, 3, 1) bytes unencrypted, depending on the frame type and kind.
232
-     * 2) Form the GCM IV for the frame as described above.
233
-     * 3) Encrypt the rest of the frame using AES-GCM.
234
-     * 4) Allocate space for the encrypted frame.
235
-     * 5) Copy the unencrypted bytes to the start of the encrypted frame.
236
-     * 6) Append the ciphertext to the encrypted frame.
237
-     * 7) Append the IV.
238
-     * 8) Append a single byte for the key identifier. TODO: we don't need all the bits.
239
-     * 9) Enqueue the encrypted frame for sending.
240
-     */
241
-    _encodeFunction(encodedFrame, controller) {
242
-        const keyIndex = this._currentKeyIndex % this._cryptoKeyRing.length;
243
-
244
-        if (this._cryptoKeyRing[keyIndex]) {
245
-            const iv = this._makeIV(encodedFrame.synchronizationSource, encodedFrame.timestamp);
246
-
247
-            return crypto.subtle.encrypt({
248
-                name: 'AES-GCM',
249
-                iv,
250
-                additionalData: new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type])
251
-            }, this._cryptoKeyRing[keyIndex], new Uint8Array(encodedFrame.data, unencryptedBytes[encodedFrame.type]))
252
-            .then(cipherText => {
253
-                const newData = new ArrayBuffer(unencryptedBytes[encodedFrame.type] + cipherText.byteLength
254
-                    + iv.byteLength + 1);
255
-                const newUint8 = new Uint8Array(newData);
256
-
257
-                newUint8.set(
258
-                    new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type])); // copy first bytes.
259
-                newUint8.set(
260
-                    new Uint8Array(cipherText), unencryptedBytes[encodedFrame.type]); // add ciphertext.
261
-                newUint8.set(
262
-                    new Uint8Array(iv), unencryptedBytes[encodedFrame.type] + cipherText.byteLength); // append IV.
263
-                newUint8[unencryptedBytes[encodedFrame.type] + cipherText.byteLength + ivLength]
264
-                    = keyIndex; // set key index.
265
-
266
-                encodedFrame.data = newData;
267
-
268
-                return controller.enqueue(encodedFrame);
269
-            }, e => {
270
-                logger.error(e);
271
-
272
-                // We are not enqueuing the frame here on purpose.
273
-            });
274
-        }
275
-
276
-        /* NOTE WELL:
277
-         * This will send unencrypted data (only protected by DTLS transport encryption) when no key is configured.
278
-         * This is ok for demo purposes but should not be done once this becomes more relied upon.
279
-         */
280
-        controller.enqueue(encodedFrame);
281
-    }
282
-
283
-    /**
284
-     * Function that will be injected in a stream and will decrypt the given encoded frames.
285
-     *
286
-     * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
287
-     * @param {TransformStreamDefaultController} controller - TransportStreamController.
288
-     *
289
-     * The decrypted frame is formed as follows:
290
-     * 1) Extract the key index from the last byte of the encrypted frame.
291
-     *    If there is no key associated with the key index, the frame is enqueued for decoding
292
-     *    and these steps terminate.
293
-     * 2) Determine the frame type in order to look up the number of unencrypted header bytes.
294
-     * 2) Extract the 12-byte IV from its position near the end of the packet.
295
-     *    Note: the IV is treated as opaque and not reconstructed from the input.
296
-     * 3) Decrypt the encrypted frame content after the unencrypted bytes using AES-GCM.
297
-     * 4) Allocate space for the decrypted frame.
298
-     * 5) Copy the unencrypted bytes from the start of the encrypted frame.
299
-     * 6) Append the plaintext to the decrypted frame.
300
-     * 7) Enqueue the decrypted frame for decoding.
301
-     */
302
-    _decodeFunction(encodedFrame, controller) {
303
-        const data = new Uint8Array(encodedFrame.data);
304
-        const keyIndex = data[encodedFrame.data.byteLength - 1];
305
-
306
-        if (this._cryptoKeyRing[keyIndex]) {
307
-            const iv = new Uint8Array(encodedFrame.data, encodedFrame.data.byteLength - ivLength - 1, ivLength);
308
-            const cipherTextStart = unencryptedBytes[encodedFrame.type];
309
-            const cipherTextLength = encodedFrame.data.byteLength - (unencryptedBytes[encodedFrame.type]
310
-                + ivLength + 1);
311
-
312
-            return crypto.subtle.decrypt({
313
-                name: 'AES-GCM',
314
-                iv,
315
-                additionalData: new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type])
316
-            }, this._cryptoKeyRing[keyIndex], new Uint8Array(encodedFrame.data, cipherTextStart, cipherTextLength))
317
-            .then(plainText => {
318
-                const newData = new ArrayBuffer(unencryptedBytes[encodedFrame.type] + plainText.byteLength);
319
-                const newUint8 = new Uint8Array(newData);
320
-
321
-                newUint8.set(new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type]));
322
-                newUint8.set(new Uint8Array(plainText), unencryptedBytes[encodedFrame.type]);
323
-
324
-                encodedFrame.data = newData;
325
-
326
-                return controller.enqueue(encodedFrame);
327
-            }, e => {
328
-                logger.error(e, encodedFrame.type);
329
-
330
-                // TODO: notify the application about error status.
331
-
332
-                // TODO: For video we need a better strategy since we do not want to based any
333
-                // non-error frames on a garbage keyframe.
334
-                if (encodedFrame.type === undefined) { // audio, replace with silence.
335
-                    // audio, replace with silence.
336
-                    const newData = new ArrayBuffer(3);
337
-                    const newUint8 = new Uint8Array(newData);
338
-
339
-                    newUint8.set([ 0xd8, 0xff, 0xfe ]); // opus silence frame.
340
-                    encodedFrame.data = newData;
341
-                    controller.enqueue(encodedFrame);
342
-                }
343
-            });
344
-        } else if (keyIndex >= this._cryptoKeyRing.length
345
-                && this._cryptoKeyRing[this._currentKeyIndex % this._cryptoKeyRing.length]) {
346
-            // If we are encrypting but don't have a key for the remote drop the frame.
347
-            // This is a heuristic since we don't know whether a packet is encrypted,
348
-            // do not have a checksum and do not have signaling for whether a remote participant does
349
-            // encrypt or not.
350
-            return;
351
-        }
352 116
 
353
-        // TODO: this just passes through to the decoder. Is that ok? If we don't know the key yet
354
-        // we might want to buffer a bit but it is still unclear how to do that (and for how long etc).
355
-        controller.enqueue(encodedFrame);
117
+        this._worker.postMessage({
118
+            operation: 'setKey',
119
+            key
120
+        });
356 121
     }
357 122
 }

+ 290
- 0
modules/e2ee/Worker.js Vedi File

@@ -0,0 +1,290 @@
1
+// Worker for E2EE/Insertable streams. Currently served as an inline blob.
2
+const code = `
3
+    // We use a ringbuffer of keys so we can change them and still decode packets that were
4
+    // encrypted with an old key.
5
+    // In the future when we dont rely on a globally shared key we will actually use it. For
6
+    // now set the size to 1 which means there is only a single key. This causes some
7
+    // glitches when changing the key but its ok.
8
+    const keyRingSize = 1;
9
+
10
+    // We use a 96 bit IV for AES GCM. This is signalled in plain together with the
11
+    // packet. See https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams
12
+    const ivLength = 12;
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
+    // An array (ring) of keys that we use for sending and receiving.
31
+    const cryptoKeyRing = new Array(keyRingSize);
32
+
33
+        // A pointer to the currently used key.
34
+    let currentKeyIndex = -1;
35
+
36
+    // We keep track of how many frames we have sent per ssrc.
37
+    // Starts with a random offset similar to the RTP sequence number.
38
+    const sendCounts = new Map();
39
+
40
+    // Salt used in key derivation
41
+    // FIXME: We currently use the MUC room name for this which has the same lifetime
42
+    // as this worker. While not (pseudo)random as recommended in
43
+    // https://developer.mozilla.org/en-US/docs/Web/API/Pbkdf2Params
44
+    // this is easily available and the same for all participants.
45
+    // We currently do not enforce a minimum length of 16 bytes either.
46
+    let salt;
47
+
48
+    /**
49
+     * Derives a AES-GCM key with 128 bits from the input using PBKDF2
50
+     * The salt is configured in the constructor of this class.
51
+     * @param {Uint8Array} keyBytes - Value to derive key from
52
+     */
53
+    async function deriveKey(keyBytes) {
54
+        // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
55
+        const material = await crypto.subtle.importKey('raw', keyBytes,
56
+            'PBKDF2', false, [ 'deriveBits', 'deriveKey' ]);
57
+
58
+        // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#PBKDF2
59
+        return crypto.subtle.deriveKey({
60
+            name: 'PBKDF2',
61
+            salt,
62
+            iterations: 100000,
63
+            hash: 'SHA-256'
64
+        }, material, {
65
+            name: 'AES-GCM',
66
+            length: 128
67
+        }, false, [ 'encrypt', 'decrypt' ]);
68
+    }
69
+
70
+    /**
71
+     * Construct the IV used for AES-GCM and sent (in plain) with the packet similar to
72
+     * https://tools.ietf.org/html/rfc7714#section-8.1
73
+     * It concatenates
74
+     * - the 32 bit synchronization source (SSRC) given on the encoded frame,
75
+     * - the 32 bit rtp timestamp given on the encoded frame,
76
+     * - a send counter that is specific to the SSRC. Starts at a random number.
77
+     * The send counter is essentially the pictureId but we currently have to implement this ourselves.
78
+     * There is no XOR with a salt. Note that this IV leaks the SSRC to the receiver but since this is
79
+     * randomly generated and SFUs may not rewrite this is considered acceptable.
80
+     * The SSRC is used to allow demultiplexing multiple streams with the same key, as described in
81
+     *   https://tools.ietf.org/html/rfc3711#section-4.1.1
82
+     * The RTP timestamp is 32 bits and advances by the codec clock rate (90khz for video, 48khz for
83
+     * opus audio) every second. For video it rolls over roughly every 13 hours.
84
+     * The send counter will advance at the frame rate (30fps for video, 50fps for 20ms opus audio)
85
+     * every second. It will take a long time to roll over.
86
+     *
87
+     * See also https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams
88
+     */
89
+    function makeIV(synchronizationSource, timestamp) {
90
+        const iv = new ArrayBuffer(ivLength);
91
+        const ivView = new DataView(iv);
92
+
93
+        // having to keep our own send count (similar to a picture id) is not ideal.
94
+        if (!sendCounts.has(synchronizationSource)) {
95
+            // Initialize with a random offset, similar to the RTP sequence number.
96
+            sendCounts.set(synchronizationSource, Math.floor(Math.random() * 0xFFFF));
97
+        }
98
+        const sendCount = sendCounts.get(synchronizationSource);
99
+
100
+        ivView.setUint32(0, synchronizationSource);
101
+        ivView.setUint32(4, timestamp);
102
+        ivView.setUint32(8, sendCount % 0xFFFF);
103
+
104
+        sendCounts.set(synchronizationSource, sendCount + 1);
105
+
106
+        return iv;
107
+    }
108
+
109
+    /**
110
+     * Function that will be injected in a stream and will encrypt the given encoded frames.
111
+     *
112
+     * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
113
+     * @param {TransformStreamDefaultController} controller - TransportStreamController.
114
+     *
115
+     * The packet format is described below. One of the design goals was to not require
116
+     * changes to the SFU which for video requires not encrypting the keyframe bit of VP8
117
+     * as SFUs need to detect a keyframe (framemarking or the generic frame descriptor will
118
+     * solve this eventually). This also "hides" that a client is using E2EE a bit.
119
+     *
120
+     * Note that this operates on the full frame, i.e. for VP8 the data described in
121
+     *   https://tools.ietf.org/html/rfc6386#section-9.1
122
+     *
123
+     * The VP8 payload descriptor described in
124
+     *   https://tools.ietf.org/html/rfc7741#section-4.2
125
+     * is part of the RTP packet and not part of the frame and is not controllable by us.
126
+     * This is fine as the SFU keeps having access to it for routing.
127
+     *
128
+     * The encrypted frame is formed as follows:
129
+     * 1) Leave the first (10, 3, 1) bytes unencrypted, depending on the frame type and kind.
130
+     * 2) Form the GCM IV for the frame as described above.
131
+     * 3) Encrypt the rest of the frame using AES-GCM.
132
+     * 4) Allocate space for the encrypted frame.
133
+     * 5) Copy the unencrypted bytes to the start of the encrypted frame.
134
+     * 6) Append the ciphertext to the encrypted frame.
135
+     * 7) Append the IV.
136
+     * 8) Append a single byte for the key identifier. TODO: we don't need all the bits.
137
+     * 9) Enqueue the encrypted frame for sending.
138
+     */
139
+    function encodeFunction(encodedFrame, controller) {
140
+        const keyIndex = currentKeyIndex % cryptoKeyRing.length;
141
+
142
+        if (cryptoKeyRing[keyIndex]) {
143
+            const iv = makeIV(encodedFrame.synchronizationSource, encodedFrame.timestamp);
144
+
145
+            return crypto.subtle.encrypt({
146
+                name: 'AES-GCM',
147
+                iv,
148
+                additionalData: new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type])
149
+            }, cryptoKeyRing[keyIndex], new Uint8Array(encodedFrame.data, unencryptedBytes[encodedFrame.type]))
150
+            .then(cipherText => {
151
+                const newData = new ArrayBuffer(unencryptedBytes[encodedFrame.type] + cipherText.byteLength
152
+                    + iv.byteLength + 1);
153
+                const newUint8 = new Uint8Array(newData);
154
+
155
+                newUint8.set(
156
+                    new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type])); // copy first bytes.
157
+                newUint8.set(
158
+                    new Uint8Array(cipherText), unencryptedBytes[encodedFrame.type]); // add ciphertext.
159
+                newUint8.set(
160
+                    new Uint8Array(iv), unencryptedBytes[encodedFrame.type] + cipherText.byteLength); // append IV.
161
+                newUint8[unencryptedBytes[encodedFrame.type] + cipherText.byteLength + ivLength]
162
+                    = keyIndex; // set key index.
163
+
164
+                encodedFrame.data = newData;
165
+
166
+                return controller.enqueue(encodedFrame);
167
+            }, e => {
168
+                console.error(e);
169
+
170
+                // We are not enqueuing the frame here on purpose.
171
+            });
172
+        }
173
+
174
+        /* NOTE WELL:
175
+         * This will send unencrypted data (only protected by DTLS transport encryption) when no key is configured.
176
+         * This is ok for demo purposes but should not be done once this becomes more relied upon.
177
+         */
178
+        controller.enqueue(encodedFrame);
179
+    }
180
+
181
+    /**
182
+     * Function that will be injected in a stream and will decrypt the given encoded frames.
183
+     *
184
+     * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
185
+     * @param {TransformStreamDefaultController} controller - TransportStreamController.
186
+     *
187
+     * The decrypted frame is formed as follows:
188
+     * 1) Extract the key index from the last byte of the encrypted frame.
189
+     *    If there is no key associated with the key index, the frame is enqueued for decoding
190
+     *    and these steps terminate.
191
+     * 2) Determine the frame type in order to look up the number of unencrypted header bytes.
192
+     * 2) Extract the 12-byte IV from its position near the end of the packet.
193
+     *    Note: the IV is treated as opaque and not reconstructed from the input.
194
+     * 3) Decrypt the encrypted frame content after the unencrypted bytes using AES-GCM.
195
+     * 4) Allocate space for the decrypted frame.
196
+     * 5) Copy the unencrypted bytes from the start of the encrypted frame.
197
+     * 6) Append the plaintext to the decrypted frame.
198
+     * 7) Enqueue the decrypted frame for decoding.
199
+     */
200
+    function decodeFunction(encodedFrame, controller) {
201
+        const data = new Uint8Array(encodedFrame.data);
202
+        const keyIndex = data[encodedFrame.data.byteLength - 1];
203
+
204
+        if (cryptoKeyRing[keyIndex]) {
205
+            const iv = new Uint8Array(encodedFrame.data, encodedFrame.data.byteLength - ivLength - 1, ivLength);
206
+            const cipherTextStart = unencryptedBytes[encodedFrame.type];
207
+            const cipherTextLength = encodedFrame.data.byteLength - (unencryptedBytes[encodedFrame.type]
208
+                + ivLength + 1);
209
+
210
+            return crypto.subtle.decrypt({
211
+                name: 'AES-GCM',
212
+                iv,
213
+                additionalData: new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type])
214
+            }, cryptoKeyRing[keyIndex], new Uint8Array(encodedFrame.data, cipherTextStart, cipherTextLength))
215
+            .then(plainText => {
216
+                const newData = new ArrayBuffer(unencryptedBytes[encodedFrame.type] + plainText.byteLength);
217
+                const newUint8 = new Uint8Array(newData);
218
+
219
+                newUint8.set(new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type]));
220
+                newUint8.set(new Uint8Array(plainText), unencryptedBytes[encodedFrame.type]);
221
+
222
+                encodedFrame.data = newData;
223
+
224
+                return controller.enqueue(encodedFrame);
225
+            }, e => {
226
+                // TODO: notify the application about error status.
227
+
228
+                // TODO: For video we need a better strategy since we do not want to based any
229
+                // non-error frames on a garbage keyframe.
230
+                if (encodedFrame.type === undefined) { // audio, replace with silence.
231
+                    // audio, replace with silence.
232
+                    const newData = new ArrayBuffer(3);
233
+                    const newUint8 = new Uint8Array(newData);
234
+
235
+                    newUint8.set([ 0xd8, 0xff, 0xfe ]); // opus silence frame.
236
+                    encodedFrame.data = newData;
237
+                    controller.enqueue(encodedFrame);
238
+                }
239
+            });
240
+        } else if (keyIndex >= cryptoKeyRing.length && cryptoKeyRing[currentKeyIndex % cryptoKeyRing.length]) {
241
+            // If we are encrypting but don't have a key for the remote drop the frame.
242
+            // This is a heuristic since we don't know whether a packet is encrypted,
243
+            // do not have a checksum and do not have signaling for whether a remote participant does
244
+            // encrypt or not.
245
+            return;
246
+        }
247
+
248
+        // TODO: this just passes through to the decoder. Is that ok? If we don't know the key yet
249
+        // we might want to buffer a bit but it is still unclear how to do that (and for how long etc).
250
+        controller.enqueue(encodedFrame);
251
+    }
252
+
253
+    onmessage = async (event) => {
254
+        const {operation} = event.data;
255
+        if (operation === 'initialize') {
256
+            salt = event.data.salt;
257
+        } else if (operation === 'encode') {
258
+            const {readableStream, writableStream} = event.data;
259
+            const transformStream = new TransformStream({
260
+                transform: encodeFunction,
261
+            });
262
+            readableStream
263
+                .pipeThrough(transformStream)
264
+                .pipeTo(writableStream);
265
+        } else if (operation === 'decode') {
266
+            const {readableStream, writableStream} = event.data;
267
+            const transformStream = new TransformStream({
268
+                transform: decodeFunction,
269
+            });
270
+            readableStream
271
+                .pipeThrough(transformStream)
272
+                .pipeTo(writableStream);
273
+        } else if (operation === 'setKey') {
274
+            const keyBytes = event.data.key;
275
+            let key;
276
+            if (keyBytes) {
277
+                key = await deriveKey(keyBytes);
278
+            } else {
279
+                key = false;
280
+            }
281
+            currentKeyIndex++;
282
+            cryptoKeyRing[currentKeyIndex % cryptoKeyRing.length] = key;
283
+        } else {
284
+            console.error('e2ee worker', operation);
285
+        }
286
+    };
287
+
288
+`;
289
+
290
+export const e2eeWorkerScript = URL.createObjectURL(new Blob([ code ], { type: 'application/javascript' }));

Loading…
Annulla
Salva