Browse Source

feat(e2ee) update to SFrame draft -02

Drop end to end signature. The impersonation attack vector is deemed out of
scope since insiders are already part of the meeting.
dev1
Saúl Ibarra Corretgé 4 years ago
parent
commit
cacf6c7b56

+ 6
- 77
modules/e2ee/Context.js View File

67
         this._sendCount = BigInt(0); // eslint-disable-line new-cap
67
         this._sendCount = BigInt(0); // eslint-disable-line new-cap
68
 
68
 
69
         this._id = id;
69
         this._id = id;
70
-
71
-        this._signatureKey = null;
72
-        this._signatureOptions = null;
73
     }
70
     }
74
 
71
 
75
     /**
72
     /**
108
         this._sendCount = BigInt(0); // eslint-disable-line new-cap
105
         this._sendCount = BigInt(0); // eslint-disable-line new-cap
109
     }
106
     }
110
 
107
 
111
-    /**
112
-     * Sets the public or private key used to sign or verify frames.
113
-     * @param {CryptoKey} public or private CryptoKey object.
114
-     * @param {Object} signature options. Will be passed to sign/verify and need to specify byteLength of the signature.
115
-     *  Defaults to ECDSA with SHA-256 and a byteLength of 132.
116
-     */
117
-    setSignatureKey(key, options = {
118
-        name: 'ECDSA',
119
-        hash: { name: 'SHA-256' },
120
-        byteLength: 132 // Length of the signature.
121
-    }) {
122
-        this._signatureKey = key;
123
-        this._signatureOptions = options;
124
-    }
125
-
126
-    /**
127
-     * Decide whether we should sign a frame.
128
-     * @returns {boolean}
129
-     * @private
130
-     */
131
-    _shouldSignFrame() {
132
-        if (!this._signatureKey) {
133
-            return false;
134
-        }
135
-
136
-        return true;
137
-    }
138
-
139
     /**
108
     /**
140
      * Function that will be injected in a stream and will encrypt the given encoded frames.
109
      * Function that will be injected in a stream and will encrypt the given encoded frames.
141
      *
110
      *
186
                 }
155
                 }
187
             }
156
             }
188
 
157
 
189
-
190
-            // If a signature is included, the S bit is set and a fixed number
191
-            // of bytes (depending on the signature algorithm) is inserted between
192
-            // CTR and the trailing byte.
193
-            const signatureLength = this._shouldSignFrame(encodedFrame) ? this._signatureOptions.byteLength : 0;
194
-            const frameTrailer = new Uint8Array(counterLength + signatureLength + 1);
158
+            const frameTrailer = new Uint8Array(counterLength + 1);
195
 
159
 
196
             frameTrailer.set(new Uint8Array(counter.buffer, counter.byteLength - counterLength));
160
             frameTrailer.set(new Uint8Array(counter.buffer, counter.byteLength - counterLength));
197
 
161
 
199
             // This is different from the sframe draft, increases the key space and lets us
163
             // This is different from the sframe draft, increases the key space and lets us
200
             // ignore the case of a zero-length counter at the receiver.
164
             // ignore the case of a zero-length counter at the receiver.
201
             frameTrailer[frameTrailer.byteLength - 1] = keyIndex | ((counterLength - 1) << 4);
165
             frameTrailer[frameTrailer.byteLength - 1] = keyIndex | ((counterLength - 1) << 4);
202
-            if (signatureLength) {
203
-                frameTrailer[frameTrailer.byteLength - 1] |= 0x80; // set the signature bit.
204
-            }
205
 
166
 
206
             // XOR the counter with the saltKey to construct the AES CTR.
167
             // XOR the counter with the saltKey to construct the AES CTR.
207
             const saltKey = new DataView(this._cryptoKeyRing[keyIndex].saltKey);
168
             const saltKey = new DataView(this._cryptoKeyRing[keyIndex].saltKey);
236
                     // Set the truncated authentication tag.
197
                     // Set the truncated authentication tag.
237
                     newUint8.set(truncatedAuthTag, UNENCRYPTED_BYTES[encodedFrame.type] + cipherText.byteLength);
198
                     newUint8.set(truncatedAuthTag, UNENCRYPTED_BYTES[encodedFrame.type] + cipherText.byteLength);
238
 
199
 
239
-                    // Sign with the long-term signature key.
240
-                    if (signatureLength) {
241
-                        const signature = await crypto.subtle.sign(this._signatureOptions,
242
-                            this._signatureKey, truncatedAuthTag);
243
-
244
-                        newUint8.set(new Uint8Array(signature), newUint8.byteLength - signature.byteLength - 1);
245
-                    }
246
                     encodedFrame.data = newData;
200
                     encodedFrame.data = newData;
247
 
201
 
248
                     return controller.enqueue(encodedFrame);
202
                     return controller.enqueue(encodedFrame);
274
 
228
 
275
         if (this._cryptoKeyRing[this._currentKeyIndex] && this._cryptoKeyRing[keyIndex]) {
229
         if (this._cryptoKeyRing[this._currentKeyIndex] && this._cryptoKeyRing[keyIndex]) {
276
             const counterLength = 1 + ((data[encodedFrame.data.byteLength - 1] >> 4) & 0x7);
230
             const counterLength = 1 + ((data[encodedFrame.data.byteLength - 1] >> 4) & 0x7);
277
-            const signatureLength = data[encodedFrame.data.byteLength - 1] & 0x80
278
-                ? this._signatureOptions.byteLength : 0;
279
             const frameHeader = new Uint8Array(encodedFrame.data, 0, UNENCRYPTED_BYTES[encodedFrame.type]);
231
             const frameHeader = new Uint8Array(encodedFrame.data, 0, UNENCRYPTED_BYTES[encodedFrame.type]);
280
 
232
 
281
             // Extract the truncated authentication tag.
233
             // Extract the truncated authentication tag.
282
             const authTagOffset = encodedFrame.data.byteLength - (DIGEST_LENGTH[encodedFrame.type]
234
             const authTagOffset = encodedFrame.data.byteLength - (DIGEST_LENGTH[encodedFrame.type]
283
-                + counterLength + signatureLength + 1);
235
+                + counterLength + 1);
284
             const authTag = encodedFrame.data.slice(authTagOffset, authTagOffset
236
             const authTag = encodedFrame.data.slice(authTagOffset, authTagOffset
285
                 + DIGEST_LENGTH[encodedFrame.type]);
237
                 + DIGEST_LENGTH[encodedFrame.type]);
286
 
238
 
287
-            // Verify the long-term signature of the authentication tag.
288
-            if (signatureLength) {
289
-                if (this._signatureKey) {
290
-                    const signature = data.subarray(data.byteLength - signatureLength - 1, data.byteLength - 1);
291
-                    const validSignature = await crypto.subtle.verify(this._signatureOptions,
292
-                            this._signatureKey, signature, authTag);
293
-
294
-                    if (!validSignature) {
295
-                        // TODO: surface this to the app. We are encrypted but validation failed.
296
-                        console.error('Long-term signature mismatch (or no signature key)');
297
-
298
-                        return;
299
-                    }
300
-
301
-                    // TODO: surface this to the app. We are now encrypted and verified.
302
-                } else {
303
-                    // TODO: surface this to the app. We are now encrypted but can not verify.
304
-                }
305
-
306
-                // Then set signature bytes to 0.
307
-                data.set(new Uint8Array(signatureLength), encodedFrame.data.byteLength - (signatureLength + 1));
308
-            }
309
-
310
             // Set authentication tag bytes to 0.
239
             // Set authentication tag bytes to 0.
311
             data.set(new Uint8Array(DIGEST_LENGTH[encodedFrame.type]), encodedFrame.data.byteLength
240
             data.set(new Uint8Array(DIGEST_LENGTH[encodedFrame.type]), encodedFrame.data.byteLength
312
-                - (DIGEST_LENGTH[encodedFrame.type] + counterLength + signatureLength + 1));
241
+                - (DIGEST_LENGTH[encodedFrame.type] + counterLength + 1));
313
 
242
 
314
             // Do truncated hash comparison of the authentication tag.
243
             // Do truncated hash comparison of the authentication tag.
315
             // If the hash does not match we might have to advance the ratchet a limited number
244
             // If the hash does not match we might have to advance the ratchet a limited number
350
             // Extract the counter.
279
             // Extract the counter.
351
             const counter = new Uint8Array(16);
280
             const counter = new Uint8Array(16);
352
 
281
 
353
-            counter.set(data.slice(encodedFrame.data.byteLength - (counterLength + signatureLength + 1),
354
-                encodedFrame.data.byteLength - (signatureLength + 1)), 16 - counterLength);
282
+            counter.set(data.slice(encodedFrame.data.byteLength - (counterLength + 1),
283
+                encodedFrame.data.byteLength - 1), 16 - counterLength);
355
             const counterView = new DataView(counter.buffer);
284
             const counterView = new DataView(counter.buffer);
356
 
285
 
357
             // XOR the counter with the saltKey to construct the AES CTR.
286
             // XOR the counter with the saltKey to construct the AES CTR.
369
             }, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
298
             }, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
370
                     UNENCRYPTED_BYTES[encodedFrame.type],
299
                     UNENCRYPTED_BYTES[encodedFrame.type],
371
                     encodedFrame.data.byteLength - (UNENCRYPTED_BYTES[encodedFrame.type]
300
                     encodedFrame.data.byteLength - (UNENCRYPTED_BYTES[encodedFrame.type]
372
-                    + DIGEST_LENGTH[encodedFrame.type] + counterLength + signatureLength + 1))
301
+                    + DIGEST_LENGTH[encodedFrame.type] + counterLength + 1))
373
             ).then(plainText => {
302
             ).then(plainText => {
374
                 const newData = new ArrayBuffer(UNENCRYPTED_BYTES[encodedFrame.type] + plainText.byteLength);
303
                 const newData = new ArrayBuffer(UNENCRYPTED_BYTES[encodedFrame.type] + plainText.byteLength);
375
                 const newUint8 = new Uint8Array(newData);
304
                 const newUint8 = new Uint8Array(newData);

+ 0
- 168
modules/e2ee/Context.spec.js View File

170
             await sender.encodeFunction(makeAudioFrame(), sendController);
170
             await sender.encodeFunction(makeAudioFrame(), sendController);
171
         });
171
         });
172
     });
172
     });
173
-
174
-    describe('E2EE Signature', () => {
175
-        let privateKey;
176
-        let publicKey;
177
-
178
-        // Generated one-time using
179
-        // await crypto.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-521'}, false, ['sign', 'verify']);
180
-        // then exported as JWK. Only use as test vectors.
181
-        const rawPublicKey = {
182
-            crv: 'P-521',
183
-            ext: true,
184
-            key_ops: [ 'verify' ], // eslint-disable-line camelcase
185
-            kty: 'EC',
186
-            x: 'AEs3y1FyefvjTC6JaJ1s00k5CFnESu5xIofPAmu286Y4UWyx8kB3jTHPKDO8bK81XT2_HbbN9ONm2D5TYCCxoR5r',
187
-            y: 'AZHlLHuSEWM401dy2lo-nu100Hp1ixcYePf9sNboaZruXctvoAt_sAX6MM0NccHx4587yhWfn9NG7fCX60P5KAvA'
188
-        };
189
-        const rawPrivateKey = {
190
-            crv: 'P-521',
191
-            ext: true,
192
-            key_ops: [ 'sign' ], // eslint-disable-line camelcase
193
-            kty: 'EC',
194
-            d: 'AV3aTIFuO9Zm0SXVlnujUvlvGvyPrY0pEOtX2pxD2JwPvWWoLXfTA052MHhqiii2RORe_7Ivm_PNeBwhYcO04i-K',
195
-            x: 'AEs3y1FyefvjTC6JaJ1s00k5CFnESu5xIofPAmu286Y4UWyx8kB3jTHPKDO8bK81XT2_HbbN9ONm2D5TYCCxoR5r',
196
-            y: 'AZHlLHuSEWM401dy2lo-nu100Hp1ixcYePf9sNboaZruXctvoAt_sAX6MM0NccHx4587yhWfn9NG7fCX60P5KAvA'
197
-        };
198
-
199
-        beforeEach(async () => {
200
-            privateKey = await crypto.subtle.importKey('jwk', rawPrivateKey, { name: 'ECDSA',
201
-                namedCurve: 'P-521' }, false, [ 'sign' ]);
202
-            publicKey = await crypto.subtle.importKey('jwk', rawPublicKey, { name: 'ECDSA',
203
-                namedCurve: 'P-521' }, false, [ 'verify' ]);
204
-
205
-            await sender.setKey(key, 0);
206
-            await receiver.setKey(key, 0);
207
-            sender.setSignatureKey(privateKey);
208
-            receiver.setSignatureKey(publicKey);
209
-        });
210
-
211
-        it('signs the first frame', async done => {
212
-            sendController = {
213
-                enqueue: encodedFrame => {
214
-                    const data = new Uint8Array(encodedFrame.data);
215
-
216
-                    // Check that the signature bit is set.
217
-                    expect(data[data.byteLength - 1] & 0x80).toEqual(0x80);
218
-
219
-                    // An audio frame will have an overhead of 6 bytes with this counter and key size:
220
-                    //   4 bytes truncated signature, counter (1 byte) and 1 byte trailer.
221
-                    // In addition to that we have the 132 bytes signature.
222
-                    expect(data.byteLength).toEqual(audioBytes.length + 6 + 132);
223
-
224
-                    // TODO: provide test vector for the signature.
225
-                    done();
226
-                }
227
-            };
228
-            await sender.encodeFunction(makeAudioFrame(), sendController);
229
-        });
230
-
231
-        it('signs subsequent frames from different sources', async done => {
232
-            let frameCount = 0;
233
-
234
-            sendController = {
235
-                enqueue: encodedFrame => {
236
-                    frameCount++;
237
-                    const data = new Uint8Array(encodedFrame.data);
238
-
239
-                    expect(data[data.byteLength - 1] & 0x80).toEqual(0x80);
240
-
241
-                    if (frameCount === 2) {
242
-                        done();
243
-                    }
244
-                }
245
-            };
246
-
247
-            await sender.encodeFunction(makeAudioFrame(), sendController);
248
-
249
-            const secondFrame = makeAudioFrame();
250
-
251
-            secondFrame.getMetadata = () => {
252
-                return { synchronizationSource: 456 };
253
-            };
254
-            await sender.encodeFunction(secondFrame, sendController);
255
-        });
256
-
257
-        it('signs subsequent key frames from the same source', async done => {
258
-            let frameCount = 0;
259
-
260
-            sendController = {
261
-                enqueue: encodedFrame => {
262
-                    frameCount++;
263
-                    const data = new Uint8Array(encodedFrame.data);
264
-
265
-                    expect(data[data.byteLength - 1] & 0x80).toEqual(0x80);
266
-
267
-                    if (frameCount === 2) {
268
-                        done();
269
-                    }
270
-                }
271
-            };
272
-
273
-            await sender.encodeFunction(makeVideoFrame(), sendController);
274
-            await sender.encodeFunction(makeVideoFrame(), sendController);
275
-        });
276
-
277
-
278
-        it('signs subsequent frames from the same source', async done => {
279
-            let frameCount = 0;
280
-
281
-            sendController = {
282
-                enqueue: encodedFrame => {
283
-                    frameCount++;
284
-                    const data = new Uint8Array(encodedFrame.data);
285
-
286
-                    expect(data[data.byteLength - 1] & 0x80).toEqual(0x80);
287
-
288
-                    if (frameCount === 2) {
289
-                        done();
290
-                    }
291
-                }
292
-            };
293
-
294
-            await sender.encodeFunction(makeAudioFrame(), sendController);
295
-            await sender.encodeFunction(makeAudioFrame(), sendController);
296
-        });
297
-
298
-        it('signs after ratcheting the sender key', async done => {
299
-            let frameCount = 0;
300
-
301
-            sendController = {
302
-                enqueue: encodedFrame => {
303
-                    frameCount++;
304
-                    const data = new Uint8Array(encodedFrame.data);
305
-
306
-                    expect(data[data.byteLength - 1] & 0x80).toEqual(0x80);
307
-
308
-                    if (frameCount === 2) {
309
-                        done();
310
-                    }
311
-                }
312
-            };
313
-
314
-            await sender.encodeFunction(makeAudioFrame(), sendController);
315
-
316
-            // Ratchet the key. We reimport from the raw bytes.
317
-            const material = await importKey(key);
318
-
319
-            await sender.setKey(await ratchet(material), 0);
320
-            await sender.encodeFunction(makeAudioFrame(), sendController);
321
-        });
322
-
323
-        it('verifies the frame', async done => {
324
-            sendController = {
325
-                enqueue: async encodedFrame => {
326
-                    await receiver.decodeFunction(encodedFrame, receiveController);
327
-                }
328
-            };
329
-            receiveController = {
330
-                enqueue: encodedFrame => {
331
-                    const data = new Uint8Array(encodedFrame.data);
332
-
333
-                    expect(data.byteLength).toEqual(audioBytes.length);
334
-                    expect(Array.from(data)).toEqual(audioBytes);
335
-                    done();
336
-                }
337
-            };
338
-            await sender.encodeFunction(makeAudioFrame(), sendController);
339
-        });
340
-    });
341
 });
173
 });

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

139
             keyIndex
139
             keyIndex
140
         });
140
         });
141
     }
141
     }
142
-
143
-    /**
144
-     * Set the E2EE signature key for the specified participant.
145
-     * @param {string} participantId - the ID of the participant who's key we are setting.
146
-     * @param {CryptoKey} key - the webcrypto key to set.
147
-     */
148
-    setSignatureKey(participantId, key) {
149
-        this._worker.postMessage({
150
-            operation: 'setSignatureKey',
151
-            participantId,
152
-            key
153
-        });
154
-    }
155
 }
142
 }

+ 0
- 32
modules/e2ee/E2EEncryption.js View File

17
 // joins or leaves.
17
 // joins or leaves.
18
 const DEBOUNCE_PERIOD = 5000;
18
 const DEBOUNCE_PERIOD = 5000;
19
 
19
 
20
-// We use ECDSA with Curve P-521 for the long-term signing keys. See
21
-//   https://developer.mozilla.org/en-US/docs/Web/API/EcKeyGenParams
22
-const SIGNATURE_OPTIONS = {
23
-    name: 'ECDSA',
24
-    namedCurve: 'P-521'
25
-};
26
-
27
 /**
20
 /**
28
  * This module integrates {@link E2EEContext} with {@link JitsiConference} in order to enable E2E encryption.
21
  * This module integrates {@link E2EEContext} with {@link JitsiConference} in order to enable E2E encryption.
29
  */
22
  */
39
         this._enabled = false;
32
         this._enabled = false;
40
         this._initialized = false;
33
         this._initialized = false;
41
         this._key = undefined;
34
         this._key = undefined;
42
-        this._signatureKeyPair = undefined;
43
 
35
 
44
         this._e2eeCtx = new E2EEContext();
36
         this._e2eeCtx = new E2EEContext();
45
         this._olmAdapter = new OlmAdapter(conference);
37
         this._olmAdapter = new OlmAdapter(conference);
131
         this._enabled = enabled;
123
         this._enabled = enabled;
132
 
124
 
133
         if (!this._initialized && enabled) {
125
         if (!this._initialized && enabled) {
134
-            // Generate a frame signing key pair. Per session currently.
135
-            this._signatureKeyPair = await crypto.subtle.generateKey(SIGNATURE_OPTIONS,
136
-                true, [ 'sign', 'verify' ]);
137
-            this._e2eeCtx.setSignatureKey(this.conference.myUserId(), this._signatureKeyPair.privateKey);
138
-
139
-            // Serialize the JWK of the signing key. Using JSON, might be easy to xml-ify.
140
-            const serializedSigningKey = await crypto.subtle.exportKey('jwk', this._signatureKeyPair.publicKey);
141
-
142
-            // TODO: sign this with the OLM account key.
143
-            this.conference.setLocalParticipantProperty('e2ee.signatureKey', JSON.stringify(serializedSigningKey));
144
-
145
             // Need to re-create the peerconnections in order to apply the insertable streams constraint.
126
             // Need to re-create the peerconnections in order to apply the insertable streams constraint.
146
             // TODO: this was necessary due to some audio issues when indertable streams are used
127
             // TODO: this was necessary due to some audio issues when indertable streams are used
147
             // even though encryption is not performed. This should be fixed in the browser eventually.
128
             // even though encryption is not performed. This should be fixed in the browser eventually.
268
         case 'e2ee.idKey':
249
         case 'e2ee.idKey':
269
             logger.debug(`Participant ${participant.getId()} updated their id key: ${newValue}`);
250
             logger.debug(`Participant ${participant.getId()} updated their id key: ${newValue}`);
270
             break;
251
             break;
271
-        case 'e2ee.signatureKey':
272
-            logger.debug(`Participant ${participant.getId()} updated their signature key: ${newValue}`);
273
-            if (newValue) {
274
-                const parsed = JSON.parse(newValue);
275
-
276
-                const importedKey = await crypto.subtle.importKey('jwk', parsed, { name: 'ECDSA',
277
-                    namedCurve: parsed.crv }, true, parsed.key_ops);
278
-
279
-                this._e2eeCtx.setSignatureKey(participant.getId(), importedKey);
280
-            } else {
281
-                logger.warn(`e2ee signatureKey for ${participant.getId()} could not be updated with empty value.`);
282
-            }
283
-            break;
284
         }
252
         }
285
     }
253
     }
286
 
254
 

+ 0
- 10
modules/e2ee/Worker.js View File

58
         } else {
58
         } else {
59
             context.setKey(false, keyIndex);
59
             context.setKey(false, keyIndex);
60
         }
60
         }
61
-    } else if (operation === 'setSignatureKey') {
62
-        const { participantId, key, signatureOptions } = event.data;
63
-
64
-        if (!contexts.has(participantId)) {
65
-            contexts.set(participantId, new Context(participantId));
66
-        }
67
-        const context = contexts.get(participantId);
68
-
69
-        context.setSignatureKey(key, signatureOptions);
70
-
71
     } else if (operation === 'cleanup') {
61
     } else if (operation === 'cleanup') {
72
         const { participantId } = event.data;
62
         const { participantId } = event.data;
73
 
63
 

Loading…
Cancel
Save