瀏覽代碼

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 年之前
父節點
當前提交
ba0777f0cf
沒有連結到貢獻者的電子郵件帳戶。
共有 3 個檔案被更改,包括 326 行新增266 行删除
  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 查看文件

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.
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
 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.
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 查看文件

1
-/* global __filename, TransformStream */
1
+/* global __filename */
2
 
2
 
3
+import { e2eeWorkerScript } from './Worker';
3
 import { getLogger } from 'jitsi-meet-logger';
4
 import { getLogger } from 'jitsi-meet-logger';
4
 
5
 
5
 const logger = getLogger(__filename);
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
 // Flag to set on senders / receivers to avoid setting up the encryption transform
8
 // Flag to set on senders / receivers to avoid setting up the encryption transform
35
 // more than once.
9
 // more than once.
36
 const kJitsiE2EE = Symbol('kJitsiE2EE');
10
 const kJitsiE2EE = Symbol('kJitsiE2EE');
62
     constructor(options) {
36
     constructor(options) {
63
         this._options = options;
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
         // Initialize the salt and convert it once.
45
         // Initialize the salt and convert it once.
76
         const encoder = new TextEncoder();
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
         if (receiver[kJitsiE2EE]) {
63
         if (receiver[kJitsiE2EE]) {
90
             return;
64
             return;
91
         }
65
         }
66
+        receiver[kJitsiE2EE] = true;
92
 
67
 
93
         const receiverStreams
68
         const receiverStreams
94
             = kind === 'video' ? receiver.createEncodedVideoStreams() : receiver.createEncodedAudioStreams();
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
         if (sender[kJitsiE2EE]) {
86
         if (sender[kJitsiE2EE]) {
115
             return;
87
             return;
116
         }
88
         }
89
+        sender[kJitsiE2EE] = true;
117
 
90
 
118
         const senderStreams
91
         const senderStreams
119
             = kind === 'video' ? sender.createEncodedVideoStreams() : sender.createEncodedAudioStreams();
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
      *
103
      *
134
      * @param {string} value - Value to be used as the new key. May be falsy to disable end-to-end encryption.
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
         let key;
107
         let key;
138
 
108
 
139
         if (value) {
109
         if (value) {
140
             const encoder = new TextEncoder();
110
             const encoder = new TextEncoder();
141
 
111
 
142
-            key = await this._deriveKey(encoder.encode(value));
112
+            key = encoder.encode(value);
143
         } else {
113
         } else {
144
             key = false;
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 查看文件

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…
取消
儲存