Browse Source

e2ee: use CTR instead of GCM

following
  https://tools.ietf.org/html/draft-omara-sframe-00
but putting the frame metadata into a trailer instead of a header.
We call this JFrame.

Also the key we get from OLM is high entropy so we do not need
to use PBKDF2 but can use HKDF instead. See
  https://wiki.developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#HKDF
dev1
Philipp Hancke 5 years ago
parent
commit
85fde1aeae
3 changed files with 244 additions and 210 deletions
  1. 30
    33
      doc/e2ee.md
  2. 0
    9
      modules/e2ee/E2EEContext.js
  3. 214
    168
      modules/e2ee/Worker.js

+ 30
- 33
doc/e2ee.md View File

4
 This document describes some of the high-level concepts and outlines the design.
4
 This document describes some of the high-level concepts and outlines the design.
5
 Please refer to the source code for details.
5
 Please refer to the source code for details.
6
 
6
 
7
-## Deriving the key from the e2eekey url hash
8
-We take the key from the url hash.  Unlike query parameters this does not get
9
-sent to the server so it is the right place for it. We use
10
-the window.location.onhashchange event to listen for changes in the e2ee
11
-key property.
12
-
13
-It is important to note that this key should not get exchanged via the server.
14
-There needs to be some other means of exchanging it.
15
-
16
-From this key we derive a 128bit key using PBKDF2. We use the room name as a salt in this key generation. This is a bit weak but we need to start with information that is the same for all participants so we can not yet use a proper random salt.
17
-We add the participant id to the salt when deriving the key which allows us to use per-sender keys. This is done to prepare the ground for the actual architecture and does not change the cryptographic properties.
18
-
19
-We plan to rotate the key whenever a participant joins or leaves. However, we need end-to-end encrypted signaling to exchange those keys so we are not doing this yet.
20
-
21
-## The encrypted frame
22
-The derived key is used in the transformations of the Insertable Streams API.
23
-These transformations use AES-GCM (with a 128 bit key; we could have used
24
-256 bits but since the keys are short-lived decided against it) and the
25
-webcrypto API:
26
-  https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt
27
-
28
-AES-GCM needs a 96 bit initialization vector which we construct
29
-based on the SSRC, the rtp timestamp and a frame counter which is similar to
30
-how the IV is constructed in SRTP with GCM
31
-  https://tools.ietf.org/html/rfc7714#section-8.1
32
-
33
-This IV gets sent along with the packet, adding 12 bytes of overhead. The GCM
34
-tag length is the default 128 bits or 16 bytes. For video this overhead is ok but
35
-for audio (where the opus frames are much, much smaller) we are considering shorter
36
-authentication tags.
7
+## Packet format
8
+We are using a variant of
9
+  https://tools.ietf.org/html/draft-omara-sframe-00
10
+that uses a trailer instead of a header. We call it JFrame.
11
+
12
+At a high level the encrypted frame format looks like this:
13
+```
14
+     +------------+------------------------------------------+^+
15
+     |unencrypted payload header (variable length)           | |
16
+   +^+------------+------------------------------------------+ |
17
+   | |                                                       | |
18
+   | |                                                       | |
19
+   | |                                                       | |
20
+   | |                                                       | |
21
+   | |                  Encrypted Frame                      | |
22
+   | |                                                       | |
23
+   | |                                                       | |
24
+   | |                                                       | |
25
+   | |                                                       | |
26
+   +^+-------------------------------------------------------+ +
27
+   | |                 Authentication Tag                    | |
28
+   | +---------------------------------------+-+-+-+-+-+-+-+-+ |
29
+   | |    CTR... (length=LEN + 1)            |S|LEN  |0| KID | |
30
+   | +---------------------------------------+-+-+-+-+-+-+-+-+^|
31
+   |                                                           |
32
+   +----+Encrypted Portion            Authenticated Portion+---+
33
+```
37
 
34
 
38
 We do not encrypt the first few bytes of the packet that form the VP8 payload
35
 We do not encrypt the first few bytes of the packet that form the VP8 payload
39
   https://tools.ietf.org/html/rfc6386#section-9.1
36
   https://tools.ietf.org/html/rfc6386#section-9.1
40
-nor the Opus TOC byte
37
+(10 bytes for key frames, 3 bytes for interframes) nor the Opus TOC byte
41
   https://tools.ietf.org/html/rfc6716#section-3.1
38
   https://tools.ietf.org/html/rfc6716#section-3.1
42
-
43
-This allows the decoder to understand the frame a bit more and makes it generate the fun looking garbage we see in the video.
39
+This allows the decoder to understand the frame a bit more and makes it decode the fun looking garbage we see in the video.
44
 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.
40
 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
 
41
 
42
+
46
 ## Using workers
43
 ## Using workers
47
 
44
 
48
 Insertable Streams are transferable and can be sent from the main javascript context to a Worker
45
 Insertable Streams are transferable and can be sent from the main javascript context to a Worker

+ 0
- 9
modules/e2ee/E2EEContext.js View File

54
 
54
 
55
         this._worker = new Worker(blobUrl, { name: 'E2EE Worker' });
55
         this._worker = new Worker(blobUrl, { name: 'E2EE Worker' });
56
         this._worker.onerror = e => logger.onerror(e);
56
         this._worker.onerror = e => logger.onerror(e);
57
-
58
-        // Initialize the salt and convert it once.
59
-        const encoder = new TextEncoder();
60
-
61
-        // Send initial options to worker.
62
-        this._worker.postMessage({
63
-            operation: 'initialize',
64
-            salt: encoder.encode(options.salt)
65
-        });
66
     }
57
     }
67
 
58
 
68
     /**
59
     /**

+ 214
- 168
modules/e2ee/Worker.js View File

1
 /* global TransformStream */
1
 /* global TransformStream */
2
+/* eslint-disable no-bitwise */
2
 
3
 
3
 // Worker for E2EE/Insertable streams.
4
 // Worker for E2EE/Insertable streams.
4
 //
5
 //
21
     controller.enqueue(encodedFrame);
22
     controller.enqueue(encodedFrame);
22
 }
23
 }
23
 
24
 
25
+/**
26
+ * Compares two byteArrays for equality.
27
+ */
28
+function isArrayEqual(a1, a2) {
29
+    if (a1.byteLength !== a2.byteLength) {
30
+        return false;
31
+    }
32
+    for (let i = 0; i < a1.byteLength; i++) {
33
+        if (a1[i] !== a2[i]) {
34
+            return false;
35
+        }
36
+    }
37
+
38
+    return true;
39
+}
40
+
24
 // We use a ringbuffer of keys so we can change them and still decode packets that were
41
 // We use a ringbuffer of keys so we can change them and still decode packets that were
25
 // encrypted with an old key.
42
 // encrypted with an old key.
26
 const keyRingSize = 3;
43
 const keyRingSize = 3;
27
 
44
 
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 ivLength = 12;
31
-
32
-// We use a 128 bit key for AES GCM.
33
-const keyGenParameters = {
34
-    name: 'AES-GCM',
35
-    length: 128
36
-};
37
-
38
 // We copy the first bytes of the VP8 payload unencrypted.
45
 // We copy the first bytes of the VP8 payload unencrypted.
39
 // For keyframes this is 10 bytes, for non-keyframes (delta) 3. See
46
 // For keyframes this is 10 bytes, for non-keyframes (delta) 3. See
40
 //   https://tools.ietf.org/html/rfc6386#section-9.1
47
 //   https://tools.ietf.org/html/rfc6386#section-9.1
51
     undefined: 1 // frame.type is not set on audio
58
     undefined: 1 // frame.type is not set on audio
52
 };
59
 };
53
 
60
 
54
-// Salt used in key derivation
55
-// FIXME: We currently use the MUC room name for this which has the same lifetime
56
-// as this worker. While not (pseudo)random as recommended in
57
-// https://developer.mozilla.org/en-US/docs/Web/API/Pbkdf2Params
58
-// this is easily available and the same for all participants.
59
-// We currently do not enforce a minimum length of 16 bytes either.
60
-let _keySalt;
61
+// Use truncated SHA-256 hashes, 80 bіts for video, 32 bits for audio.
62
+// This follows the same principles as DTLS-SRTP.
63
+const authenticationTagOptions = {
64
+    name: 'HMAC',
65
+    hash: 'SHA-256'
66
+};
67
+const digestLength = {
68
+    key: 10,
69
+    delta: 10,
70
+    undefined: 4 // frame.type is not set on audio
71
+};
61
 
72
 
62
 /**
73
 /**
63
- * Derives a AES-GCM key from the input using PBKDF2
64
- * The key length can be configured above and should be either 128 or 256 bits.
74
+ * Derives a set of keys from the master key.
65
  * @param {Uint8Array} keyBytes - Value to derive key from
75
  * @param {Uint8Array} keyBytes - Value to derive key from
66
  * @param {Uint8Array} salt - Salt used in key derivation
76
  * @param {Uint8Array} salt - Salt used in key derivation
77
+ *
78
+ * See https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.1
67
  */
79
  */
68
-async function deriveKey(keyBytes, salt) {
80
+async function deriveKeys(keyBytes) {
69
     // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
81
     // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
70
     const material = await crypto.subtle.importKey('raw', keyBytes,
82
     const material = await crypto.subtle.importKey('raw', keyBytes,
71
-        'PBKDF2', false, [ 'deriveBits', 'deriveKey' ]);
72
-
73
-    // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#PBKDF2
74
-    return crypto.subtle.deriveKey({
75
-        name: 'PBKDF2',
76
-        salt,
77
-        iterations: 100000,
83
+        'HKDF', false, [ 'deriveBits', 'deriveKey' ]);
84
+
85
+    const info = new ArrayBuffer();
86
+    const textEncoder = new TextEncoder();
87
+
88
+    // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#HKDF
89
+    // https://developer.mozilla.org/en-US/docs/Web/API/HkdfParams
90
+    const encryptionKey = await crypto.subtle.deriveKey({
91
+        name: 'HKDF',
92
+        salt: textEncoder.encode('JFrameEncryptionKey'),
93
+        hash: 'SHA-256',
94
+        info
95
+    }, material, {
96
+        name: 'AES-CTR',
97
+        length: 128
98
+    }, false, [ 'encrypt', 'decrypt' ]);
99
+    const authenticationKey = await crypto.subtle.deriveKey({
100
+        name: 'HKDF',
101
+        salt: textEncoder.encode('JFrameAuthenticationKey'),
102
+        hash: 'SHA-256',
103
+        info
104
+    }, material, {
105
+        name: 'HMAC',
78
         hash: 'SHA-256'
106
         hash: 'SHA-256'
79
-    }, material, keyGenParameters, false, [ 'encrypt', 'decrypt' ]);
107
+    }, false, [ 'sign' ]);
108
+    const saltKey = await crypto.subtle.deriveBits({
109
+        name: 'HKDF',
110
+        salt: textEncoder.encode('JFrameSaltKey'),
111
+        hash: 'SHA-256',
112
+        info
113
+    }, material, 128);
114
+
115
+    return {
116
+        encryptionKey,
117
+        authenticationKey,
118
+        saltKey
119
+    };
80
 }
120
 }
81
 
121
 
82
 
122
 
95
         // A pointer to the currently used key.
135
         // A pointer to the currently used key.
96
         this._currentKeyIndex = -1;
136
         this._currentKeyIndex = -1;
97
 
137
 
98
-        // We keep track of how many frames we have sent per ssrc.
99
-        // Starts with a random offset similar to the RTP sequence number.
100
-        this._sendCounts = new Map();
138
+        // A per-sender counter that is used create the AES CTR.
139
+        // Must be incremented on every frame that is sent, can be reset on
140
+        // key changes.
141
+        this._sendCount = 0n;
101
 
142
 
102
         this._id = id;
143
         this._id = id;
103
     }
144
     }
104
 
145
 
105
     /**
146
     /**
106
-     * Derives a per-participant key.
107
-     * @param {Uint8Array} keyBytes - Value to derive key from
108
-     * @param {Uint8Array} salt - Salt used in key derivation
109
-     */
110
-    async deriveKey(keyBytes, salt) {
111
-        const encoder = new TextEncoder();
112
-        const idBytes = encoder.encode(this._id);
113
-
114
-        // Separate both parts by a null byte to avoid ambiguity attacks.
115
-        const participantSalt = new Uint8Array(salt.byteLength + idBytes.byteLength + 1);
116
-
117
-        participantSalt.set(salt);
118
-        participantSalt.set(idBytes, salt.byteLength + 1);
119
-
120
-        return deriveKey(keyBytes, participantSalt);
121
-    }
122
-
123
-    /**
124
-     * Sets a key and starts using it for encrypting.
147
+     * Sets a key, derives the different subkeys and starts using them for encryption or
148
+     * decryption.
125
      * @param {CryptoKey} key
149
      * @param {CryptoKey} key
126
      * @param {Number} keyIndex
150
      * @param {Number} keyIndex
127
      */
151
      */
128
-    setKey(key, keyIndex) {
152
+    async setKey(key, keyIndex) {
129
         this._currentKeyIndex = keyIndex % this._cryptoKeyRing.length;
153
         this._currentKeyIndex = keyIndex % this._cryptoKeyRing.length;
130
-        this._cryptoKeyRing[this._currentKeyIndex] = key;
131
-    }
132
-
133
-    /**
134
-     * Construct the IV used for AES-GCM and sent (in plain) with the packet similar to
135
-     * https://tools.ietf.org/html/rfc7714#section-8.1
136
-     * It concatenates
137
-     * - the 32 bit synchronization source (SSRC) given on the encoded frame,
138
-     * - the 32 bit rtp timestamp given on the encoded frame,
139
-     * - a send counter that is specific to the SSRC. Starts at a random number.
140
-     * The send counter is essentially the pictureId but we currently have to implement this ourselves.
141
-     * There is no XOR with a salt. Note that this IV leaks the SSRC to the receiver but since this is
142
-     * randomly generated and SFUs may not rewrite this is considered acceptable.
143
-     * The SSRC is used to allow demultiplexing multiple streams with the same key, as described in
144
-     *   https://tools.ietf.org/html/rfc3711#section-4.1.1
145
-     * The RTP timestamp is 32 bits and advances by the codec clock rate (90khz for video, 48khz for
146
-     * opus audio) every second. For video it rolls over roughly every 13 hours.
147
-     * The send counter will advance at the frame rate (30fps for video, 50fps for 20ms opus audio)
148
-     * every second. It will take a long time to roll over.
149
-     *
150
-     * See also https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams
151
-     */
152
-    makeIV(synchronizationSource, timestamp) {
153
-        const iv = new ArrayBuffer(ivLength);
154
-        const ivView = new DataView(iv);
155
-
156
-        // having to keep our own send count (similar to a picture id) is not ideal.
157
-        if (!this._sendCounts.has(synchronizationSource)) {
158
-            // Initialize with a random offset, similar to the RTP sequence number.
159
-            this._sendCounts.set(synchronizationSource, Math.floor(Math.random() * 0xFFFF));
154
+        if (key) {
155
+            this._cryptoKeyRing[this._currentKeyIndex] = await deriveKeys(key);
156
+        } else {
157
+            this._cryptoKeyRing[this._currentKeyIndex] = false;
160
         }
158
         }
161
-        const sendCount = this._sendCounts.get(synchronizationSource);
162
-
163
-        ivView.setUint32(0, synchronizationSource);
164
-        ivView.setUint32(4, timestamp);
165
-        ivView.setUint32(8, sendCount % 0xFFFF);
166
-
167
-        this._sendCounts.set(synchronizationSource, sendCount + 1);
168
-
169
-        return iv;
159
+        this._sendCount = 0n; // Reset the send count (bigint).
170
     }
160
     }
171
 
161
 
172
     /**
162
     /**
175
      * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
165
      * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
176
      * @param {TransformStreamDefaultController} controller - TransportStreamController.
166
      * @param {TransformStreamDefaultController} controller - TransportStreamController.
177
      *
167
      *
178
-     * The packet format is described below. One of the design goals was to not require
168
+     * The packet format is a variant of
169
+     *   https://tools.ietf.org/html/draft-omara-sframe-00
170
+     * using a trailer instead of a header. One of the design goals was to not require
179
      * changes to the SFU which for video requires not encrypting the keyframe bit of VP8
171
      * changes to the SFU which for video requires not encrypting the keyframe bit of VP8
180
      * as SFUs need to detect a keyframe (framemarking or the generic frame descriptor will
172
      * as SFUs need to detect a keyframe (framemarking or the generic frame descriptor will
181
      * solve this eventually). This also "hides" that a client is using E2EE a bit.
173
      * solve this eventually). This also "hides" that a client is using E2EE a bit.
185
      *
177
      *
186
      * The VP8 payload descriptor described in
178
      * The VP8 payload descriptor described in
187
      *   https://tools.ietf.org/html/rfc7741#section-4.2
179
      *   https://tools.ietf.org/html/rfc7741#section-4.2
188
-     * is part of the RTP packet and not part of the frame and is not controllable by us.
189
-     * This is fine as the SFU keeps having access to it for routing.
190
-     *
191
-     * The encrypted frame is formed as follows:
192
-     * 1) Leave the first (10, 3, 1) bytes unencrypted, depending on the frame type and kind.
193
-     * 2) Form the GCM IV for the frame as described above.
194
-     * 3) Encrypt the rest of the frame using AES-GCM.
195
-     * 4) Allocate space for the encrypted frame.
196
-     * 5) Copy the unencrypted bytes to the start of the encrypted frame.
197
-     * 6) Append the ciphertext to the encrypted frame.
198
-     * 7) Append the IV.
199
-     * 8) Append a single byte for the key identifier. TODO: we don't need all the bits.
200
-     * 9) Enqueue the encrypted frame for sending.
180
+     * is part of the RTP packet and not part of the encoded frame and is therefore not
181
+     * controllable by us. This is fine as the SFU keeps having access to it for routing.
201
      */
182
      */
202
     encodeFunction(encodedFrame, controller) {
183
     encodeFunction(encodedFrame, controller) {
203
         const keyIndex = this._currentKeyIndex;
184
         const keyIndex = this._currentKeyIndex;
204
 
185
 
205
         if (this._cryptoKeyRing[keyIndex]) {
186
         if (this._cryptoKeyRing[keyIndex]) {
206
-            const iv = this.makeIV(encodedFrame.getMetadata().synchronizationSource, encodedFrame.timestamp);
187
+            this._sendCount++;
188
+
189
+            // Thіs is not encrypted and contains the VP8 payload descriptor or the Opus TOC byte.
190
+            const frameHeader = new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type]);
191
+
192
+            // Construct frame trailer. Similar to the frame header described in
193
+            // https://tools.ietf.org/html/draft-omara-sframe-00#section-4.2
194
+            // but we put it at the end.
195
+            //                                             0 1 2 3 4 5 6 7
196
+            // ---------+---------------------------------+-+-+-+-+-+-+-+-+
197
+            // payload  |    CTR... (length=LEN)          |S|LEN  |0| KID |
198
+            // ---------+---------------------------------+-+-+-+-+-+-+-+-+
199
+            const counter = new Uint8Array(16);
200
+            const counterView = new DataView(counter.buffer);
201
+
202
+            // The counter is encoded as a variable-length field.
203
+            counterView.setBigUint64(8, this._sendCount);
204
+            let counterLength = 8;
205
+
206
+            for (let i = 8; i < counter.byteLength; i++ && counterLength--) {
207
+                if (counterView.getUint8(i) !== 0) {
208
+                    break;
209
+                }
210
+            }
211
+
212
+            const frameTrailer = new Uint8Array(counterLength + 1);
213
+
214
+            frameTrailer.set(new Uint8Array(counter.buffer, counter.byteLength - counterLength));
215
+
216
+            // Since we never send a counter of 0 we send counterLength - 1 on the wire.
217
+            // This is different from the sframe draft, increases the key space and lets us
218
+            // ignore the case of a zero-length counter at the receiver.
219
+            frameTrailer[frameTrailer.byteLength - 1] = keyIndex | ((counterLength - 1) << 4);
220
+
221
+            // XOR the counter with the saltKey to construct the AES CTR.
222
+            const saltKey = new DataView(this._cryptoKeyRing[keyIndex].saltKey);
223
+
224
+            for (let i = 0; i < counter.byteLength; i++) {
225
+                counterView.setUint8(i, counterView.getUint8(i) ^ saltKey.getUint8(i));
226
+            }
207
 
227
 
208
             return crypto.subtle.encrypt({
228
             return crypto.subtle.encrypt({
209
-                name: 'AES-GCM',
210
-                iv,
211
-                additionalData: new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type])
212
-            }, this._cryptoKeyRing[keyIndex], new Uint8Array(encodedFrame.data,
229
+                name: 'AES-CTR',
230
+                counter,
231
+                length: 64
232
+            }, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
213
                 unencryptedBytes[encodedFrame.type]))
233
                 unencryptedBytes[encodedFrame.type]))
214
             .then(cipherText => {
234
             .then(cipherText => {
215
-                const newData = new ArrayBuffer(unencryptedBytes[encodedFrame.type] + cipherText.byteLength
216
-                    + iv.byteLength + 1);
235
+                const newData = new ArrayBuffer(frameHeader.byteLength + cipherText.byteLength
236
+                    + digestLength[encodedFrame.type] + frameTrailer.byteLength);
217
                 const newUint8 = new Uint8Array(newData);
237
                 const newUint8 = new Uint8Array(newData);
218
 
238
 
219
-                newUint8.set(
220
-                    new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type])); // copy first bytes.
221
-                newUint8.set(
222
-                    new Uint8Array(cipherText), unencryptedBytes[encodedFrame.type]); // add ciphertext.
223
-                newUint8.set(
224
-                    new Uint8Array(iv), unencryptedBytes[encodedFrame.type] + cipherText.byteLength); // append IV.
225
-                newUint8[unencryptedBytes[encodedFrame.type] + cipherText.byteLength + ivLength]
226
-                    = keyIndex; // set key index.
227
-
228
-                encodedFrame.data = newData;
239
+                newUint8.set(frameHeader); // copy first bytes.
240
+                newUint8.set(new Uint8Array(cipherText), unencryptedBytes[encodedFrame.type]); // add ciphertext.
241
+                // Leave some space for the authentication tag. This is filled with 0s initially, similar to
242
+                // STUN message-integrity described in https://tools.ietf.org/html/rfc5389#section-15.4
243
+                newUint8.set(frameTrailer, frameHeader.byteLength + cipherText.byteLength
244
+                    + digestLength[encodedFrame.type]); // append trailer.
245
+
246
+                return crypto.subtle.sign(authenticationTagOptions, this._cryptoKeyRing[keyIndex].authenticationKey,
247
+                    new Uint8Array(newData)).then(authTag => {
248
+                    // Set the truncated authentication tag.
249
+                    newUint8.set(new Uint8Array(authTag, 0, digestLength[encodedFrame.type]),
250
+                        unencryptedBytes[encodedFrame.type] + cipherText.byteLength);
251
+                    encodedFrame.data = newData;
229
 
252
 
230
-                return controller.enqueue(encodedFrame);
253
+                    return controller.enqueue(encodedFrame);
254
+                });
231
             }, e => {
255
             }, e => {
256
+                // TODO: surface this to the app.
232
                 console.error(e);
257
                 console.error(e);
233
 
258
 
234
                 // We are not enqueuing the frame here on purpose.
259
                 // We are not enqueuing the frame here on purpose.
247
      *
272
      *
248
      * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
273
      * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
249
      * @param {TransformStreamDefaultController} controller - TransportStreamController.
274
      * @param {TransformStreamDefaultController} controller - TransportStreamController.
250
-     *
251
-     * The decrypted frame is formed as follows:
252
-     * 1) Extract the key index from the last byte of the encrypted frame.
253
-     *    If there is no key associated with the key index, the frame is enqueued for decoding
254
-     *    and these steps terminate.
255
-     * 2) Determine the frame type in order to look up the number of unencrypted header bytes.
256
-     * 2) Extract the 12-byte IV from its position near the end of the packet.
257
-     *    Note: the IV is treated as opaque and not reconstructed from the input.
258
-     * 3) Decrypt the encrypted frame content after the unencrypted bytes using AES-GCM.
259
-     * 4) Allocate space for the decrypted frame.
260
-     * 5) Copy the unencrypted bytes from the start of the encrypted frame.
261
-     * 6) Append the plaintext to the decrypted frame.
262
-     * 7) Enqueue the decrypted frame for decoding.
263
      */
275
      */
264
     decodeFunction(encodedFrame, controller) {
276
     decodeFunction(encodedFrame, controller) {
265
         const data = new Uint8Array(encodedFrame.data);
277
         const data = new Uint8Array(encodedFrame.data);
266
-        const keyIndex = data[encodedFrame.data.byteLength - 1];
278
+        const keyIndex = data[encodedFrame.data.byteLength - 1] & 0x7;
267
 
279
 
268
         if (this._cryptoKeyRing[keyIndex]) {
280
         if (this._cryptoKeyRing[keyIndex]) {
269
-            const iv = new Uint8Array(encodedFrame.data, encodedFrame.data.byteLength - ivLength - 1, ivLength);
270
-            const cipherTextStart = unencryptedBytes[encodedFrame.type];
271
-            const cipherTextLength = encodedFrame.data.byteLength - (unencryptedBytes[encodedFrame.type]
272
-                + ivLength + 1);
273
-
274
-            return crypto.subtle.decrypt({
275
-                name: 'AES-GCM',
276
-                iv,
277
-                additionalData: new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type])
278
-            }, this._cryptoKeyRing[keyIndex], new Uint8Array(encodedFrame.data, cipherTextStart, cipherTextLength))
279
-            .then(plainText => {
280
-                const newData = new ArrayBuffer(unencryptedBytes[encodedFrame.type] + plainText.byteLength);
281
-                const newUint8 = new Uint8Array(newData);
281
+            const counterLength = 1 + ((data[encodedFrame.data.byteLength - 1] >> 4) & 0x7);
282
+            const frameHeader = new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type]);
282
 
283
 
283
-                newUint8.set(new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type]));
284
-                newUint8.set(new Uint8Array(plainText), unencryptedBytes[encodedFrame.type]);
284
+            // Extract the truncated authentication tag.
285
+            const authTagOffset = encodedFrame.data.byteLength - (digestLength[encodedFrame.type]
286
+                + counterLength + 1);
287
+            const authTag = encodedFrame.data.slice(authTagOffset, authTagOffset
288
+                + digestLength[encodedFrame.type]);
285
 
289
 
286
-                encodedFrame.data = newData;
290
+            // Set authentication tag bytes to 0.
291
+            const zeros = new Uint8Array(digestLength[encodedFrame.type]);
287
 
292
 
288
-                return controller.enqueue(encodedFrame);
289
-            }, e => {
290
-                console.error(e);
293
+            data.set(zeros, encodedFrame.data.byteLength - (digestLength[encodedFrame.type] + counterLength + 1));
294
+
295
+            return crypto.subtle.sign(authenticationTagOptions, this._cryptoKeyRing[keyIndex].authenticationKey,
296
+                encodedFrame.data).then(calculatedTag => {
297
+                // Do truncated hash comparison.
298
+                if (!isArrayEqual(authTag, calculatedTag.slice(0, digestLength[encodedFrame.type]))) {
299
+                    // TODO: surface this to the app.
300
+                    console.error('Authentication tag mismatch', new Uint8Array(authTag), new Uint8Array(calculatedTag,
301
+                        0, digestLength[encodedFrame.type]));
302
+
303
+                    return;
304
+                }
291
 
305
 
292
-                // TODO: notify the application about error status.
306
+                // Extract the counter.
307
+                const counter = new Uint8Array(16);
293
 
308
 
294
-                // TODO: For video we need a better strategy since we do not want to based any
295
-                // non-error frames on a garbage keyframe.
296
-                if (encodedFrame.type === undefined) { // audio, replace with silence.
297
-                    // audio, replace with silence.
298
-                    const newData = new ArrayBuffer(3);
309
+                counter.set(data.slice(encodedFrame.data.byteLength - (counterLength + 1),
310
+                    encodedFrame.data.byteLength - 1), 16 - counterLength);
311
+                const counterView = new DataView(counter.buffer);
312
+
313
+                // XOR the counter with the saltKey to construct the AES CTR.
314
+                const saltKey = new DataView(this._cryptoKeyRing[keyIndex].saltKey);
315
+
316
+                for (let i = 0; i < counter.byteLength; i++) {
317
+                    counterView.setUint8(i,
318
+                        counterView.getUint8(i) ^ saltKey.getUint8(i));
319
+                }
320
+
321
+                return crypto.subtle.decrypt({
322
+                    name: 'AES-CTR',
323
+                    counter,
324
+                    length: 64
325
+                }, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
326
+                        unencryptedBytes[encodedFrame.type],
327
+                        encodedFrame.data.byteLength - (unencryptedBytes[encodedFrame.type]
328
+                        + digestLength[encodedFrame.type] + counterLength + 1))
329
+                ).then(plainText => {
330
+                    const newData = new ArrayBuffer(unencryptedBytes[encodedFrame.type] + plainText.byteLength);
299
                     const newUint8 = new Uint8Array(newData);
331
                     const newUint8 = new Uint8Array(newData);
300
 
332
 
301
-                    newUint8.set([ 0xd8, 0xff, 0xfe ]); // opus silence frame.
333
+                    newUint8.set(frameHeader);
334
+                    newUint8.set(new Uint8Array(plainText), unencryptedBytes[encodedFrame.type]);
302
                     encodedFrame.data = newData;
335
                     encodedFrame.data = newData;
303
-                    controller.enqueue(encodedFrame);
304
-                }
336
+
337
+                    return controller.enqueue(encodedFrame);
338
+                }, e => {
339
+                    console.error(e);
340
+
341
+                    // TODO: notify the application about error status.
342
+                    // TODO: For video we need a better strategy since we do not want to based any
343
+                    // non-error frames on a garbage keyframe.
344
+                    if (encodedFrame.type === undefined) { // audio, replace with silence.
345
+                        const newData = new ArrayBuffer(3);
346
+                        const newUint8 = new Uint8Array(newData);
347
+
348
+                        newUint8.set([ 0xd8, 0xff, 0xfe ]); // opus silence frame.
349
+                        encodedFrame.data = newData;
350
+                        controller.enqueue(encodedFrame);
351
+                    }
352
+                });
305
             });
353
             });
306
         } else if (keyIndex >= this._cryptoKeyRing.length && this._cryptoKeyRing[this._currentKeyIndex]) {
354
         } else if (keyIndex >= this._cryptoKeyRing.length && this._cryptoKeyRing[this._currentKeyIndex]) {
307
             // If we are encrypting but don't have a key for the remote drop the frame.
355
             // If we are encrypting but don't have a key for the remote drop the frame.
322
 onmessage = async event => {
370
 onmessage = async event => {
323
     const { operation } = event.data;
371
     const { operation } = event.data;
324
 
372
 
325
-    if (operation === 'initialize') {
326
-        _keySalt = event.data.salt;
327
-    } else if (operation === 'encode') {
373
+    if (operation === 'encode') {
328
         const { readableStream, writableStream, participantId } = event.data;
374
         const { readableStream, writableStream, participantId } = event.data;
329
 
375
 
330
         if (!contexts.has(participantId)) {
376
         if (!contexts.has(participantId)) {
367
         const context = contexts.get(participantId);
413
         const context = contexts.get(participantId);
368
 
414
 
369
         if (key) {
415
         if (key) {
370
-            context.setKey(await context.deriveKey(key, _keySalt), keyIndex);
416
+            context.setKey(key, keyIndex);
371
         } else {
417
         } else {
372
             context.setKey(false, keyIndex);
418
             context.setKey(false, keyIndex);
373
         }
419
         }

Loading…
Cancel
Save