Sfoglia il codice sorgente

task(e2ee): switch back to GCM

dev1
tmoldovan8x8 4 anni fa
parent
commit
e354745db5
Nessun account collegato all'indirizzo email del committer
4 ha cambiato i file con 205 aggiunte e 218 eliminazioni
  1. 32
    19
      doc/e2ee.md
  2. 165
    173
      modules/e2ee/Context.js
  3. 6
    7
      modules/e2ee/Context.spec.js
  4. 2
    19
      modules/e2ee/crypto-utils.js

+ 32
- 19
doc/e2ee.md Vedi File

@@ -39,28 +39,41 @@ the set of keys when we find a valid signature which avoids a denial of service
39 39
 
40 40
 We are using a variant of [SFrame](https://tools.ietf.org/html/draft-omara-sframe-00)
41 41
 that uses a trailer instead of a header. We call it JFrame.
42
+`
43
+These transformations use AES-GCM (with a 128 bit key; we could have used
44
+256 bits but since the keys are short-lived decided against it) and the
45
+webcrypto API:
46
+  https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt
47
+
48
+AES-GCM needs a 96 bit initialization vector which we construct
49
+based on the SSRC, the rtp timestamp and a frame counter which is similar to
50
+how the IV is constructed in SRTP with GCM
51
+  https://tools.ietf.org/html/rfc7714#section-8.1
52
+
53
+This IV gets sent along with the packet, adding 12 bytes of overhead. The GCM
54
+tag length is the default 128 bits or 16 bytes. For video this overhead is ok but
55
+for audio (where the opus frames are much, much smaller) we are considering shorter
56
+authentication tags.
42 57
 
43 58
 At a high level the encrypted frame format looks like this:
44 59
 ```
45
-     +------------+------------------------------------------+^+
46
-     |unencrypted payload header (variable length)           | |
47
-   +^+------------+------------------------------------------+ |
48
-   | |                                                       | |
49
-   | |                                                       | |
50
-   | |                                                       | |
51
-   | |                                                       | |
52
-   | |                  Encrypted Frame                      | |
53
-   | |                                                       | |
54
-   | |                                                       | |
55
-   | |                                                       | |
56
-   | |                                                       | |
57
-   +^+-------------------------------------------------------+ +
58
-   | |                 Authentication Tag                    | |
59
-   | +---------------------------------------+-+-+-+-+-+-+-+-+ |
60
-   | |    CTR... (length=LEN + 1)            |R|LEN  |KID    | |
61
-   | +---------------------------------------+-+-+-+-+-+-+-+-+^|
62
-   |                                                           |
63
-   +----+Encrypted Portion            Authenticated Portion+---+
60
+     +------------+--------------------------------------+^+
61
+     |unencrypted payload header (variable length)       | |
62
+   +^+------------+--------------------------------------+ |
63
+   | |                                                   | |
64
+   | |                                                   | |
65
+   | |                                                   | |
66
+   | |                                                   | |
67
+   | |              Encrypted Frame                      | |
68
+   | |                                                   | |
69
+   | |                                                   | |
70
+   | |                                                   | |
71
+   | |                                                   | |
72
+   | | ---------+-------------------------+-+---------+----
73
+   | | payload  |IV...(length = IV_LENGTH)|R|IV_LENGTH|KID |
74
+   | | ---------+-------------------------+-+---------+----
75
+   |                                                       |
76
+   +--+Encrypted Portion        Authenticated Portion+---+
64 77
 ```
65 78
 
66 79
 We do not encrypt the first few bytes of the packet that form the

+ 165
- 173
modules/e2ee/Context.js Vedi File

@@ -2,7 +2,6 @@
2 2
 /* global BigInt */
3 3
 
4 4
 import { deriveKeys, importKey, ratchet } from './crypto-utils';
5
-import { isArrayEqual } from './utils';
6 5
 
7 6
 // We use a ringbuffer of keys so we can change them and still decode packets that were
8 7
 // encrypted with an old key. We use a size of 16 which corresponds to the four bits
@@ -24,26 +23,12 @@ const UNENCRYPTED_BYTES = {
24 23
     delta: 3,
25 24
     undefined: 1 // frame.type is not set on audio
26 25
 };
26
+const ENCRYPTION_ALGORITHM = 'AES-GCM';
27 27
 
28
-// Use truncated SHA-256 hashes, 80 bіts for video, 32 bits for audio.
29
-// This follows the same principles as DTLS-SRTP.
30
-const AUTHENTICATIONTAG_OPTIONS = {
31
-    name: 'HMAC',
32
-    hash: 'SHA-256'
33
-};
34
-const ENCRYPTION_ALGORITHM = 'AES-CTR';
35
-
36
-// https://developer.mozilla.org/en-US/docs/Web/API/AesCtrParams
37
-const CTR_LENGTH = 64;
38
-
39
-const DIGEST_LENGTH = {
40
-    key: 10,
41
-    delta: 10,
42
-    undefined: 4 // frame.type is not set on audio
43
-};
28
+/* We use a 96 bit IV for AES GCM. This is signalled in plain together with the
29
+ packet. See https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams */
30
+const IV_LENGTH = 12;
44 31
 
45
-// Maximum number of forward ratchets to attempt when the authentication
46
-// tag on a remote packet does not match the current key.
47 32
 const RATCHET_WINDOW_SIZE = 8;
48 33
 
49 34
 /**
@@ -61,10 +46,7 @@ export class Context {
61 46
         // A pointer to the currently used key.
62 47
         this._currentKeyIndex = -1;
63 48
 
64
-        // A per-sender counter that is used create the AES CTR.
65
-        // Must be incremented on every frame that is sent, can be reset on
66
-        // key changes.
67
-        this._sendCount = BigInt(0); // eslint-disable-line new-cap
49
+        this._sendCounts = new Map();
68 50
 
69 51
         this._id = id;
70 52
     }
@@ -111,96 +93,68 @@ export class Context {
111 93
      * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
112 94
      * @param {TransformStreamDefaultController} controller - TransportStreamController.
113 95
      *
114
-     * The packet format is a variant of
115
-     *   https://tools.ietf.org/html/draft-omara-sframe-00
116
-     * using a trailer instead of a header. One of the design goals was to not require
117
-     * changes to the SFU which for video requires not encrypting the keyframe bit of VP8
118
-     * as SFUs need to detect a keyframe (framemarking or the generic frame descriptor will
119
-     * solve this eventually). This also "hides" that a client is using E2EE a bit.
120
-     *
121
-     * Note that this operates on the full frame, i.e. for VP8 the data described in
122
-     *   https://tools.ietf.org/html/rfc6386#section-9.1
123
-     *
124 96
      * The VP8 payload descriptor described in
125
-     *   https://tools.ietf.org/html/rfc7741#section-4.2
126
-     * is part of the RTP packet and not part of the encoded frame and is therefore not
127
-     * controllable by us. This is fine as the SFU keeps having access to it for routing.
97
+     * https://tools.ietf.org/html/rfc7741#section-4.2
98
+     * is part of the RTP packet and not part of the frame and is not controllable by us.
99
+     * This is fine as the SFU keeps having access to it for routing.
100
+     *
101
+     * The encrypted frame is formed as follows:
102
+     * 1) Leave the first (10, 3, 1) bytes unencrypted, depending on the frame type and kind.
103
+     * 2) Form the GCM IV for the frame as described above.
104
+     * 3) Encrypt the rest of the frame using AES-GCM.
105
+     * 4) Allocate space for the encrypted frame.
106
+     * 5) Copy the unencrypted bytes to the start of the encrypted frame.
107
+     * 6) Append the ciphertext to the encrypted frame.
108
+     * 7) Append the IV.
109
+     * 8) Append a single byte for the key identifier.
110
+     * 9) Enqueue the encrypted frame for sending.
128 111
      */
129 112
     encodeFunction(encodedFrame, controller) {
130 113
         const keyIndex = this._currentKeyIndex;
131 114
 
132 115
         if (this._cryptoKeyRing[keyIndex]) {
133
-            this._sendCount++;
116
+            const iv = this._makeIV(encodedFrame.getMetadata().synchronizationSource, encodedFrame.timestamp);
134 117
 
135 118
             // Thіs is not encrypted and contains the VP8 payload descriptor or the Opus TOC byte.
136 119
             const frameHeader = new Uint8Array(encodedFrame.data, 0, UNENCRYPTED_BYTES[encodedFrame.type]);
137 120
 
121
+            // Frame trailer contains the R|IV_LENGTH and key index
122
+            const frameTrailer = new Uint8Array(2);
123
+
124
+            frameTrailer[0] = IV_LENGTH;
125
+            frameTrailer[1] = keyIndex;
126
+
138 127
             // Construct frame trailer. Similar to the frame header described in
139 128
             // https://tools.ietf.org/html/draft-omara-sframe-00#section-4.2
140 129
             // but we put it at the end.
141
-            //                                             0 1 2 3 4 5 6 7
142
-            // ---------+---------------------------------+-+-+-+-+-+-+-+-+
143
-            // payload  |    CTR... (length=LEN)          |S|LEN  |KID    |
144
-            // ---------+---------------------------------+-+-+-+-+-+-+-+-+
145
-            const counter = new Uint8Array(16);
146
-            const counterView = new DataView(counter.buffer);
147
-
148
-            // The counter is encoded as a variable-length field.
149
-            counterView.setBigUint64(8, this._sendCount);
150
-            let counterLength = 8;
151
-
152
-            for (let i = 8; i < counter.byteLength; i++ && counterLength--) {
153
-                if (counterView.getUint8(i) !== 0) {
154
-                    break;
155
-                }
156
-            }
157
-
158
-            const frameTrailer = new Uint8Array(counterLength + 1);
159
-
160
-            frameTrailer.set(new Uint8Array(counter.buffer, counter.byteLength - counterLength));
161
-
162
-            // Since we never send a counter of 0 we send counterLength - 1 on the wire.
163
-            // This is different from the sframe draft, increases the key space and lets us
164
-            // ignore the case of a zero-length counter at the receiver.
165
-            frameTrailer[frameTrailer.byteLength - 1] = keyIndex | ((counterLength - 1) << 4);
166
-
167
-            // XOR the counter with the saltKey to construct the AES CTR.
168
-            const saltKey = new DataView(this._cryptoKeyRing[keyIndex].saltKey);
169
-
170
-            for (let i = 0; i < counter.byteLength; i++) {
171
-                counterView.setUint8(i, counterView.getUint8(i) ^ saltKey.getUint8(i));
172
-            }
130
+            //
131
+            // ---------+-------------------------+-+---------+----
132
+            // payload  |IV...(length = IV_LENGTH)|R|IV_LENGTH|KID |
133
+            // ---------+-------------------------+-+---------+----
173 134
 
174 135
             return crypto.subtle.encrypt({
175 136
                 name: ENCRYPTION_ALGORITHM,
176
-                counter,
177
-                length: CTR_LENGTH
137
+                iv,
138
+                additionalData: new Uint8Array(encodedFrame.data, 0, frameHeader.byteLength)
178 139
             }, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
179 140
                 UNENCRYPTED_BYTES[encodedFrame.type]))
180 141
             .then(cipherText => {
181 142
                 const newData = new ArrayBuffer(frameHeader.byteLength + cipherText.byteLength
182
-                    + DIGEST_LENGTH[encodedFrame.type] + frameTrailer.byteLength);
143
+                    + iv.byteLength + frameTrailer.byteLength);
183 144
                 const newUint8 = new Uint8Array(newData);
184 145
 
185 146
                 newUint8.set(frameHeader); // copy first bytes.
186
-                newUint8.set(new Uint8Array(cipherText), UNENCRYPTED_BYTES[encodedFrame.type]); // add ciphertext.
187
-                // Leave some space for the authentication tag. This is filled with 0s initially, similar to
188
-                // STUN message-integrity described in https://tools.ietf.org/html/rfc5389#section-15.4
189
-                newUint8.set(frameTrailer, frameHeader.byteLength + cipherText.byteLength
190
-                    + DIGEST_LENGTH[encodedFrame.type]); // append trailer.
191
-
192
-                return crypto.subtle.sign(AUTHENTICATIONTAG_OPTIONS, this._cryptoKeyRing[keyIndex].authenticationKey,
193
-                    new Uint8Array(newData)).then(async authTag => {
194
-                    const truncatedAuthTag = new Uint8Array(authTag, 0, DIGEST_LENGTH[encodedFrame.type]);
195
-
147
+                newUint8.set(
148
+                    new Uint8Array(cipherText), frameHeader.byteLength); // add ciphertext.
149
+                newUint8.set(
150
+                    new Uint8Array(iv), frameHeader.byteLength + cipherText.byteLength); // append IV.
151
+                newUint8.set(
152
+                        frameTrailer,
153
+                        frameHeader.byteLength + cipherText.byteLength + iv.byteLength); // append frame trailer.
196 154
 
197
-                    // Set the truncated authentication tag.
198
-                    newUint8.set(truncatedAuthTag, UNENCRYPTED_BYTES[encodedFrame.type] + cipherText.byteLength);
199
-
200
-                    encodedFrame.data = newData;
155
+                encodedFrame.data = newData;
201 156
 
202
-                    return controller.enqueue(encodedFrame);
203
-                });
157
+                return controller.enqueue(encodedFrame);
204 158
             }, e => {
205 159
                 // TODO: surface this to the app.
206 160
                 console.error(e);
@@ -224,109 +178,147 @@ export class Context {
224 178
      */
225 179
     async decodeFunction(encodedFrame, controller) {
226 180
         const data = new Uint8Array(encodedFrame.data);
227
-        const keyIndex = data[encodedFrame.data.byteLength - 1] & 0xf; // lower four bits.
181
+        const keyIndex = data[encodedFrame.data.byteLength - 1];
182
+
183
+        if (this._cryptoKeyRing[keyIndex]) {
184
+
185
+            const decodedFrame = await this._decryptFrame(
186
+                encodedFrame,
187
+                keyIndex);
188
+
189
+            return controller.enqueue(decodedFrame);
190
+        }
191
+
192
+        // TODO: this just passes through to the decoder. Is that ok? If we don't know the key yet
193
+        // we might want to buffer a bit but it is still unclear how to do that (and for how long etc).
194
+        controller.enqueue(encodedFrame);
195
+    }
228 196
 
229
-        if (this._cryptoKeyRing[this._currentKeyIndex] && this._cryptoKeyRing[keyIndex]) {
230
-            const counterLength = 1 + ((data[encodedFrame.data.byteLength - 1] >> 4) & 0x7);
197
+    /**
198
+     * Function that will decrypt the given encoded frame. If the decryption fails, it will
199
+     * ratchet the key for up to RATCHET_WINDOW_SIZE times.
200
+     *
201
+     * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
202
+     * @param {number} keyIndex - the index of the decryption data in _cryptoKeyRing array.
203
+     * @param {number} ratchetCount - the number of retries after ratcheting the key.
204
+     * @returns {RTCEncodedVideoFrame|RTCEncodedAudioFrame} - The decrypted frame.
205
+     * @private
206
+     */
207
+    async _decryptFrame(
208
+            encodedFrame,
209
+            keyIndex,
210
+            ratchetCount = 0) {
211
+
212
+        const { encryptionKey } = this._cryptoKeyRing[keyIndex];
213
+        let { material } = this._cryptoKeyRing[keyIndex];
214
+
215
+        // Construct frame trailer. Similar to the frame header described in
216
+        // https://tools.ietf.org/html/draft-omara-sframe-00#section-4.2
217
+        // but we put it at the end.
218
+        //
219
+        // ---------+-------------------------+-+---------+----
220
+        // payload  |IV...(length = IV_LENGTH)|R|IV_LENGTH|KID |
221
+        // ---------+-------------------------+-+---------+----
222
+
223
+        try {
231 224
             const frameHeader = new Uint8Array(encodedFrame.data, 0, UNENCRYPTED_BYTES[encodedFrame.type]);
225
+            const frameTrailer = new Uint8Array(encodedFrame.data, encodedFrame.data.byteLength - 2, 2);
232 226
 
233
-            // Extract the truncated authentication tag.
234
-            const authTagOffset = encodedFrame.data.byteLength - (DIGEST_LENGTH[encodedFrame.type]
235
-                + counterLength + 1);
236
-            const authTag = encodedFrame.data.slice(authTagOffset, authTagOffset
237
-                + DIGEST_LENGTH[encodedFrame.type]);
238
-
239
-            // Set authentication tag bytes to 0.
240
-            data.set(new Uint8Array(DIGEST_LENGTH[encodedFrame.type]), encodedFrame.data.byteLength
241
-                - (DIGEST_LENGTH[encodedFrame.type] + counterLength + 1));
242
-
243
-            // Do truncated hash comparison of the authentication tag.
244
-            // If the hash does not match we might have to advance the ratchet a limited number
245
-            // of times. See (even though the description there is odd)
246
-            // https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.5.1
247
-            let { authenticationKey, material } = this._cryptoKeyRing[keyIndex];
248
-            let validAuthTag = false;
249
-            let newKeys = null;
250
-
251
-            for (let distance = 0; distance < RATCHET_WINDOW_SIZE; distance++) {
252
-                const calculatedTag = await crypto.subtle.sign(AUTHENTICATIONTAG_OPTIONS,
253
-                    authenticationKey, encodedFrame.data);
254
-
255
-                if (isArrayEqual(new Uint8Array(authTag),
256
-                        new Uint8Array(calculatedTag.slice(0, DIGEST_LENGTH[encodedFrame.type])))) {
257
-                    validAuthTag = true;
258
-                    if (distance > 0) {
259
-                        this._setKeys(newKeys, keyIndex);
260
-                    }
261
-                    break;
262
-                }
263
-
264
-                // Attempt to ratchet and generate the next set of keys.
265
-                material = await importKey(await ratchet(material));
266
-                newKeys = await deriveKeys(material);
267
-                authenticationKey = newKeys.authenticationKey;
268
-            }
227
+            const ivLength = frameTrailer[0];
228
+            const iv = new Uint8Array(
229
+                encodedFrame.data,
230
+                encodedFrame.data.byteLength - ivLength - frameTrailer.byteLength,
231
+                ivLength);
269 232
 
270
-            // Check whether we found a valid authentication tag.
271
-            if (!validAuthTag) {
272
-                // TODO: return an error to the app.
233
+            const cipherTextStart = frameHeader.byteLength;
234
+            const cipherTextLength = encodedFrame.data.byteLength
235
+                    - (frameHeader.byteLength + ivLength + frameTrailer.byteLength);
273 236
 
274
-                console.error('Authentication tag mismatch');
237
+            const plainText = await crypto.subtle.decrypt({
238
+                name: 'AES-GCM',
239
+                iv,
240
+                additionalData: new Uint8Array(encodedFrame.data, 0, frameHeader.byteLength)
241
+            },
242
+                encryptionKey,
243
+                new Uint8Array(encodedFrame.data, cipherTextStart, cipherTextLength));
275 244
 
276
-                return;
277
-            }
245
+            const newData = new ArrayBuffer(frameHeader.byteLength + plainText.byteLength);
246
+            const newUint8 = new Uint8Array(newData);
247
+
248
+            newUint8.set(new Uint8Array(encodedFrame.data, 0, frameHeader.byteLength));
249
+            newUint8.set(new Uint8Array(plainText), frameHeader.byteLength);
278 250
 
279
-            // Extract the counter.
280
-            const counter = new Uint8Array(16);
251
+            encodedFrame.data = newData;
252
+        } catch (error) {
253
+            console.error(error);
254
+
255
+            if (ratchetCount < RATCHET_WINDOW_SIZE) {
256
+                material = await importKey(await ratchet(material));
281 257
 
282
-            counter.set(data.slice(encodedFrame.data.byteLength - (counterLength + 1),
283
-                encodedFrame.data.byteLength - 1), 16 - counterLength);
284
-            const counterView = new DataView(counter.buffer);
258
+                const newKey = await deriveKeys(material);
285 259
 
286
-            // XOR the counter with the saltKey to construct the AES CTR.
287
-            const saltKey = new DataView(this._cryptoKeyRing[keyIndex].saltKey);
260
+                this._setKeys(newKey);
288 261
 
289
-            for (let i = 0; i < counter.byteLength; i++) {
290
-                counterView.setUint8(i,
291
-                    counterView.getUint8(i) ^ saltKey.getUint8(i));
262
+                return await this._decryptFrame(
263
+                    encodedFrame,
264
+                    keyIndex,
265
+                    ratchetCount + 1);
292 266
             }
293 267
 
294
-            return crypto.subtle.decrypt({
295
-                name: ENCRYPTION_ALGORITHM,
296
-                counter,
297
-                length: CTR_LENGTH
298
-            }, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
299
-                    UNENCRYPTED_BYTES[encodedFrame.type],
300
-                    encodedFrame.data.byteLength - (UNENCRYPTED_BYTES[encodedFrame.type]
301
-                    + DIGEST_LENGTH[encodedFrame.type] + counterLength + 1))
302
-            ).then(plainText => {
303
-                const newData = new ArrayBuffer(UNENCRYPTED_BYTES[encodedFrame.type] + plainText.byteLength);
268
+            // TODO: notify the application about error status.
269
+
270
+            // TODO: For video we need a better strategy since we do not want to based any
271
+            // non-error frames on a garbage keyframe.
272
+            if (encodedFrame.type === undefined) { // audio, replace with silence.
273
+                const newData = new ArrayBuffer(3);
304 274
                 const newUint8 = new Uint8Array(newData);
305 275
 
306
-                newUint8.set(frameHeader);
307
-                newUint8.set(new Uint8Array(plainText), UNENCRYPTED_BYTES[encodedFrame.type]);
276
+                newUint8.set([ 0xd8, 0xff, 0xfe ]); // opus silence frame.
308 277
                 encodedFrame.data = newData;
278
+            }
279
+        }
309 280
 
310
-                return controller.enqueue(encodedFrame);
311
-            }, e => {
312
-                console.error(e);
281
+        return encodedFrame;
282
+    }
313 283
 
314
-                // TODO: notify the application about error status.
315
-                // TODO: For video we need a better strategy since we do not want to based any
316
-                // non-error frames on a garbage keyframe.
317
-                if (encodedFrame.type === undefined) { // audio, replace with silence.
318
-                    const newData = new ArrayBuffer(3);
319
-                    const newUint8 = new Uint8Array(newData);
320
-
321
-                    newUint8.set([ 0xd8, 0xff, 0xfe ]); // opus silence frame.
322
-                    encodedFrame.data = newData;
323
-                    controller.enqueue(encodedFrame);
324
-                }
325
-            });
284
+
285
+    /**
286
+     * Construct the IV used for AES-GCM and sent (in plain) with the packet similar to
287
+     * https://tools.ietf.org/html/rfc7714#section-8.1
288
+     * It concatenates
289
+     * - the 32 bit synchronization source (SSRC) given on the encoded frame,
290
+     * - the 32 bit rtp timestamp given on the encoded frame,
291
+     * - a send counter that is specific to the SSRC. Starts at a random number.
292
+     * The send counter is essentially the pictureId but we currently have to implement this ourselves.
293
+     * There is no XOR with a salt. Note that this IV leaks the SSRC to the receiver but since this is
294
+     * randomly generated and SFUs may not rewrite this is considered acceptable.
295
+     * The SSRC is used to allow demultiplexing multiple streams with the same key, as described in
296
+     *   https://tools.ietf.org/html/rfc3711#section-4.1.1
297
+     * The RTP timestamp is 32 bits and advances by the codec clock rate (90khz for video, 48khz for
298
+     * opus audio) every second. For video it rolls over roughly every 13 hours.
299
+     * The send counter will advance at the frame rate (30fps for video, 50fps for 20ms opus audio)
300
+     * every second. It will take a long time to roll over.
301
+     *
302
+     * See also https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams
303
+     */
304
+    _makeIV(synchronizationSource, timestamp) {
305
+        const iv = new ArrayBuffer(IV_LENGTH);
306
+        const ivView = new DataView(iv);
307
+
308
+        // having to keep our own send count (similar to a picture id) is not ideal.
309
+        if (!this._sendCounts.has(synchronizationSource)) {
310
+            // Initialize with a random offset, similar to the RTP sequence number.
311
+            this._sendCounts.set(synchronizationSource, Math.floor(Math.random() * 0xFFFF));
326 312
         }
327 313
 
328
-        // TODO: this just passes through to the decoder. Is that ok? If we don't know the key yet
329
-        // we might want to buffer a bit but it is still unclear how to do that (and for how long etc).
330
-        controller.enqueue(encodedFrame);
314
+        const sendCount = this._sendCounts.get(synchronizationSource);
315
+
316
+        ivView.setUint32(0, synchronizationSource);
317
+        ivView.setUint32(4, timestamp);
318
+        ivView.setUint32(8, sendCount % 0xFFFF);
319
+
320
+        this._sendCounts.set(synchronizationSource, sendCount + 1);
321
+
322
+        return iv;
331 323
     }
332 324
 }

+ 6
- 7
modules/e2ee/Context.spec.js Vedi File

@@ -81,9 +81,9 @@ describe('E2EE Context', () => {
81 81
                 enqueue: encodedFrame => {
82 82
                     const data = new Uint8Array(encodedFrame.data);
83 83
 
84
-                    // An audio frame will have an overhead of 6 bytes with this counter and key size:
85
-                    //   4 bytes truncated signature, counter (1 byte) and 1 byte trailer.
86
-                    expect(data.byteLength).toEqual(audioBytes.length + 6);
84
+                    // An audio frame will have an overhead of 30 bytes and key size:
85
+                    // 16 bytes authentication tag, 12 bytes iv, iv length (1 byte) and 1 byte key index.
86
+                    expect(data.byteLength).toEqual(audioBytes.length + 30);
87 87
 
88 88
                     // TODO: provide test vector.
89 89
                     done();
@@ -98,10 +98,9 @@ describe('E2EE Context', () => {
98 98
                 enqueue: encodedFrame => {
99 99
                     const data = new Uint8Array(encodedFrame.data);
100 100
 
101
-                    // A video frame will have an overhead of 12 bytes with this counter and key size:
102
-                    //   10 bytes signature, counter (1 byte) and 1 byte trailer.
103
-
104
-                    expect(data.byteLength).toEqual(videoBytes.length + 12);
101
+                    // A video frame will have an overhead of 30 bytes and key size:
102
+                    // 16 bytes authentication tag, 12 bytes iv, iv length (1 byte) and 1 byte key index.
103
+                    expect(data.byteLength).toEqual(videoBytes.length + 30);
105 104
 
106 105
                     // TODO: provide test vector.
107 106
                     done();

+ 2
- 19
modules/e2ee/crypto-utils.js Vedi File

@@ -16,30 +16,13 @@ export async function deriveKeys(material) {
16 16
         hash: 'SHA-256',
17 17
         info
18 18
     }, material, {
19
-        name: 'AES-CTR',
19
+        name: 'AES-GCM',
20 20
         length: 128
21 21
     }, false, [ 'encrypt', 'decrypt' ]);
22
-    const authenticationKey = await crypto.subtle.deriveKey({
23
-        name: 'HKDF',
24
-        salt: textEncoder.encode('JFrameAuthenticationKey'),
25
-        hash: 'SHA-256',
26
-        info
27
-    }, material, {
28
-        name: 'HMAC',
29
-        hash: 'SHA-256'
30
-    }, false, [ 'sign' ]);
31
-    const saltKey = await crypto.subtle.deriveBits({
32
-        name: 'HKDF',
33
-        salt: textEncoder.encode('JFrameSaltKey'),
34
-        hash: 'SHA-256',
35
-        info
36
-    }, material, 128);
37 22
 
38 23
     return {
39 24
         material,
40
-        encryptionKey,
41
-        authenticationKey,
42
-        saltKey
25
+        encryptionKey
43 26
     };
44 27
 }
45 28
 

Loading…
Annulla
Salva