Browse Source

feat(e2ee) add ExternallyManagedKeyHandler

dev1
tmoldovan8x8 4 years ago
parent
commit
afc006e99a
No account linked to committer's email address

+ 11
- 0
JitsiConference.js View File

3694
     this._e2eEncryption.setEnabled(enabled);
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
  * Returns <tt>true</tt> if lobby support is enabled in the backend.
3709
  * Returns <tt>true</tt> if lobby support is enabled in the backend.
3699
  *
3710
  *

+ 28
- 21
modules/e2ee/Context.js View File

37
  */
37
  */
38
 export class Context {
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
         // An array (ring) of keys that we use for sending and receiving.
43
         // An array (ring) of keys that we use for sending and receiving.
44
         this._cryptoKeyRing = new Array(KEYRING_SIZE);
44
         this._cryptoKeyRing = new Array(KEYRING_SIZE);
45
 
45
 
48
 
48
 
49
         this._sendCounts = new Map();
49
         this._sendCounts = new Map();
50
 
50
 
51
-        this._id = id;
51
+        this._sharedKey = sharedKey;
52
     }
52
     }
53
 
53
 
54
     /**
54
     /**
57
      * @param {Uint8Array|false} key bytes. Pass false to disable.
57
      * @param {Uint8Array|false} key bytes. Pass false to disable.
58
      * @param {Number} keyIndex
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
      */
82
      */
81
     _setKeys(keys, keyIndex = -1) {
83
     _setKeys(keys, keyIndex = -1) {
82
         if (keyIndex >= 0) {
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
         this._sendCount = BigInt(0); // eslint-disable-line new-cap
90
         this._sendCount = BigInt(0); // eslint-disable-line new-cap
88
     }
91
     }
89
 
92
 
251
 
254
 
252
             encodedFrame.data = newData;
255
             encodedFrame.data = newData;
253
         } catch (error) {
256
         } catch (error) {
257
+            if (this._sharedKey) {
258
+                return encodedFrame;
259
+            }
260
+
254
             if (ratchetCount < RATCHET_WINDOW_SIZE) {
261
             if (ratchetCount < RATCHET_WINDOW_SIZE) {
255
                 material = await importKey(await ratchet(material));
262
                 material = await importKey(await ratchet(material));
256
 
263
 
265
                     ratchetCount + 1);
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
             this._setKeys(initialKey);
281
             this._setKeys(initialKey);
275
 
282
 
276
             // TODO: notify the application about error status.
283
             // TODO: notify the application about error status.

+ 20
- 3
modules/e2ee/E2EEContext.js View File

23
 export default class E2EEcontext {
23
 export default class E2EEcontext {
24
     /**
24
     /**
25
      * Build a new E2EE context instance, which will be used in a given conference.
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
         // Determine the URL for the worker script. Relative URLs are relative to
29
         // Determine the URL for the worker script. Relative URLs are relative to
29
         // the entry point, not the script that launches the worker.
30
         // the entry point, not the script that launches the worker.
30
         let baseUrl = '';
31
         let baseUrl = '';
44
         const blobUrl = window.URL.createObjectURL(workerBlob);
45
         const blobUrl = window.URL.createObjectURL(workerBlob);
45
 
46
 
46
         this._worker = new Worker(blobUrl, { name: 'E2EE Worker' });
47
         this._worker = new Worker(blobUrl, { name: 'E2EE Worker' });
48
+
47
         this._worker.onerror = e => logger.error(e);
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
         });
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
      * Handles the given {@code RTCRtpReceiver} by creating a {@code TransformStream} which will inject
81
      * Handles the given {@code RTCRtpReceiver} by creating a {@code TransformStream} which will inject
65
      * a frame decoder.
82
      * a frame decoder.
136
     setKey(participantId, key, keyIndex) {
153
     setKey(participantId, key, keyIndex) {
137
         this._worker.postMessage({
154
         this._worker.postMessage({
138
             operation: 'setKey',
155
             operation: 'setKey',
139
-            participantId,
140
             key,
156
             key,
141
-            keyIndex
157
+            keyIndex,
158
+            participantId
142
         });
159
         });
143
     }
160
     }
144
 }
161
 }

+ 25
- 306
modules/e2ee/E2EEncryption.js View File

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
 import browser from '../browser';
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
 import { OlmAdapter } from './OlmAdapter';
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
 export class E2EEncryption {
10
 export class E2EEncryption {
25
     /**
11
     /**
27
      * @param {JitsiConference} conference - The conference instance for which E2E encryption is to be enabled.
13
      * @param {JitsiConference} conference - The conference instance for which E2E encryption is to be enabled.
28
      */
14
      */
29
     constructor(conference) {
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
      * @returns {boolean}
31
      * @returns {boolean}
97
      */
32
      */
98
     static isSupported(config) {
33
     static isSupported(config) {
34
+        const { e2ee = {} } = config;
35
+
36
+        if (!e2ee.externallyManagedKey && !OlmAdapter.isSupported()) {
37
+            return false;
38
+        }
39
+
99
         return !(config.testing && config.testing.disableE2EE)
40
         return !(config.testing && config.testing.disableE2EE)
100
             && (browser.supportsInsertableStreams()
41
             && (browser.supportsInsertableStreams()
101
-                || (config.enableEncodedTransformSupport && browser.supportsEncodedTransform()))
102
-            && OlmAdapter.isSupported();
42
+                || (config.enableEncodedTransformSupport && browser.supportsEncodedTransform()));
103
     }
43
     }
104
 
44
 
105
     /**
45
     /**
108
      * @returns {boolean}
48
      * @returns {boolean}
109
      */
49
      */
110
     isEnabled() {
50
     isEnabled() {
111
-        return this._enabled;
51
+        return this._keyHandler.isEnabled();
112
     }
52
     }
113
 
53
 
114
     /**
54
     /**
118
      * @returns {void}
58
      * @returns {void}
119
      */
59
      */
120
     async setEnabled(enabled) {
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 View File

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 View File

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 View File

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 View File

230
 
230
 
231
             logger.debug(`Olm ${Olm.get_library_version().join('.')} initialized`);
231
             logger.debug(`Olm ${Olm.get_library_version().join('.')} initialized`);
232
             this._init.resolve();
232
             this._init.resolve();
233
-            this.eventEmitter.emit(OlmAdapterEvents.OLM_ID_KEY_READY, this._idKey);
233
+            this._onIdKeyReady(this._idKey);
234
         } catch (e) {
234
         } catch (e) {
235
             logger.error('Failed to initialize Olm', e);
235
             logger.error('Failed to initialize Olm', e);
236
             this._init.reject(e);
236
             this._init.reject(e);
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
      * Internal helper for encrypting the current key information for a given participant.
261
      * Internal helper for encrypting the current key information for a given participant.
243
      *
262
      *
339
                 };
358
                 };
340
 
359
 
341
                 this._sendMessage(ack, pId);
360
                 this._sendMessage(ack, pId);
342
-                this.eventEmitter.emit(OlmAdapterEvents.PARTICIPANT_E2EE_CHANNEL_READY, pId);
361
+                this._onParticipantE2EEChannelReady(pId);
343
             }
362
             }
344
             break;
363
             break;
345
         }
364
         }
364
                 olmData.session = session;
383
                 olmData.session = session;
365
                 olmData.pendingSessionUuid = undefined;
384
                 olmData.pendingSessionUuid = undefined;
366
 
385
 
367
-                this.eventEmitter.emit(OlmAdapterEvents.PARTICIPANT_E2EE_CHANNEL_READY, pId);
386
+                this._onParticipantE2EEChannelReady(pId);
368
 
387
 
369
                 this._reqs.delete(msg.data.uuid);
388
                 this._reqs.delete(msg.data.uuid);
370
                 d.resolve();
389
                 d.resolve();
611
     }
630
     }
612
 }
631
 }
613
 
632
 
614
-OlmAdapter.events = OlmAdapterEvents;
615
-
616
 /**
633
 /**
617
  * Helper to ensure JSON parsing always returns an object.
634
  * Helper to ensure JSON parsing always returns an object.
618
  *
635
  *
626
         return {};
643
         return {};
627
     }
644
     }
628
 }
645
 }
646
+
647
+OlmAdapter.events = OlmAdapterEvents;

+ 16
- 2
modules/e2ee/Worker.js View File

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

Loading…
Cancel
Save