ソースを参照

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年前
コミット
cacf6c7b56
5個のファイルの変更6行の追加300行の削除
  1. 6
    77
      modules/e2ee/Context.js
  2. 0
    168
      modules/e2ee/Context.spec.js
  3. 0
    13
      modules/e2ee/E2EEContext.js
  4. 0
    32
      modules/e2ee/E2EEncryption.js
  5. 0
    10
      modules/e2ee/Worker.js

+ 6
- 77
modules/e2ee/Context.js ファイルの表示

@@ -67,9 +67,6 @@ export class Context {
67 67
         this._sendCount = BigInt(0); // eslint-disable-line new-cap
68 68
 
69 69
         this._id = id;
70
-
71
-        this._signatureKey = null;
72
-        this._signatureOptions = null;
73 70
     }
74 71
 
75 72
     /**
@@ -108,34 +105,6 @@ export class Context {
108 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 109
      * Function that will be injected in a stream and will encrypt the given encoded frames.
141 110
      *
@@ -186,12 +155,7 @@ export class Context {
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 160
             frameTrailer.set(new Uint8Array(counter.buffer, counter.byteLength - counterLength));
197 161
 
@@ -199,9 +163,6 @@ export class Context {
199 163
             // This is different from the sframe draft, increases the key space and lets us
200 164
             // ignore the case of a zero-length counter at the receiver.
201 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 167
             // XOR the counter with the saltKey to construct the AES CTR.
207 168
             const saltKey = new DataView(this._cryptoKeyRing[keyIndex].saltKey);
@@ -236,13 +197,6 @@ export class Context {
236 197
                     // Set the truncated authentication tag.
237 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 200
                     encodedFrame.data = newData;
247 201
 
248 202
                     return controller.enqueue(encodedFrame);
@@ -274,42 +228,17 @@ export class Context {
274 228
 
275 229
         if (this._cryptoKeyRing[this._currentKeyIndex] && this._cryptoKeyRing[keyIndex]) {
276 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 231
             const frameHeader = new Uint8Array(encodedFrame.data, 0, UNENCRYPTED_BYTES[encodedFrame.type]);
280 232
 
281 233
             // Extract the truncated authentication tag.
282 234
             const authTagOffset = encodedFrame.data.byteLength - (DIGEST_LENGTH[encodedFrame.type]
283
-                + counterLength + signatureLength + 1);
235
+                + counterLength + 1);
284 236
             const authTag = encodedFrame.data.slice(authTagOffset, authTagOffset
285 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 239
             // Set authentication tag bytes to 0.
311 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 243
             // Do truncated hash comparison of the authentication tag.
315 244
             // If the hash does not match we might have to advance the ratchet a limited number
@@ -350,8 +279,8 @@ export class Context {
350 279
             // Extract the counter.
351 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 284
             const counterView = new DataView(counter.buffer);
356 285
 
357 286
             // XOR the counter with the saltKey to construct the AES CTR.
@@ -369,7 +298,7 @@ export class Context {
369 298
             }, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
370 299
                     UNENCRYPTED_BYTES[encodedFrame.type],
371 300
                     encodedFrame.data.byteLength - (UNENCRYPTED_BYTES[encodedFrame.type]
372
-                    + DIGEST_LENGTH[encodedFrame.type] + counterLength + signatureLength + 1))
301
+                    + DIGEST_LENGTH[encodedFrame.type] + counterLength + 1))
373 302
             ).then(plainText => {
374 303
                 const newData = new ArrayBuffer(UNENCRYPTED_BYTES[encodedFrame.type] + plainText.byteLength);
375 304
                 const newUint8 = new Uint8Array(newData);

+ 0
- 168
modules/e2ee/Context.spec.js ファイルの表示

@@ -170,172 +170,4 @@ describe('E2EE Context', () => {
170 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 ファイルの表示

@@ -139,17 +139,4 @@ export default class E2EEcontext {
139 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 ファイルの表示

@@ -17,13 +17,6 @@ const logger = getLogger(__filename);
17 17
 // joins or leaves.
18 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 21
  * This module integrates {@link E2EEContext} with {@link JitsiConference} in order to enable E2E encryption.
29 22
  */
@@ -39,7 +32,6 @@ export class E2EEncryption {
39 32
         this._enabled = false;
40 33
         this._initialized = false;
41 34
         this._key = undefined;
42
-        this._signatureKeyPair = undefined;
43 35
 
44 36
         this._e2eeCtx = new E2EEContext();
45 37
         this._olmAdapter = new OlmAdapter(conference);
@@ -131,17 +123,6 @@ export class E2EEncryption {
131 123
         this._enabled = enabled;
132 124
 
133 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 126
             // Need to re-create the peerconnections in order to apply the insertable streams constraint.
146 127
             // TODO: this was necessary due to some audio issues when indertable streams are used
147 128
             // even though encryption is not performed. This should be fixed in the browser eventually.
@@ -268,19 +249,6 @@ export class E2EEncryption {
268 249
         case 'e2ee.idKey':
269 250
             logger.debug(`Participant ${participant.getId()} updated their id key: ${newValue}`);
270 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 ファイルの表示

@@ -58,16 +58,6 @@ onmessage = async event => {
58 58
         } else {
59 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 61
     } else if (operation === 'cleanup') {
72 62
         const { participantId } = event.data;
73 63
 

読み込み中…
キャンセル
保存