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,45 +4,42 @@
4 4
 This document describes some of the high-level concepts and outlines the design.
5 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 35
 We do not encrypt the first few bytes of the packet that form the VP8 payload
39 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 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 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 43
 ## Using workers
47 44
 
48 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,15 +54,6 @@ export default class E2EEcontext {
54 54
 
55 55
         this._worker = new Worker(blobUrl, { name: 'E2EE Worker' });
56 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,4 +1,5 @@
1 1
 /* global TransformStream */
2
+/* eslint-disable no-bitwise */
2 3
 
3 4
 // Worker for E2EE/Insertable streams.
4 5
 //
@@ -21,20 +22,26 @@ function polyFillEncodedFrameMetadata(encodedFrame, controller) {
21 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 41
 // We use a ringbuffer of keys so we can change them and still decode packets that were
25 42
 // encrypted with an old key.
26 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 45
 // We copy the first bytes of the VP8 payload unencrypted.
39 46
 // For keyframes this is 10 bytes, for non-keyframes (delta) 3. See
40 47
 //   https://tools.ietf.org/html/rfc6386#section-9.1
@@ -51,32 +58,65 @@ const unencryptedBytes = {
51 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 75
  * @param {Uint8Array} keyBytes - Value to derive key from
66 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 81
     // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
70 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 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,78 +135,28 @@ class Context {
95 135
         // A pointer to the currently used key.
96 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 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 149
      * @param {CryptoKey} key
126 150
      * @param {Number} keyIndex
127 151
      */
128
-    setKey(key, keyIndex) {
152
+    async setKey(key, keyIndex) {
129 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,7 +165,9 @@ class Context {
175 165
      * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
176 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 171
      * changes to the SFU which for video requires not encrypting the keyframe bit of VP8
180 172
      * as SFUs need to detect a keyframe (framemarking or the generic frame descriptor will
181 173
      * solve this eventually). This also "hides" that a client is using E2EE a bit.
@@ -185,50 +177,83 @@ class Context {
185 177
      *
186 178
      * The VP8 payload descriptor described in
187 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 183
     encodeFunction(encodedFrame, controller) {
203 184
         const keyIndex = this._currentKeyIndex;
204 185
 
205 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 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 233
                 unencryptedBytes[encodedFrame.type]))
214 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 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 255
             }, e => {
256
+                // TODO: surface this to the app.
232 257
                 console.error(e);
233 258
 
234 259
                 // We are not enqueuing the frame here on purpose.
@@ -247,61 +272,84 @@ class Context {
247 272
      *
248 273
      * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
249 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 276
     decodeFunction(encodedFrame, controller) {
265 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 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 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 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 354
         } else if (keyIndex >= this._cryptoKeyRing.length && this._cryptoKeyRing[this._currentKeyIndex]) {
307 355
             // If we are encrypting but don't have a key for the remote drop the frame.
@@ -322,9 +370,7 @@ const contexts = new Map(); // Map participant id => context
322 370
 onmessage = async event => {
323 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 374
         const { readableStream, writableStream, participantId } = event.data;
329 375
 
330 376
         if (!contexts.has(participantId)) {
@@ -367,7 +413,7 @@ onmessage = async event => {
367 413
         const context = contexts.get(participantId);
368 414
 
369 415
         if (key) {
370
-            context.setKey(await context.deriveKey(key, _keySalt), keyIndex);
416
+            context.setKey(key, keyIndex);
371 417
         } else {
372 418
             context.setKey(false, keyIndex);
373 419
         }

Loading…
Cancel
Save