Ver código fonte

e2ee: Ratchet the key forward on authentication tag errors

similar to what is explained here:
  https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.5.1
but we do it on authentication tag failures since it is not possible
to tell whether decrypt.
dev1
Philipp Hancke 5 anos atrás
pai
commit
4a6e493f41
4 arquivos alterados com 126 adições e 28 exclusões
  1. 8
    0
      doc/e2ee.md
  2. 11
    0
      modules/e2ee/E2EEContext.js
  3. 6
    5
      modules/e2ee/E2EEncryption.js
  4. 101
    23
      modules/e2ee/Worker.js

+ 8
- 0
doc/e2ee.md Ver arquivo

@@ -39,6 +39,14 @@ We do not encrypt the first few bytes of the packet that form the VP8 payload
39 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.
40 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.
41 41
 
42
+## Key Ratcheting
43
+Unlike described in
44
+  https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.5.1
45
+we attempt to ratchet the key forward when we do not find a valid
46
+authentication tag. Note that we only update the set of keys when
47
+we find a valid signature which avoids a denial of service attack with invalid signatures.
48
+
49
+TODO: if a frame ratchets the key forward it should be signed with the senders private key.
42 50
 
43 51
 ## Using workers
44 52
 

+ 11
- 0
modules/e2ee/E2EEContext.js Ver arquivo

@@ -148,4 +148,15 @@ export default class E2EEcontext {
148 148
             keyIndex
149 149
         });
150 150
     }
151
+
152
+    /**
153
+     * Ratchet our own key.
154
+     * @param {string} participantId - the ID of the participant who's key we should ratchet.
155
+     */
156
+    ratchet(participantId) {
157
+        this._worker.postMessage({
158
+            operation: 'ratchet',
159
+            participantId
160
+        });
161
+    }
151 162
 }

+ 6
- 5
modules/e2ee/E2EEncryption.js Ver arquivo

@@ -169,7 +169,7 @@ export class E2EEncryption {
169 169
     }
170 170
 
171 171
     /**
172
-     * Advances (using ratcheting) the current key whern a new participant joins the conference.
172
+     * Advances (using ratcheting) the current key when a new participant joins the conference.
173 173
      * @private
174 174
      */
175 175
     _onParticipantJoined(id) {
@@ -195,7 +195,7 @@ export class E2EEncryption {
195 195
     }
196 196
 
197 197
     /**
198
-     * Event posted when the E2EE signalling channel has been establioshed with the given participant.
198
+     * Event posted when the E2EE signalling channel has been established with the given participant.
199 199
      * @private
200 200
      */
201 201
     _onParticipantE2EEChannelReady(id) {
@@ -218,15 +218,16 @@ export class E2EEncryption {
218 218
 
219 219
     /**
220 220
      * Advances the current key by using ratcheting.
221
-     * TODO: not yet implemented, we are just rotating the key at the moment,
222
-     * which is a heavier operation.
223 221
      *
224 222
      * @private
225 223
      */
226 224
     async _ratchetKeyImpl() {
227 225
         logger.debug('Ratchetting key');
228 226
 
229
-        return this._rotateKey();
227
+        this._e2eeCtx.ratchet(this.conference.myUserId());
228
+
229
+        // TODO: how do we tell the olm adapter which might need to send the current ratchet key
230
+        //      to the other side?
230 231
     }
231 232
 
232 233
     /**

+ 101
- 23
modules/e2ee/Worker.js Ver arquivo

@@ -70,18 +70,17 @@ const digestLength = {
70 70
     undefined: 4 // frame.type is not set on audio
71 71
 };
72 72
 
73
+// Maximum number of forward ratchets to attempt when the authentication
74
+// tag on a remote packet does not match the current key.
75
+const ratchetWindow = 8;
76
+
73 77
 /**
74 78
  * Derives a set of keys from the master key.
75
- * @param {Uint8Array} keyBytes - Value to derive key from
76
- * @param {Uint8Array} salt - Salt used in key derivation
79
+ * @param {CryptoKey} material - master key to derive from
77 80
  *
78 81
  * See https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.1
79 82
  */
80
-async function deriveKeys(keyBytes) {
81
-    // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
82
-    const material = await crypto.subtle.importKey('raw', keyBytes,
83
-        'HKDF', false, [ 'deriveBits', 'deriveKey' ]);
84
-
83
+async function deriveKeys(material) {
85 84
     const info = new ArrayBuffer();
86 85
     const textEncoder = new TextEncoder();
87 86
 
@@ -113,12 +112,30 @@ async function deriveKeys(keyBytes) {
113 112
     }, material, 128);
114 113
 
115 114
     return {
115
+        material,
116 116
         encryptionKey,
117 117
         authenticationKey,
118 118
         saltKey
119 119
     };
120 120
 }
121 121
 
122
+/**
123
+ * Ratchets a key. See
124
+ * https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.5.1
125
+ * @param {CryptoKey} material - base key material
126
+ * @returns {ArrayBuffer} - ratcheted key material
127
+ */
128
+async function ratchet(material) {
129
+    const textEncoder = new TextEncoder();
130
+
131
+    return crypto.subtle.deriveBits({
132
+        name: 'HKDF',
133
+        salt: textEncoder.encode('JFrameRatchetKey'),
134
+        hash: 'SHA-256',
135
+        info: new ArrayBuffer()
136
+    }, material, 256);
137
+}
138
+
122 139
 
123 140
 /**
124 141
  * Per-participant context holding the cryptographic keys and
@@ -144,21 +161,47 @@ class Context {
144 161
     }
145 162
 
146 163
     /**
147
-     * Sets a key, derives the different subkeys and starts using them for encryption or
164
+     * Derives the different subkeys and starts using them for encryption or
148 165
      * decryption.
149
-     * @param {CryptoKey} key
166
+     * @param {Uint8Array|false} key bytes. Pass false to disable.
150 167
      * @param {Number} keyIndex
151 168
      */
152
-    async setKey(key, keyIndex) {
153
-        this._currentKeyIndex = keyIndex % this._cryptoKeyRing.length;
154
-        if (key) {
155
-            this._cryptoKeyRing[this._currentKeyIndex] = await deriveKeys(key);
169
+    async setKey(keyBytes, keyIndex) {
170
+        let newKey;
171
+
172
+        if (keyBytes) {
173
+            // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
174
+            const material = await crypto.subtle.importKey('raw', keyBytes,
175
+                'HKDF', false, [ 'deriveBits', 'deriveKey' ]);
176
+
177
+            newKey = await deriveKeys(material);
156 178
         } else {
157
-            this._cryptoKeyRing[this._currentKeyIndex] = false;
179
+            newKey = false;
158 180
         }
181
+        this._currentKeyIndex = keyIndex % this._cryptoKeyRing.length;
182
+        this._setKeys(newKey);
183
+    }
184
+
185
+    /**
186
+     * Sets a set of keys and resets the sendCount.
187
+     * decryption.
188
+     * @param {Object} keys set of keys.
189
+     */
190
+    _setKeys(keys) {
191
+        this._cryptoKeyRing[this._currentKeyIndex] = keys;
159 192
         this._sendCount = 0n; // Reset the send count (bigint).
160 193
     }
161 194
 
195
+    /**
196
+     * Ratchets a key forward one step.
197
+     */
198
+    async ratchet() {
199
+        const keys = this._cryptoKeyRing[this._currentKeyIndex];
200
+        const material = await ratchet(keys.material);
201
+
202
+        this.setKey(material, this._currentKeyIndex);
203
+    }
204
+
162 205
     /**
163 206
      * Function that will be injected in a stream and will encrypt the given encoded frames.
164 207
      *
@@ -292,16 +335,38 @@ class Context {
292 335
 
293 336
             data.set(zeros, encodedFrame.data.byteLength - (digestLength[encodedFrame.type] + counterLength + 1));
294 337
 
295
-            const calculatedTag = await crypto.subtle.sign(authenticationTagOptions,
296
-                this._cryptoKeyRing[keyIndex].authenticationKey, encodedFrame.data);
338
+            // Do truncated hash comparison. If the hash does not match we might have to advance the
339
+            // ratchet a limited number of times. See (even though the description there is odd)
340
+            // https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.5.1
341
+            let { authenticationKey, material } = this._cryptoKeyRing[keyIndex];
342
+            let valid = false;
343
+            let newKeys = null;
344
+
345
+            for (let distance = 0; distance < ratchetWindow; distance++) {
346
+                const calculatedTag = await crypto.subtle.sign(authenticationTagOptions,
347
+                    authenticationKey, encodedFrame.data);
348
+
349
+                if (isArrayEqual(new Uint8Array(authTag),
350
+                        new Uint8Array(calculatedTag.slice(0, digestLength[encodedFrame.type])))) {
351
+                    valid = true;
352
+                    if (distance > 0) {
353
+                        this._setKeys(newKeys);
354
+                    }
355
+                    break;
356
+                }
357
+
358
+                // Attempt to ratchet and generate the next set of keys.
359
+                material = await crypto.subtle.importKey('raw', await ratchet(material),
360
+                    'HKDF', false, [ 'deriveBits', 'deriveKey' ]);
361
+                newKeys = await deriveKeys(material);
362
+                authenticationKey = newKeys.authenticationKey;
363
+            }
364
+
365
+            // Check whether we found a valid signature.
366
+            if (!valid) {
367
+                // TODO: return an error to the app.
297 368
 
298
-            // Do truncated hash comparison.
299
-            if (!isArrayEqual(new Uint8Array(authTag),
300
-                    new Uint8Array(calculatedTag.slice(0, digestLength[encodedFrame.type])))) {
301
-                // TODO: at this point we need to ratchet until we get a key that works. If we ratchet too often
302
-                // we need to return an error to the app.
303
-                console.error('Authentication tag mismatch', new Uint8Array(authTag), new Uint8Array(calculatedTag,
304
-                    0, digestLength[encodedFrame.type]));
369
+                console.error('Authentication tag mismatch');
305 370
 
306 371
                 return;
307 372
             }
@@ -419,6 +484,19 @@ onmessage = async event => {
419 484
         } else {
420 485
             context.setKey(false, keyIndex);
421 486
         }
487
+    } else if (operation === 'ratchet') {
488
+        const { participantId } = event.data;
489
+
490
+        // TODO: can we ensure this is for our own sender key?
491
+
492
+        if (!contexts.has(participantId)) {
493
+            console.error('Could not find context for', participantId);
494
+
495
+            return;
496
+        }
497
+        const context = contexts.get(participantId);
498
+
499
+        context.ratchet();
422 500
     } else if (operation === 'cleanup') {
423 501
         const { participantId } = event.data;
424 502
 

Carregando…
Cancelar
Salvar