Pārlūkot izejas kodu

e2ee: move to per-participant contexts

moves the cryptography bit to a datastructure which allows per-sender keys.
Does not yet use per-sender keys.

Also fixes lots of lint errors in the worker.
master
Philipp Hancke 5 gadus atpakaļ
vecāks
revīzija
b9afcdd6fb
3 mainītis faili ar 250 papildinājumiem un 206 dzēšanām
  1. 3
    6
      JitsiConference.js
  2. 4
    4
      modules/e2ee/E2EEContext.js
  3. 243
    196
      modules/e2ee/Worker.js

+ 3
- 6
JitsiConference.js Parādīt failu

@@ -244,10 +244,7 @@ export default function JitsiConference(options) {
244 244
     this.maxFrameHeight = null;
245 245
 
246 246
     if (browser.supportsInsertableStreams()) {
247
-        this._e2eeCtx = new E2EEContext({
248
-            myUserId: this.myUserId(),
249
-            salt: this.options.name
250
-        });
247
+        this._e2eeCtx = new E2EEContext({ salt: this.options.name });
251 248
     }
252 249
 }
253 250
 
@@ -3428,7 +3425,7 @@ JitsiConference.prototype._setupSenderE2EEForTrack = function(session, track) {
3428 3425
     const sender = pc.findSenderForTrack(track.track);
3429 3426
 
3430 3427
     if (sender) {
3431
-        this._e2eeCtx.handleSender(sender, track.getType());
3428
+        this._e2eeCtx.handleSender(sender, track.getType(), track.getParticipantId());
3432 3429
     } else {
3433 3430
         logger.warn(`Could not handle E2EE for local ${track.getType()} track: sender not found`);
3434 3431
     }
@@ -3451,7 +3448,7 @@ JitsiConference.prototype._setupReceiverE2EEForTrack = function(track) {
3451 3448
         const receiver = pc.findReceiverForTrack(track.track);
3452 3449
 
3453 3450
         if (receiver) {
3454
-            this._e2eeCtx.handleReceiver(receiver, track.getType(), track.id());
3451
+            this._e2eeCtx.handleReceiver(receiver, track.getType(), track.getParticipantId());
3455 3452
         } else {
3456 3453
             logger.warn(`Could not handle E2EE for remote ${track.getType()} track: receiver not found`);
3457 3454
         }

+ 4
- 4
modules/e2ee/E2EEContext.js Parādīt failu

@@ -32,7 +32,6 @@ export default class E2EEcontext {
32 32
      *        https://developer.mozilla.org/en-US/docs/Web/API/Pbkdf2Params
33 33
      *      this is easily available and the same for all participants.
34 34
      *      We currently do not enforce a minimum length of 16 bytes either.
35
-     * @param {string} options.myUserId- local client id. This is the local MUC resourcepart.
36 35
      */
37 36
     constructor(options) {
38 37
         this._options = options;
@@ -59,7 +58,7 @@ export default class E2EEcontext {
59 58
      *
60 59
      * @param {RTCRtpReceiver} receiver - The receiver which will get the decoding function injected.
61 60
      * @param {string} kind - The kind of track this receiver belongs to.
62
-     * @param {string} participantId - The (muc participant) id that this receiver belongs to.
61
+     * @param {string} participantId - The participant id that this receiver belongs to.
63 62
      */
64 63
     handleReceiver(receiver, kind, participantId) {
65 64
         if (receiver[kJitsiE2EE]) {
@@ -84,8 +83,9 @@ export default class E2EEcontext {
84 83
      *
85 84
      * @param {RTCRtpSender} sender - The sender which will get the encoding function injected.
86 85
      * @param {string} kind - The kind of track this sender belongs to.
86
+     * @param {string} participantId - The participant id that this sender belongs to.
87 87
      */
88
-    handleSender(sender, kind) {
88
+    handleSender(sender, kind, participantId) {
89 89
         if (sender[kJitsiE2EE]) {
90 90
             return;
91 91
         }
@@ -98,7 +98,7 @@ export default class E2EEcontext {
98 98
             operation: 'encode',
99 99
             readableStream: senderStreams.readableStream,
100 100
             writableStream: senderStreams.writableStream,
101
-            participantId: this._options.myUserId
101
+            participantId
102 102
         }, [ senderStreams.readableStream, senderStreams.writableStream ]);
103 103
     }
104 104
 

+ 243
- 196
modules/e2ee/Worker.js Parādīt failu

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

Notiek ielāde…
Atcelt
Saglabāt