瀏覽代碼

feat(e2ee) add ExternallyManagedKeyHandler

dev1
tmoldovan8x8 4 年之前
父節點
當前提交
afc006e99a
沒有連結到貢獻者的電子郵件帳戶。

+ 11
- 0
JitsiConference.js 查看文件

@@ -3694,6 +3694,17 @@ JitsiConference.prototype.toggleE2EE = function(enabled) {
3694 3694
     this._e2eEncryption.setEnabled(enabled);
3695 3695
 };
3696 3696
 
3697
+/**
3698
+ * Sets the key and index for End-to-End encryption.
3699
+ *
3700
+ * @param {CryptoKey} [keyInfo.encryptionKey] - encryption key.
3701
+ * @param {Number} [keyInfo.index] - the index of the encryption key.
3702
+ * @returns {void}
3703
+ */
3704
+JitsiConference.prototype.setMediaEncryptionKey = function(keyInfo) {
3705
+    this._e2eEncryption.setEncryptionKey(keyInfo);
3706
+};
3707
+
3697 3708
 /**
3698 3709
  * Returns <tt>true</tt> if lobby support is enabled in the backend.
3699 3710
  *

+ 28
- 21
modules/e2ee/Context.js 查看文件

@@ -37,9 +37,9 @@ const RATCHET_WINDOW_SIZE = 8;
37 37
  */
38 38
 export class Context {
39 39
     /**
40
-     * @param {string} id - local muc resourcepart
40
+     * @param {Object} options
41 41
      */
42
-    constructor(id) {
42
+    constructor({ sharedKey = false } = {}) {
43 43
         // An array (ring) of keys that we use for sending and receiving.
44 44
         this._cryptoKeyRing = new Array(KEYRING_SIZE);
45 45
 
@@ -48,7 +48,7 @@ export class Context {
48 48
 
49 49
         this._sendCounts = new Map();
50 50
 
51
-        this._id = id;
51
+        this._sharedKey = sharedKey;
52 52
     }
53 53
 
54 54
     /**
@@ -57,18 +57,20 @@ export class Context {
57 57
      * @param {Uint8Array|false} key bytes. Pass false to disable.
58 58
      * @param {Number} keyIndex
59 59
      */
60
-    async setKey(keyBytes, keyIndex) {
61
-        let newKey;
60
+    async setKey(key, keyIndex = -1) {
61
+        let newKey = false;
62 62
 
63
-        if (keyBytes) {
64
-            const material = await importKey(keyBytes);
63
+        if (key) {
64
+            if (this._sharedKey) {
65
+                newKey = key;
66
+            } else {
67
+                const material = await importKey(key);
65 68
 
66
-            newKey = await deriveKeys(material);
67
-        } else {
68
-            newKey = false;
69
+                newKey = await deriveKeys(material);
70
+            }
69 71
         }
70
-        this._currentKeyIndex = keyIndex % this._cryptoKeyRing.length;
71
-        this._setKeys(newKey);
72
+
73
+        this._setKeys(newKey, keyIndex);
72 74
     }
73 75
 
74 76
     /**
@@ -80,10 +82,11 @@ export class Context {
80 82
      */
81 83
     _setKeys(keys, keyIndex = -1) {
82 84
         if (keyIndex >= 0) {
83
-            this._cryptoKeyRing[keyIndex] = keys;
84
-        } else {
85
-            this._cryptoKeyRing[this._currentKeyIndex] = keys;
85
+            this._currentKeyIndex = keyIndex % this._cryptoKeyRing.length;
86 86
         }
87
+
88
+        this._cryptoKeyRing[this._currentKeyIndex] = keys;
89
+
87 90
         this._sendCount = BigInt(0); // eslint-disable-line new-cap
88 91
     }
89 92
 
@@ -251,6 +254,10 @@ export class Context {
251 254
 
252 255
             encodedFrame.data = newData;
253 256
         } catch (error) {
257
+            if (this._sharedKey) {
258
+                return encodedFrame;
259
+            }
260
+
254 261
             if (ratchetCount < RATCHET_WINDOW_SIZE) {
255 262
                 material = await importKey(await ratchet(material));
256 263
 
@@ -265,12 +272,12 @@ export class Context {
265 272
                     ratchetCount + 1);
266 273
             }
267 274
 
268
-            /*
269
-               Since the key it is first send and only afterwards actually used for encrypting, there were
270
-               situations when the decrypting failed due to the fact that the received frame was not encrypted
271
-               yet and ratcheting, of course, did not solve the problem. So if we fail RATCHET_WINDOW_SIZE times,
272
-               we come back to the initial key.
273
-            */
275
+            /**
276
+             * Since the key it is first send and only afterwards actually used for encrypting, there were
277
+             * situations when the decrypting failed due to the fact that the received frame was not encrypted
278
+             * yet and ratcheting, of course, did not solve the problem. So if we fail RATCHET_WINDOW_SIZE times,
279
+             * we come back to the initial key.
280
+             */
274 281
             this._setKeys(initialKey);
275 282
 
276 283
             // TODO: notify the application about error status.

+ 20
- 3
modules/e2ee/E2EEContext.js 查看文件

@@ -23,8 +23,9 @@ const kJitsiE2EE = Symbol('kJitsiE2EE');
23 23
 export default class E2EEcontext {
24 24
     /**
25 25
      * Build a new E2EE context instance, which will be used in a given conference.
26
+     * @param {boolean} [options.sharedKey] - whether there is a uniques key shared amoung all participants.
26 27
      */
27
-    constructor() {
28
+    constructor({ sharedKey } = {}) {
28 29
         // Determine the URL for the worker script. Relative URLs are relative to
29 30
         // the entry point, not the script that launches the worker.
30 31
         let baseUrl = '';
@@ -44,7 +45,13 @@ export default class E2EEcontext {
44 45
         const blobUrl = window.URL.createObjectURL(workerBlob);
45 46
 
46 47
         this._worker = new Worker(blobUrl, { name: 'E2EE Worker' });
48
+
47 49
         this._worker.onerror = e => logger.error(e);
50
+
51
+        this._worker.postMessage({
52
+            operation: 'initialize',
53
+            sharedKey
54
+        });
48 55
     }
49 56
 
50 57
     /**
@@ -60,6 +67,16 @@ export default class E2EEcontext {
60 67
         });
61 68
     }
62 69
 
70
+    /**
71
+     * Cleans up all state associated with all participants in the conference. This is needed when disabling e2ee.
72
+     *
73
+     */
74
+    cleanupAll() {
75
+        this._worker.postMessage({
76
+            operation: 'cleanupAll'
77
+        });
78
+    }
79
+
63 80
     /**
64 81
      * Handles the given {@code RTCRtpReceiver} by creating a {@code TransformStream} which will inject
65 82
      * a frame decoder.
@@ -136,9 +153,9 @@ export default class E2EEcontext {
136 153
     setKey(participantId, key, keyIndex) {
137 154
         this._worker.postMessage({
138 155
             operation: 'setKey',
139
-            participantId,
140 156
             key,
141
-            keyIndex
157
+            keyIndex,
158
+            participantId
142 159
         });
143 160
     }
144 161
 }

+ 25
- 306
modules/e2ee/E2EEncryption.js 查看文件

@@ -1,25 +1,11 @@
1
-/* global __filename */
2
-
3
-import { getLogger } from 'jitsi-meet-logger';
4
-import debounce from 'lodash.debounce';
5
-
6
-import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
7
-import RTCEvents from '../../service/RTC/RTCEvents';
8 1
 import browser from '../browser';
9
-import Deferred from '../util/Deferred';
10 2
 
11
-import E2EEContext from './E2EEContext';
3
+import { ExternallyManagedKeyHandler } from './ExternallyManagedKeyHandler';
4
+import { ManagedKeyHandler } from './ManagedKeyHandler';
12 5
 import { OlmAdapter } from './OlmAdapter';
13
-import { importKey, ratchet } from './crypto-utils';
14
-
15
-const logger = getLogger(__filename);
16
-
17
-// Period which we'll wait before updating / rotating our keys when a participant
18
-// joins or leaves.
19
-const DEBOUNCE_PERIOD = 5000;
20 6
 
21 7
 /**
22
- * This module integrates {@link E2EEContext} with {@link JitsiConference} in order to enable E2E encryption.
8
+ * This module integrates {@link KeyHandler} with {@link JitsiConference} in order to enable E2E encryption.
23 9
  */
24 10
 export class E2EEncryption {
25 11
     /**
@@ -27,66 +13,15 @@ export class E2EEncryption {
27 13
      * @param {JitsiConference} conference - The conference instance for which E2E encryption is to be enabled.
28 14
      */
29 15
     constructor(conference) {
30
-        this.conference = conference;
31
-
32
-        this._conferenceJoined = false;
33
-        this._enabled = false;
34
-        this._key = undefined;
35
-        this._enabling = undefined;
36
-
37
-        this._e2eeCtx = new E2EEContext();
38
-        this._olmAdapter = new OlmAdapter(conference);
39
-
40
-        // Debounce key rotation / ratcheting to avoid a storm of messages.
41
-        this._ratchetKey = debounce(this._ratchetKeyImpl, DEBOUNCE_PERIOD);
42
-        this._rotateKey = debounce(this._rotateKeyImpl, DEBOUNCE_PERIOD);
43
-
44
-        // Participant join / leave operations. Used for key advancement / rotation.
45
-        //
16
+        const { e2ee = {} } = conference.options.config;
46 17
 
47
-        this.conference.on(
48
-            JitsiConferenceEvents.CONFERENCE_JOINED,
49
-            () => {
50
-                this._conferenceJoined = true;
51
-            });
52
-        this.conference.on(
53
-            JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
54
-            this._onParticipantPropertyChanged.bind(this));
55
-        this.conference.on(
56
-            JitsiConferenceEvents.USER_JOINED,
57
-            this._onParticipantJoined.bind(this));
58
-        this.conference.on(
59
-            JitsiConferenceEvents.USER_LEFT,
60
-            this._onParticipantLeft.bind(this));
18
+        this._externallyManaged = e2ee.externallyManagedKey;
61 19
 
62
-        // Conference media events in order to attach the encryptor / decryptor.
63
-        // FIXME add events to TraceablePeerConnection which will allow to see when there's new receiver or sender
64
-        // added instead of shenanigans around conference track events and track muted.
65
-        //
66
-
67
-        this.conference.on(
68
-            JitsiConferenceEvents._MEDIA_SESSION_STARTED,
69
-            this._onMediaSessionStarted.bind(this));
70
-        this.conference.on(
71
-            JitsiConferenceEvents.TRACK_ADDED,
72
-            track => track.isLocal() && this._onLocalTrackAdded(track));
73
-        this.conference.rtc.on(
74
-            RTCEvents.REMOTE_TRACK_ADDED,
75
-            (track, tpc) => this._setupReceiverE2EEForTrack(tpc, track));
76
-        this.conference.on(
77
-            JitsiConferenceEvents.TRACK_MUTE_CHANGED,
78
-            this._trackMuteChanged.bind(this));
79
-
80
-        // Olm signalling events.
81
-        this._olmAdapter.on(
82
-            OlmAdapter.events.OLM_ID_KEY_READY,
83
-            this._onOlmIdKeyReady.bind(this));
84
-        this._olmAdapter.on(
85
-            OlmAdapter.events.PARTICIPANT_E2EE_CHANNEL_READY,
86
-            this._onParticipantE2EEChannelReady.bind(this));
87
-        this._olmAdapter.on(
88
-            OlmAdapter.events.PARTICIPANT_KEY_UPDATED,
89
-            this._onParticipantKeyUpdated.bind(this));
20
+        if (this._externallyManaged) {
21
+            this._keyHandler = new ExternallyManagedKeyHandler(conference);
22
+        } else {
23
+            this._keyHandler = new ManagedKeyHandler(conference);
24
+        }
90 25
     }
91 26
 
92 27
     /**
@@ -96,10 +31,15 @@ export class E2EEncryption {
96 31
      * @returns {boolean}
97 32
      */
98 33
     static isSupported(config) {
34
+        const { e2ee = {} } = config;
35
+
36
+        if (!e2ee.externallyManagedKey && !OlmAdapter.isSupported()) {
37
+            return false;
38
+        }
39
+
99 40
         return !(config.testing && config.testing.disableE2EE)
100 41
             && (browser.supportsInsertableStreams()
101
-                || (config.enableEncodedTransformSupport && browser.supportsEncodedTransform()))
102
-            && OlmAdapter.isSupported();
42
+                || (config.enableEncodedTransformSupport && browser.supportsEncodedTransform()));
103 43
     }
104 44
 
105 45
     /**
@@ -108,7 +48,7 @@ export class E2EEncryption {
108 48
      * @returns {boolean}
109 49
      */
110 50
     isEnabled() {
111
-        return this._enabled;
51
+        return this._keyHandler.isEnabled();
112 52
     }
113 53
 
114 54
     /**
@@ -118,238 +58,17 @@ export class E2EEncryption {
118 58
      * @returns {void}
119 59
      */
120 60
     async setEnabled(enabled) {
121
-        if (enabled === this._enabled) {
122
-            return;
123
-        }
124
-
125
-        this._enabling && await this._enabling;
126
-
127
-        this._enabling = new Deferred();
128
-
129
-        this._enabled = enabled;
130
-
131
-        if (enabled) {
132
-            await this._olmAdapter.initSessions();
133
-        } else {
134
-            for (const participant of this.conference.getParticipants()) {
135
-                this._e2eeCtx.cleanup(participant.getId());
136
-            }
137
-            this._olmAdapter.clearAllParticipantsSessions();
138
-        }
139
-
140
-        this.conference.setLocalParticipantProperty('e2ee.enabled', enabled);
141
-
142
-        this.conference._restartMediaSessions();
143
-
144
-        // Generate a random key in case we are enabling.
145
-        this._key = enabled ? this._generateKey() : false;
146
-
147
-        // Send it to others using the E2EE olm channel.
148
-        const index = await this._olmAdapter.updateKey(this._key);
149
-
150
-        // Set our key so we begin encrypting.
151
-        this._e2eeCtx.setKey(this.conference.myUserId(), this._key, index);
152
-
153
-        this._enabling.resolve();
154
-    }
155
-
156
-    /**
157
-     * Generates a new 256 bit random key.
158
-     *
159
-     * @returns {Uint8Array}
160
-     * @private
161
-     */
162
-    _generateKey() {
163
-        return window.crypto.getRandomValues(new Uint8Array(32));
164
-    }
165
-
166
-    /**
167
-     * Setup E2EE on the new track that has been added to the conference, apply it on all the open peerconnections.
168
-     * @param {JitsiLocalTrack} track - the new track that's being added to the conference.
169
-     * @private
170
-     */
171
-    _onLocalTrackAdded(track) {
172
-        for (const session of this.conference._getMediaSessions()) {
173
-            this._setupSenderE2EEForTrack(session, track);
174
-        }
175
-    }
176
-
177
-    /**
178
-     * Setups E2E encryption for the new session.
179
-     * @param {JingleSessionPC} session - the new media session.
180
-     * @private
181
-     */
182
-    _onMediaSessionStarted(session) {
183
-        const localTracks = this.conference.getLocalTracks();
184
-
185
-        for (const track of localTracks) {
186
-            this._setupSenderE2EEForTrack(session, track);
187
-        }
188
-    }
189
-
190
-    /**
191
-     * Publushes our own Olmn id key in presence.
192
-     * @private
193
-     */
194
-    _onOlmIdKeyReady(idKey) {
195
-        logger.debug(`Olm id key ready: ${idKey}`);
196
-
197
-        // Publish it in presence.
198
-        this.conference.setLocalParticipantProperty('e2ee.idKey', idKey);
199
-    }
200
-
201
-    /**
202
-     * Advances (using ratcheting) the current key when a new participant joins the conference.
203
-     * @private
204
-     */
205
-    _onParticipantJoined() {
206
-        if (this._conferenceJoined && this._enabled) {
207
-            this._ratchetKey();
208
-        }
209
-    }
210
-
211
-    /**
212
-     * Rotates the current key when a participant leaves the conference.
213
-     * @private
214
-     */
215
-    _onParticipantLeft(id) {
216
-        this._e2eeCtx.cleanup(id);
217
-
218
-        if (this._enabled) {
219
-            this._rotateKey();
220
-        }
221
-    }
222
-
223
-    /**
224
-     * Event posted when the E2EE signalling channel has been established with the given participant.
225
-     * @private
226
-     */
227
-    _onParticipantE2EEChannelReady(id) {
228
-        logger.debug(`E2EE channel with participant ${id} is ready`);
229
-    }
230
-
231
-    /**
232
-     * Handles an update in a participant's key.
233
-     *
234
-     * @param {string} id - The participant ID.
235
-     * @param {Uint8Array | boolean} key - The new key for the participant.
236
-     * @param {Number} index - The new key's index.
237
-     * @private
238
-     */
239
-    _onParticipantKeyUpdated(id, key, index) {
240
-        logger.debug(`Participant ${id} updated their key`);
241
-
242
-        this._e2eeCtx.setKey(id, key, index);
243
-    }
244
-
245
-    /**
246
-     * Handles an update in a participant's presence property.
247
-     *
248
-     * @param {JitsiParticipant} participant - The participant.
249
-     * @param {string} name - The name of the property that changed.
250
-     * @param {*} oldValue - The property's previous value.
251
-     * @param {*} newValue - The property's new value.
252
-     * @private
253
-     */
254
-    async _onParticipantPropertyChanged(participant, name, oldValue, newValue) {
255
-        switch (name) {
256
-        case 'e2ee.idKey':
257
-            logger.debug(`Participant ${participant.getId()} updated their id key: ${newValue}`);
258
-            break;
259
-        case 'e2ee.enabled':
260
-            if (!newValue && this._enabled) {
261
-                this._olmAdapter.clearParticipantSession(participant);
262
-
263
-                this._rotateKey();
264
-            }
265
-            break;
266
-        }
267
-    }
268
-
269
-    /**
270
-     * Advances the current key by using ratcheting.
271
-     *
272
-     * @private
273
-     */
274
-    async _ratchetKeyImpl() {
275
-        logger.debug('Ratchetting key');
276
-
277
-        const material = await importKey(this._key);
278
-        const newKey = await ratchet(material);
279
-
280
-        this._key = new Uint8Array(newKey);
281
-
282
-        const index = this._olmAdapter.updateCurrentKey(this._key);
283
-
284
-        this._e2eeCtx.setKey(this.conference.myUserId(), this._key, index);
61
+        await this._keyHandler.setEnabled(enabled);
285 62
     }
286 63
 
287 64
     /**
288
-     * Rotates the local key. Rotating the key implies creating a new one, then distributing it
289
-     * to all participants and once they all received it, start using it.
65
+     * Sets the key and index for End-to-End encryption.
290 66
      *
291
-     * @private
292
-     */
293
-    async _rotateKeyImpl() {
294
-        logger.debug('Rotating key');
295
-
296
-        this._key = this._generateKey();
297
-        const index = await this._olmAdapter.updateKey(this._key);
298
-
299
-        this._e2eeCtx.setKey(this.conference.myUserId(), this._key, index);
300
-    }
301
-
302
-    /**
303
-     * Setup E2EE for the receiving side.
304
-     *
305
-     * @private
306
-     */
307
-    _setupReceiverE2EEForTrack(tpc, track) {
308
-        if (!this._enabled) {
309
-            return;
310
-        }
311
-
312
-        const receiver = tpc.findReceiverForTrack(track.track);
313
-
314
-        if (receiver) {
315
-            this._e2eeCtx.handleReceiver(receiver, track.getType(), track.getParticipantId());
316
-        } else {
317
-            logger.warn(`Could not handle E2EE for ${track}: receiver not found in: ${tpc}`);
318
-        }
319
-    }
320
-
321
-    /**
322
-     * Setup E2EE for the sending side.
323
-     *
324
-     * @param {JingleSessionPC} session - the session which sends the media produced by the track.
325
-     * @param {JitsiLocalTrack} track - the local track for which e2e encoder will be configured.
326
-     * @private
327
-     */
328
-    _setupSenderE2EEForTrack(session, track) {
329
-        if (!this._enabled) {
330
-            return;
331
-        }
332
-
333
-        const pc = session.peerconnection;
334
-        const sender = pc && pc.findSenderForTrack(track.track);
335
-
336
-        if (sender) {
337
-            this._e2eeCtx.handleSender(sender, track.getType(), track.getParticipantId());
338
-        } else {
339
-            logger.warn(`Could not handle E2EE for ${track}: sender not found in ${pc}`);
340
-        }
341
-    }
342
-
343
-    /**
344
-     * Setup E2EE on the sender that is created for the unmuted local video track.
345
-     * @param {JitsiLocalTrack} track - the track for which muted status has changed.
346
-     * @private
67
+     * @param {CryptoKey} [keyInfo.encryptionKey] - encryption key.
68
+     * @param {Number} [keyInfo.index] - the index of the encryption key.
69
+     * @returns {void}
347 70
      */
348
-    _trackMuteChanged(track) {
349
-        if (browser.doesVideoMuteByStreamRemove() && track.isLocal() && track.isVideoTrack() && !track.isMuted()) {
350
-            for (const session of this.conference._getMediaSessions()) {
351
-                this._setupSenderE2EEForTrack(session, track);
352
-            }
353
-        }
71
+    setEncryptionKey(keyInfo) {
72
+        this._keyHandler.setKey(keyInfo);
354 73
     }
355 74
 }

+ 25
- 0
modules/e2ee/ExternallyManagedKeyHandler.js 查看文件

@@ -0,0 +1,25 @@
1
+import { KeyHandler } from './KeyHandler';
2
+
3
+/**
4
+ * This module integrates {@link E2EEContext} with {external} in order to set the keys for encryption.
5
+ */
6
+export class ExternallyManagedKeyHandler extends KeyHandler {
7
+    /**
8
+     * Build a new ExternallyManagedKeyHandler instance, which will be used in a given conference.
9
+     * @param conference - the current conference.
10
+     */
11
+    constructor(conference) {
12
+        super(conference, { sharedKey: true });
13
+    }
14
+
15
+    /**
16
+     * Sets the key and index for End-to-End encryption.
17
+     *
18
+     * @param {CryptoKey} [keyInfo.encryptionKey] - encryption key.
19
+     * @param {Number} [keyInfo.index] - the index of the encryption key.
20
+     * @returns {void}
21
+     */
22
+    setKey(keyInfo) {
23
+        this.e2eeCtx.setKey(undefined, { encryptionKey: keyInfo.encryptionKey }, keyInfo.index);
24
+    }
25
+}

+ 177
- 0
modules/e2ee/KeyHandler.js 查看文件

@@ -0,0 +1,177 @@
1
+/* global __filename */
2
+
3
+import { getLogger } from 'jitsi-meet-logger';
4
+
5
+import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
6
+import RTCEvents from '../../service/RTC/RTCEvents';
7
+import browser from '../browser';
8
+import Deferred from '../util/Deferred';
9
+import Listenable from '../util/Listenable';
10
+
11
+import E2EEContext from './E2EEContext';
12
+
13
+const logger = getLogger(__filename);
14
+
15
+/**
16
+ * Abstract class that integrates {@link E2EEContext} with a key management system.
17
+ */
18
+export class KeyHandler extends Listenable {
19
+    /**
20
+     * Build a new KeyHandler instance, which will be used in a given conference.
21
+     * @param {JitsiConference} conference - the current conference.
22
+     * @param {object} options - the options passed to {E2EEContext}, see implemention.
23
+     */
24
+    constructor(conference, options = {}) {
25
+        super();
26
+
27
+        this.conference = conference;
28
+        this.e2eeCtx = new E2EEContext(options);
29
+
30
+        this.enabled = false;
31
+        this._enabling = undefined;
32
+
33
+        // Conference media events in order to attach the encryptor / decryptor.
34
+        // FIXME add events to TraceablePeerConnection which will allow to see when there's new receiver or sender
35
+        // added instead of shenanigans around conference track events and track muted.
36
+        //
37
+
38
+        this.conference.on(
39
+            JitsiConferenceEvents._MEDIA_SESSION_STARTED,
40
+            this._onMediaSessionStarted.bind(this));
41
+        this.conference.on(
42
+            JitsiConferenceEvents.TRACK_ADDED,
43
+            track => track.isLocal() && this._onLocalTrackAdded(track));
44
+        this.conference.rtc.on(
45
+            RTCEvents.REMOTE_TRACK_ADDED,
46
+            (track, tpc) => this._setupReceiverE2EEForTrack(tpc, track));
47
+        this.conference.on(
48
+            JitsiConferenceEvents.TRACK_MUTE_CHANGED,
49
+            this._trackMuteChanged.bind(this));
50
+    }
51
+
52
+    /**
53
+     * Indicates whether E2EE is currently enabled or not.
54
+     *
55
+     * @returns {boolean}
56
+     */
57
+    isEnabled() {
58
+        return this.enabled;
59
+    }
60
+
61
+    /**
62
+     * Enables / disables End-To-End encryption.
63
+     *
64
+     * @param {boolean} enabled - whether E2EE should be enabled or not.
65
+     * @returns {void}
66
+     */
67
+    async setEnabled(enabled) {
68
+        if (enabled === this.enabled) {
69
+            return;
70
+        }
71
+
72
+        this._enabling && await this._enabling;
73
+
74
+        this._enabling = new Deferred();
75
+
76
+        this.enabled = enabled;
77
+
78
+        if (!enabled) {
79
+            this.e2eeCtx.cleanupAll();
80
+        }
81
+
82
+        this._setEnabled && await this._setEnabled(enabled);
83
+
84
+        this.conference.setLocalParticipantProperty('e2ee.enabled', enabled);
85
+
86
+        this.conference._restartMediaSessions();
87
+
88
+        this._enabling.resolve();
89
+    }
90
+
91
+    /**
92
+     * Sets the key for End-to-End encryption.
93
+     *
94
+     * @returns {void}
95
+     */
96
+    setEncryptionKey() {
97
+        throw new Error('Not implemented by subclass');
98
+    }
99
+
100
+    /**
101
+     * Setup E2EE on the new track that has been added to the conference, apply it on all the open peerconnections.
102
+     * @param {JitsiLocalTrack} track - the new track that's being added to the conference.
103
+     * @private
104
+     */
105
+    _onLocalTrackAdded(track) {
106
+        for (const session of this.conference._getMediaSessions()) {
107
+            this._setupSenderE2EEForTrack(session, track);
108
+        }
109
+    }
110
+
111
+    /**
112
+     * Setups E2E encryption for the new session.
113
+     * @param {JingleSessionPC} session - the new media session.
114
+     * @private
115
+     */
116
+    _onMediaSessionStarted(session) {
117
+        const localTracks = this.conference.getLocalTracks();
118
+
119
+        for (const track of localTracks) {
120
+            this._setupSenderE2EEForTrack(session, track);
121
+        }
122
+    }
123
+
124
+    /**
125
+     * Setup E2EE for the receiving side.
126
+     *
127
+     * @private
128
+     */
129
+    _setupReceiverE2EEForTrack(tpc, track) {
130
+        if (!this.enabled) {
131
+            return;
132
+        }
133
+
134
+        const receiver = tpc.findReceiverForTrack(track.track);
135
+
136
+        if (receiver) {
137
+            this.e2eeCtx.handleReceiver(receiver, track.getType(), track.getParticipantId());
138
+        } else {
139
+            logger.warn(`Could not handle E2EE for ${track}: receiver not found in: ${tpc}`);
140
+        }
141
+    }
142
+
143
+    /**
144
+     * Setup E2EE for the sending side.
145
+     *
146
+     * @param {JingleSessionPC} session - the session which sends the media produced by the track.
147
+     * @param {JitsiLocalTrack} track - the local track for which e2e encoder will be configured.
148
+     * @private
149
+     */
150
+    _setupSenderE2EEForTrack(session, track) {
151
+        if (!this.enabled) {
152
+            return;
153
+        }
154
+
155
+        const pc = session.peerconnection;
156
+        const sender = pc && pc.findSenderForTrack(track.track);
157
+
158
+        if (sender) {
159
+            this.e2eeCtx.handleSender(sender, track.getType(), track.getParticipantId());
160
+        } else {
161
+            logger.warn(`Could not handle E2EE for ${track}: sender not found in ${pc}`);
162
+        }
163
+    }
164
+
165
+    /**
166
+     * Setup E2EE on the sender that is created for the unmuted local video track.
167
+     * @param {JitsiLocalTrack} track - the track for which muted status has changed.
168
+     * @private
169
+     */
170
+    _trackMuteChanged(track) {
171
+        if (browser.doesVideoMuteByStreamRemove() && track.isLocal() && track.isVideoTrack() && !track.isMuted()) {
172
+            for (const session of this.conference._getMediaSessions()) {
173
+                this._setupSenderE2EEForTrack(session, track);
174
+            }
175
+        }
176
+    }
177
+}

+ 181
- 0
modules/e2ee/ManagedKeyHandler.js 查看文件

@@ -0,0 +1,181 @@
1
+/* global __filename */
2
+
3
+import { getLogger } from 'jitsi-meet-logger';
4
+import debounce from 'lodash.debounce';
5
+
6
+import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
7
+
8
+import { KeyHandler } from './KeyHandler';
9
+import { OlmAdapter } from './OlmAdapter';
10
+import { importKey, ratchet } from './crypto-utils';
11
+
12
+const logger = getLogger(__filename);
13
+
14
+// Period which we'll wait before updating / rotating our keys when a participant
15
+// joins or leaves.
16
+const DEBOUNCE_PERIOD = 5000;
17
+
18
+/**
19
+ * This module integrates {@link E2EEContext} with {@link OlmAdapter} in order to distribute the keys for encryption.
20
+ */
21
+export class ManagedKeyHandler extends KeyHandler {
22
+    /**
23
+     * Build a new AutomaticKeyHandler instance, which will be used in a given conference.
24
+     */
25
+    constructor(conference) {
26
+        super(conference);
27
+
28
+        this._key = undefined;
29
+        this._conferenceJoined = false;
30
+
31
+        this._olmAdapter = new OlmAdapter(conference);
32
+
33
+        this._rotateKey = debounce(this._rotateKeyImpl, DEBOUNCE_PERIOD);
34
+        this._ratchetKey = debounce(this._ratchetKeyImpl, DEBOUNCE_PERIOD);
35
+
36
+        // Olm signalling events.
37
+        this._olmAdapter.on(
38
+            OlmAdapter.events.PARTICIPANT_KEY_UPDATED,
39
+            this._onParticipantKeyUpdated.bind(this));
40
+
41
+        this.conference.on(
42
+            JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
43
+            this._onParticipantPropertyChanged.bind(this));
44
+        this.conference.on(
45
+            JitsiConferenceEvents.USER_JOINED,
46
+            this._onParticipantJoined.bind(this));
47
+        this.conference.on(
48
+            JitsiConferenceEvents.USER_LEFT,
49
+            this._onParticipantLeft.bind(this));
50
+        this.conference.on(
51
+                JitsiConferenceEvents.CONFERENCE_JOINED,
52
+                () => {
53
+                    this._conferenceJoined = true;
54
+                });
55
+    }
56
+
57
+    /**
58
+     * When E2EE is enabled it initializes sessions and sets the key.
59
+     * Cleans up the sessions when disabled.
60
+     *
61
+     * @param {boolean} enabled - whether E2EE should be enabled or not.
62
+     * @returns {void}
63
+     */
64
+    async _setEnabled(enabled) {
65
+        if (enabled) {
66
+            await this._olmAdapter.initSessions();
67
+        } else {
68
+            this._olmAdapter.clearAllParticipantsSessions();
69
+        }
70
+
71
+        // Generate a random key in case we are enabling.
72
+        this._key = enabled ? this._generateKey() : false;
73
+
74
+        // Send it to others using the E2EE olm channel.
75
+        const index = await this._olmAdapter.updateKey(this._key);
76
+
77
+        // Set our key so we begin encrypting.
78
+        this.e2eeCtx.setKey(this.conference.myUserId(), this._key, index);
79
+    }
80
+
81
+    /**
82
+     * Handles an update in a participant's presence property.
83
+     *
84
+     * @param {JitsiParticipant} participant - The participant.
85
+     * @param {string} name - The name of the property that changed.
86
+     * @param {*} oldValue - The property's previous value.
87
+     * @param {*} newValue - The property's new value.
88
+     * @private
89
+     */
90
+    async _onParticipantPropertyChanged(participant, name, oldValue, newValue) {
91
+        switch (name) {
92
+        case 'e2ee.idKey':
93
+            logger.debug(`Participant ${participant.getId()} updated their id key: ${newValue}`);
94
+            break;
95
+        case 'e2ee.enabled':
96
+            if (!newValue && this.enabled) {
97
+                this._olmAdapter.clearParticipantSession(participant);
98
+            }
99
+            break;
100
+        }
101
+    }
102
+
103
+    /**
104
+     * Advances (using ratcheting) the current key when a new participant joins the conference.
105
+     * @private
106
+     */
107
+    _onParticipantJoined() {
108
+        if (this._conferenceJoined && this.enabled) {
109
+            this._ratchetKey();
110
+        }
111
+    }
112
+
113
+    /**
114
+     * Rotates the current key when a participant leaves the conference.
115
+     * @private
116
+     */
117
+    _onParticipantLeft(id) {
118
+        this.e2eeCtx.cleanup(id);
119
+
120
+        if (this.enabled) {
121
+            this._rotateKey();
122
+        }
123
+    }
124
+
125
+    /**
126
+     * Rotates the local key. Rotating the key implies creating a new one, then distributing it
127
+     * to all participants and once they all received it, start using it.
128
+     *
129
+     * @private
130
+     */
131
+    async _rotateKeyImpl() {
132
+        logger.debug('Rotating key');
133
+
134
+        this._key = this._generateKey();
135
+        const index = await this._olmAdapter.updateKey(this._key);
136
+
137
+        this.e2eeCtx.setKey(this.conference.myUserId(), this._key, index);
138
+    }
139
+
140
+    /**
141
+     * Advances the current key by using ratcheting.
142
+     *
143
+     * @private
144
+     */
145
+    async _ratchetKeyImpl() {
146
+        logger.debug('Ratchetting key');
147
+
148
+        const material = await importKey(this._key);
149
+        const newKey = await ratchet(material);
150
+
151
+        this._key = new Uint8Array(newKey);
152
+
153
+        const index = this._olmAdapter.updateCurrentKey(this._key);
154
+
155
+        this.e2eeCtx.setKey(this.conference.myUserId(), this._key, index);
156
+    }
157
+
158
+    /**
159
+     * Handles an update in a participant's key.
160
+     *
161
+     * @param {string} id - The participant ID.
162
+     * @param {Uint8Array | boolean} key - The new key for the participant.
163
+     * @param {Number} index - The new key's index.
164
+     * @private
165
+     */
166
+    _onParticipantKeyUpdated(id, key, index) {
167
+        logger.debug(`Participant ${id} updated their key`);
168
+
169
+        this.e2eeCtx.setKey(id, key, index);
170
+    }
171
+
172
+    /**
173
+     * Generates a new 256 bit random key.
174
+     *
175
+     * @returns {Uint8Array}
176
+     * @private
177
+     */
178
+    _generateKey() {
179
+        return window.crypto.getRandomValues(new Uint8Array(32));
180
+    }
181
+}

+ 24
- 5
modules/e2ee/OlmAdapter.js 查看文件

@@ -230,7 +230,7 @@ export class OlmAdapter extends Listenable {
230 230
 
231 231
             logger.debug(`Olm ${Olm.get_library_version().join('.')} initialized`);
232 232
             this._init.resolve();
233
-            this.eventEmitter.emit(OlmAdapterEvents.OLM_ID_KEY_READY, this._idKey);
233
+            this._onIdKeyReady(this._idKey);
234 234
         } catch (e) {
235 235
             logger.error('Failed to initialize Olm', e);
236 236
             this._init.reject(e);
@@ -238,6 +238,25 @@ export class OlmAdapter extends Listenable {
238 238
 
239 239
     }
240 240
 
241
+    /**
242
+     * Publishes our own Olmn id key in presence.
243
+     * @private
244
+     */
245
+    _onIdKeyReady(idKey) {
246
+        logger.debug(`Olm id key ready: ${idKey}`);
247
+
248
+        // Publish it in presence.
249
+        this._conf.setLocalParticipantProperty('e2ee.idKey', idKey);
250
+    }
251
+
252
+    /**
253
+     * Event posted when the E2EE signalling channel has been established with the given participant.
254
+     * @private
255
+     */
256
+    _onParticipantE2EEChannelReady(id) {
257
+        logger.debug(`E2EE channel with participant ${id} is ready`);
258
+    }
259
+
241 260
     /**
242 261
      * Internal helper for encrypting the current key information for a given participant.
243 262
      *
@@ -339,7 +358,7 @@ export class OlmAdapter extends Listenable {
339 358
                 };
340 359
 
341 360
                 this._sendMessage(ack, pId);
342
-                this.eventEmitter.emit(OlmAdapterEvents.PARTICIPANT_E2EE_CHANNEL_READY, pId);
361
+                this._onParticipantE2EEChannelReady(pId);
343 362
             }
344 363
             break;
345 364
         }
@@ -364,7 +383,7 @@ export class OlmAdapter extends Listenable {
364 383
                 olmData.session = session;
365 384
                 olmData.pendingSessionUuid = undefined;
366 385
 
367
-                this.eventEmitter.emit(OlmAdapterEvents.PARTICIPANT_E2EE_CHANNEL_READY, pId);
386
+                this._onParticipantE2EEChannelReady(pId);
368 387
 
369 388
                 this._reqs.delete(msg.data.uuid);
370 389
                 d.resolve();
@@ -611,8 +630,6 @@ export class OlmAdapter extends Listenable {
611 630
     }
612 631
 }
613 632
 
614
-OlmAdapter.events = OlmAdapterEvents;
615
-
616 633
 /**
617 634
  * Helper to ensure JSON parsing always returns an object.
618 635
  *
@@ -626,3 +643,5 @@ function safeJsonParse(data) {
626 643
         return {};
627 644
     }
628 645
 }
646
+
647
+OlmAdapter.events = OlmAdapterEvents;

+ 16
- 2
modules/e2ee/Worker.js 查看文件

@@ -7,6 +7,8 @@ import { Context } from './Context';
7 7
 
8 8
 const contexts = new Map(); // Map participant id => context
9 9
 
10
+let sharedContext;
11
+
10 12
 /**
11 13
  * Retrieves the participant {@code Context}, creating it if necessary.
12 14
  *
@@ -14,8 +16,12 @@ const contexts = new Map(); // Map participant id => context
14 16
  * @returns {Object} The context.
15 17
  */
16 18
 function getParticipantContext(participantId) {
19
+    if (sharedContext) {
20
+        return sharedContext;
21
+    }
22
+
17 23
     if (!contexts.has(participantId)) {
18
-        contexts.set(participantId, new Context(participantId));
24
+        contexts.set(participantId, new Context());
19 25
     }
20 26
 
21 27
     return contexts.get(participantId);
@@ -47,7 +53,13 @@ function handleTransform(context, operation, readableStream, writableStream) {
47 53
 onmessage = async event => {
48 54
     const { operation } = event.data;
49 55
 
50
-    if (operation === 'encode' || operation === 'decode') {
56
+    if (operation === 'initialize') {
57
+        const { sharedKey } = event.data;
58
+
59
+        if (sharedKey) {
60
+            sharedContext = new Context({ sharedKey });
61
+        }
62
+    } else if (operation === 'encode' || operation === 'decode') {
51 63
         const { readableStream, writableStream, participantId } = event.data;
52 64
         const context = getParticipantContext(participantId);
53 65
 
@@ -65,6 +77,8 @@ onmessage = async event => {
65 77
         const { participantId } = event.data;
66 78
 
67 79
         contexts.delete(participantId);
80
+    } else if (operation === 'cleanupAll') {
81
+        contexts.clear();
68 82
     } else {
69 83
         console.error('e2ee worker', operation);
70 84
     }

Loading…
取消
儲存