Browse Source

e2ee: introduce per-participant randomly generated keys

This the second stage in our E2EE journey.

Instead of using a single pre-shared passphrase for deriving the key used for
E2EE, we now establish a secure E2EE communication channel amongst peers.

This channel is implemented using libolm, using XMPP groupchat or JVB channels
as the transport.

Once the secure E2EE channel has been established each participant will generate
a random 32 byte key and exchange it over this channel.

Keys are rotated (well, just re-created at the moment) when a participant joins
or leaves.
master
Saúl Ibarra Corretgé 4 years ago
parent
commit
735c30ec4f

+ 51
- 44
JitsiConference.js View File

@@ -241,6 +241,15 @@ export default function JitsiConference(options) {
241 241
      * @private
242 242
      */
243 243
     this._conferenceJoinAnalyticsEventSent = undefined;
244
+
245
+    /**
246
+     * End-to-End Encryption. Make it available if supported.
247
+     */
248
+    if (this.isE2EESupported()) {
249
+        logger.info('End-to-End Encryprtion is supported');
250
+
251
+        this._e2eEncryption = new E2EEncryption(this);
252
+    }
244 253
 }
245 254
 
246 255
 // FIXME convert JitsiConference to ES6 - ASAP !
@@ -1376,9 +1385,7 @@ JitsiConference.prototype.isInLastN = function(participantId) {
1376 1385
  * conference.
1377 1386
  */
1378 1387
 JitsiConference.prototype.getParticipants = function() {
1379
-    return Object.keys(this.participants).map(function(key) {
1380
-        return this.participants[key];
1381
-    }, this);
1388
+    return Object.values(this.participants);
1382 1389
 };
1383 1390
 
1384 1391
 /**
@@ -1910,7 +1917,7 @@ JitsiConference.prototype._acceptJvbIncomingCall = function(
1910 1917
     try {
1911 1918
         jingleSession.initialize(this.room, this.rtc, {
1912 1919
             ...this.options.config,
1913
-            enableInsertableStreams: Boolean(this._e2eEncryption)
1920
+            enableInsertableStreams: this._isE2EEEnabled()
1914 1921
         });
1915 1922
     } catch (error) {
1916 1923
         GlobalOnErrorHandler.callErrorHandler(error);
@@ -2651,7 +2658,7 @@ JitsiConference.prototype._acceptP2PIncomingCall = function(
2651 2658
         this.room,
2652 2659
         this.rtc, {
2653 2660
             ...this.options.config,
2654
-            enableInsertableStreams: Boolean(this._e2eEncryption)
2661
+            enableInsertableStreams: this._isE2EEEnabled()
2655 2662
         });
2656 2663
 
2657 2664
     logger.info('Starting CallStats for P2P connection...');
@@ -3012,7 +3019,7 @@ JitsiConference.prototype._startP2PSession = function(remoteJid) {
3012 3019
         this.room,
3013 3020
         this.rtc, {
3014 3021
             ...this.options.config,
3015
-            enableInsertableStreams: Boolean(this._e2eEncryption)
3022
+            enableInsertableStreams: this._isE2EEEnabled()
3016 3023
         });
3017 3024
 
3018 3025
     logger.info('Starting CallStats for P2P connection...');
@@ -3374,64 +3381,64 @@ JitsiConference.prototype._sendConferenceLeftAnalyticsEvent = function() {
3374 3381
 };
3375 3382
 
3376 3383
 /**
3377
- * Returns whether End-To-End encryption is supported. Note that not all participants
3378
- * in the conference may support it.
3384
+ * Restarts all active media sessions.
3379 3385
  *
3380
- * @returns {boolean}
3386
+ * @returns {void}
3381 3387
  */
3382
-JitsiConference.prototype.isE2EESupported = function() {
3383
-    const config = this.options.config;
3388
+JitsiConference.prototype._restartMediaSessions = function() {
3389
+    if (this.p2pJingleSession) {
3390
+        this.stopP2PSession();
3391
+    }
3392
+
3393
+    if (this.jvbJingleSession) {
3394
+        this.jvbJingleSession.terminate(
3395
+            null /* success callback => we don't care */,
3396
+            error => {
3397
+                logger.warn('An error occurred while trying to terminate the JVB session', error);
3398
+            }, {
3399
+                reason: 'success',
3400
+                reasonDescription: 'restart required',
3401
+                requestRestart: true,
3402
+                sendSessionTerminate: true
3403
+            });
3404
+    }
3384 3405
 
3385
-    return browser.supportsInsertableStreams() && !(config.testing && config.testing.disableE2EE);
3406
+    this._maybeStartOrStopP2P(false);
3386 3407
 };
3387 3408
 
3388 3409
 /**
3389
- * Initializes the E2E encryption module. Currently any active media session muste be restarted due to
3390
- * the limitation that the insertable streams constraint can only be set when a new PeerConnection instance is created.
3410
+ * Returns whether End-To-End encryption is enabled.
3391 3411
  *
3392
- * @private
3393
- * @returns {void}
3412
+ * @returns {boolean}
3394 3413
  */
3395
-JitsiConference.prototype._initializeE2EEncryption = function() {
3396
-    this._e2eEncryption = new E2EEncryption(this, { salt: this.options.name });
3397
-
3398
-    // Need to re-create the peerconnections in order to apply the insertable streams constraint
3399
-    this.p2pJingleSession && this.stopP2PSession();
3400
-
3401
-    const jvbJingleSession = this.jvbJingleSession;
3402
-
3403
-    jvbJingleSession && jvbJingleSession.terminate(
3404
-        null /* success callback => we don't care */,
3405
-        error => {
3406
-            logger.warn(`An error occurred while trying to terminate ${jvbJingleSession}`, error);
3407
-        }, {
3408
-            reason: 'success',
3409
-            reasonDescription: 'restart required',
3410
-            requestRestart: true,
3411
-            sendSessionTerminate: true
3412
-        });
3414
+JitsiConference.prototype._isE2EEEnabled = function() {
3415
+    return this._e2eEncryption && this._e2eEncryption.isEnabled();
3416
+};
3413 3417
 
3414
-    this._maybeStartOrStopP2P(false);
3418
+/**
3419
+ * Returns whether End-To-End encryption is supported. Note that not all participants
3420
+ * in the conference may support it.
3421
+ *
3422
+ * @returns {boolean}
3423
+ */
3424
+JitsiConference.prototype.isE2EESupported = function() {
3425
+    return E2EEncryption.isSupported(this.options.config);
3415 3426
 };
3416 3427
 
3417 3428
 /**
3418
- * Sets the key to be used for End-To-End encryption.
3429
+ * Enables / disables End-to-End encryption.
3419 3430
  *
3420
- * @param {string} key the key to be used.
3431
+ * @param {boolean} enabled whether to enable E2EE or not.
3421 3432
  * @returns {void}
3422 3433
  */
3423
-JitsiConference.prototype.setE2EEKey = function(key) {
3434
+JitsiConference.prototype.toggleE2EE = function(enabled) {
3424 3435
     if (!this.isE2EESupported()) {
3425
-        logger.warn('Cannot set E2EE key: platform is not supported.');
3436
+        logger.warn('Cannot enable / disable E2EE: platform is not supported.');
3426 3437
 
3427 3438
         return;
3428 3439
     }
3429 3440
 
3430
-    if (!this._e2eEncryption) {
3431
-        this._initializeE2EEncryption();
3432
-    }
3433
-
3434
-    this._e2eEncryption.setKey(key);
3441
+    this._e2eEncryption.setEnabled(enabled);
3435 3442
 };
3436 3443
 
3437 3444
 /**

+ 22
- 15
modules/e2ee/E2EEContext.js View File

@@ -34,7 +34,7 @@ export default class E2EEcontext {
34 34
     constructor(options) {
35 35
         this._options = options;
36 36
 
37
-        // Figure out the URL for the worker script. Relative URLs are relative to
37
+        // Determine the URL for the worker script. Relative URLs are relative to
38 38
         // the entry point, not the script that launches the worker.
39 39
         let baseUrl = '';
40 40
         const ljm = document.querySelector('script[src*="lib-jitsi-meet"]');
@@ -59,6 +59,19 @@ export default class E2EEcontext {
59 59
         });
60 60
     }
61 61
 
62
+    /**
63
+     * Cleans up all state associated with the given participant. This is needed when a
64
+     * participant leaves the current conference.
65
+     *
66
+     * @param {string} participantId - The participant that just left.
67
+     */
68
+    cleanup(participantId) {
69
+        this._worker.postMessage({
70
+            operation: 'cleanup',
71
+            participantId
72
+        });
73
+    }
74
+
62 75
     /**
63 76
      * Handles the given {@code RTCRtpReceiver} by creating a {@code TransformStream} which will inject
64 77
      * a frame decoder.
@@ -124,24 +137,18 @@ export default class E2EEcontext {
124 137
     }
125 138
 
126 139
     /**
127
-     * Sets the key to be used for E2EE.
140
+     * Set the E2EE key for the specified participant.
128 141
      *
129
-     * @param {string} value - Value to be used as the new key. May be falsy to disable end-to-end encryption.
142
+     * @param {string} participantId - the ID of the participant who's key we are setting.
143
+     * @param {Uint8Array | boolean} key - they key for the given participant.
144
+     * @param {Number} keyIndex - the key index.
130 145
      */
131
-    setKey(value) {
132
-        let key;
133
-
134
-        if (value) {
135
-            const encoder = new TextEncoder();
136
-
137
-            key = encoder.encode(value);
138
-        } else {
139
-            key = false;
140
-        }
141
-
146
+    setKey(participantId, key, keyIndex) {
142 147
         this._worker.postMessage({
143 148
             operation: 'setKey',
144
-            key
149
+            participantId,
150
+            key,
151
+            keyIndex
145 152
         });
146 153
     }
147 154
 }

+ 198
- 19
modules/e2ee/E2EEncryption.js View File

@@ -1,14 +1,21 @@
1 1
 /* global __filename */
2
+
2 3
 import { getLogger } from 'jitsi-meet-logger';
4
+import debounce from 'lodash.debounce';
3 5
 
4 6
 import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
5 7
 import RTCEvents from '../../service/RTC/RTCEvents';
6 8
 import browser from '../browser';
7 9
 
8 10
 import E2EEContext from './E2EEContext';
11
+import { OlmAdapter } from './OlmAdapter';
9 12
 
10 13
 const logger = getLogger(__filename);
11 14
 
15
+// Period which we'll wait before updating / rotating our keys when a participant
16
+// joins or leaves.
17
+const DEBOUNCE_PERIOD = 5000;
18
+
12 19
 /**
13 20
  * This module integrates {@link E2EEContext} with {@link JitsiConference} in order to enable E2E encryption.
14 21
  */
@@ -16,18 +23,44 @@ export class E2EEncryption {
16 23
     /**
17 24
      * A constructor.
18 25
      * @param {JitsiConference} conference - The conference instance for which E2E encryption is to be enabled.
19
-     * @param {Object} options
20
-     * @param {string} options.salt - Salt to be used for key deviation. Check {@link E2EEContext} for more details.
21 26
      */
22
-    constructor(conference, { salt }) {
27
+    constructor(conference) {
23 28
         this.conference = conference;
24
-        this._e2eeCtx = new E2EEContext({ salt });
29
+
30
+        this._conferenceJoined = false;
31
+        this._enabled = false;
32
+        this._initialized = false;
33
+
34
+        this._e2eeCtx = new E2EEContext({ salt: conference.getName() });
35
+        this._olmAdapter = new OlmAdapter(conference);
36
+
37
+        // Debounce key rotation / ratcheting to avoid a storm of messages.
38
+        this._ratchetKey = debounce(this._ratchetKeyImpl, DEBOUNCE_PERIOD);
39
+        this._rotateKey = debounce(this._rotateKeyImpl, DEBOUNCE_PERIOD);
40
+
41
+        // Participant join / leave operations. Used for key advancement / rotation.
42
+        //
43
+
25 44
         this.conference.on(
26
-            JitsiConferenceEvents._MEDIA_SESSION_STARTED,
27
-            this._onMediaSessionStarted.bind(this));
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
+            });
28 55
 
56
+        // Conference media events in order to attach the encryptor / decryptor.
29 57
         // FIXME add events to TraceablePeerConnection which will allow to see when there's new receiver or sender
30
-        //  added instead of shenanigans around conference track events and track muted.
58
+        // added instead of shenanigans around conference track events and track muted.
59
+        //
60
+
61
+        this.conference.on(
62
+            JitsiConferenceEvents._MEDIA_SESSION_STARTED,
63
+            this._onMediaSessionStarted.bind(this));
31 64
         this.conference.on(
32 65
             JitsiConferenceEvents.TRACK_ADDED,
33 66
             track => track.isLocal() && this._onLocalTrackAdded(track));
@@ -37,6 +70,89 @@ export class E2EEncryption {
37 70
         this.conference.on(
38 71
             JitsiConferenceEvents.TRACK_MUTE_CHANGED,
39 72
             this._trackMuteChanged.bind(this));
73
+
74
+        // Olm signalling events.
75
+        this._olmAdapter.on(
76
+            OlmAdapter.events.PARTICIPANT_E2EE_CHANNEL_READY,
77
+            this._onParticipantE2EEChannelReady.bind(this));
78
+        this._olmAdapter.on(
79
+            OlmAdapter.events.PARTICIPANT_KEY_UPDATED,
80
+            this._onParticipantKeyUpdated.bind(this));
81
+    }
82
+
83
+    /**
84
+     * Indicates if E2EE is supported in the current platform.
85
+     *
86
+     * @param {object} config - Global configuration.
87
+     * @returns {boolean}
88
+     */
89
+    static isSupported(config) {
90
+        return browser.supportsInsertableStreams()
91
+            && OlmAdapter.isSupported()
92
+            && !(config.testing && config.testing.disableE2EE);
93
+    }
94
+
95
+    /**
96
+     * Indicates whether E2EE is currently enabled or not.
97
+     *
98
+     * @returns {boolean}
99
+     */
100
+    isEnabled() {
101
+        return this._enabled;
102
+    }
103
+
104
+    /**
105
+     * Enables / disables End-To-End encryption.
106
+     *
107
+     * @param {boolean} enabled - whether E2EE should be enabled or not.
108
+     * @returns {void}
109
+     */
110
+    setEnabled(enabled) {
111
+        if (enabled === this._enabled) {
112
+            return;
113
+        }
114
+
115
+        this._enabled = enabled;
116
+
117
+        if (!this._initialized && enabled) {
118
+            // Need to re-create the peerconnections in order to apply the insertable streams constraint.
119
+            // TODO: this was necessary due to some audio issues when indertable streams are used
120
+            // even though encryption is not performed. This should be fixed in the browser eventually.
121
+            // https://bugs.chromium.org/p/chromium/issues/detail?id=1103280
122
+            this.conference._restartMediaSessions();
123
+
124
+            this._initialized = true;
125
+        }
126
+
127
+        // Generate a random key in case we are enabling.
128
+        const key = enabled ? this._generateKey() : false;
129
+
130
+        // Send it to others using the E2EE olm channel.
131
+        this._olmAdapter.updateKey(key).then(index => {
132
+            // Set our key so we begin encrypting.
133
+            this._e2eeCtx.setKey(this.conference.myUserId(), key, index);
134
+        });
135
+    }
136
+
137
+    /**
138
+     * Generates a new 256 bit random key.
139
+     *
140
+     * @returns {Uint8Array}
141
+     * @private
142
+     */
143
+    _generateKey() {
144
+        return window.crypto.getRandomValues(new Uint8Array(32));
145
+    }
146
+
147
+    /**
148
+     * Setup E2EE on the new track that has been added to the conference, apply it on all the open peerconnections.
149
+     * @param {JitsiLocalTrack} track - the new track that's being added to the conference.
150
+     * @private
151
+     */
152
+    _onLocalTrackAdded(track) {
153
+        for (const session of this.conference._getMediaSessions()) {
154
+            this._setupSenderE2EEForTrack(session, track);
155
+        }
40 156
     }
41 157
 
42 158
     /**
@@ -53,32 +169,91 @@ export class E2EEncryption {
53 169
     }
54 170
 
55 171
     /**
56
-     * Setup E2EE on the new track that has been added to the conference, apply it on all the open peerconnections.
57
-     * @param {JitsiLocalTrack} track - the new track that's being added to the conference.
172
+     * Advances (using ratcheting) the current key whern a new participant joins the conference.
58 173
      * @private
59 174
      */
60
-    _onLocalTrackAdded(track) {
61
-        for (const session of this.conference._getMediaSessions()) {
62
-            this._setupSenderE2EEForTrack(session, track);
175
+    _onParticipantJoined(id) {
176
+        logger.debug(`Participant ${id} joined`);
177
+
178
+        if (this._conferenceJoined && this._enabled) {
179
+            this._ratchetKey();
63 180
         }
64 181
     }
65 182
 
66 183
     /**
67
-     * Sets the key to be used for End-To-End encryption.
184
+     * Rotates the current key when a participant leaves the conference.
185
+     * @private
186
+     */
187
+    _onParticipantLeft(id) {
188
+        logger.debug(`Participant ${id} left`);
189
+
190
+        this._e2eeCtx.cleanup(id);
191
+
192
+        if (this._enabled) {
193
+            this._rotateKey();
194
+        }
195
+    }
196
+
197
+    /**
198
+     * Event posted when the E2EE signalling channel has been establioshed with the given participant.
199
+     * @private
200
+     */
201
+    _onParticipantE2EEChannelReady(id) {
202
+        logger.debug(`E2EE channel with participant ${id} is ready`);
203
+    }
204
+
205
+    /**
206
+     * Handles an update in a participant's key.
68 207
      *
69
-     * @param {string} key - the key to be used.
70
-     * @returns {void}
208
+     * @param {string} id - The participant ID.
209
+     * @param {Uint8Array | boolean} key - The new key for the participant.
210
+     * @param {Number} index - The new key's index.
211
+     * @private
71 212
      */
72
-    setKey(key) {
73
-        this._e2eeCtx.setKey(key);
213
+    _onParticipantKeyUpdated(id, key, index) {
214
+        logger.debug(`Participant ${id} updated their key`);
215
+
216
+        this._e2eeCtx.setKey(id, key, index);
217
+    }
218
+
219
+    /**
220
+     * Advances the current key by using ratcheting.
221
+     * TODO: not yet implemented, we are just rotating the key at the moment,
222
+     * which is a heavier operation.
223
+     *
224
+     * @private
225
+     */
226
+    async _ratchetKeyImpl() {
227
+        logger.debug('Ratchetting key');
228
+
229
+        return this._rotateKey();
230
+    }
231
+
232
+    /**
233
+     * Rotates the local key. Rotating the key implies creating a new one, then distributing it
234
+     * to all participants and once they all received it, start using it.
235
+     *
236
+     * @private
237
+     */
238
+    async _rotateKeyImpl() {
239
+        logger.debug('Rotating key');
240
+
241
+        const key = this._generateKey();
242
+        const index = await this._olmAdapter.updateKey(key);
243
+
244
+        this._e2eeCtx.setKey(this.conference.myUserId(), key, index);
74 245
     }
75 246
 
76 247
     /**
77 248
      * Setup E2EE for the receiving side.
78 249
      *
79
-     * @returns {void}
250
+     * @private
80 251
      */
81 252
     _setupReceiverE2EEForTrack(tpc, track) {
253
+        if (!this._enabled) {
254
+            return;
255
+        }
256
+
82 257
         const receiver = tpc.findReceiverForTrack(track.track);
83 258
 
84 259
         if (receiver) {
@@ -93,9 +268,13 @@ export class E2EEncryption {
93 268
      *
94 269
      * @param {JingleSessionPC} session - the session which sends the media produced by the track.
95 270
      * @param {JitsiLocalTrack} track - the local track for which e2e encoder will be configured.
96
-     * @returns {void}
271
+     * @private
97 272
      */
98 273
     _setupSenderE2EEForTrack(session, track) {
274
+        if (!this._enabled) {
275
+            return;
276
+        }
277
+
99 278
         const pc = session.peerconnection;
100 279
         const sender = pc && pc.findSenderForTrack(track.track);
101 280
 

+ 548
- 0
modules/e2ee/OlmAdapter.js View File

@@ -0,0 +1,548 @@
1
+/* global __filename, Olm */
2
+
3
+import base64js from 'base64-js';
4
+import { getLogger } from 'jitsi-meet-logger';
5
+import isEqual from 'lodash.isequal';
6
+import { v4 as uuidv4 } from 'uuid';
7
+
8
+import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
9
+import Deferred from '../util/Deferred';
10
+import Listenable from '../util/Listenable';
11
+import { JITSI_MEET_MUC_TYPE } from '../xmpp/xmpp';
12
+
13
+const logger = getLogger(__filename);
14
+
15
+const REQ_TIMEOUT = 5 * 1000;
16
+const OLM_MESSAGE_TYPE = 'olm';
17
+const OLM_MESSAGE_TYPES = {
18
+    ERROR: 'error',
19
+    KEY_INFO: 'key-info',
20
+    KEY_INFO_ACK: 'key-info-ack',
21
+    SESSION_ACK: 'session-ack',
22
+    SESSION_INIT: 'session-init'
23
+};
24
+
25
+const kOlmData = Symbol('OlmData');
26
+
27
+const OlmAdapterEvents = {
28
+    PARTICIPANT_E2EE_CHANNEL_READY: 'olm.participant_e2ee_channel_ready',
29
+    PARTICIPANT_KEY_UPDATED: 'olm.partitipant_key_updated'
30
+};
31
+
32
+/**
33
+ * This class implements an End-to-End Encrypted communication channel between every two peers
34
+ * in the conference. This channel uses libolm to achieve E2EE.
35
+ *
36
+ * The created channel is then used to exchange the secret key that each participant will use
37
+ * to encrypt the actual media (see {@link E2EEContext}).
38
+ *
39
+ * A simple JSON message based protocol is implemented, which follows a request - response model:
40
+ * - session-init: Initiates an olm session establishment procedure. This message will be sent
41
+ *                 by the participant who just joined, to everyone else.
42
+ * - session-ack: Completes the olm session etablishment. This messsage may contain ancilliary
43
+ *                encrypted data, more specifically the sender's current key.
44
+ * - key-info: Includes the sender's most up to date key information.
45
+ * - key-info-ack: Acknowledges the reception of a key-info request. In addition, it may contain
46
+ *                 the sender's key information, if available.
47
+ * - error: Indicates a request processing error has occurred.
48
+ *
49
+ * These requessts and responses are transport independent. Currently they are sent using XMPP
50
+ * MUC private messages.
51
+ */
52
+export class OlmAdapter extends Listenable {
53
+    /**
54
+     * Creates an adapter instance for the given conference.
55
+     */
56
+    constructor(conference) {
57
+        super();
58
+
59
+        this._conf = conference;
60
+        this._init = new Deferred();
61
+        this._key = undefined;
62
+        this._keyIndex = -1;
63
+        this._reqs = new Map();
64
+
65
+        if (OlmAdapter.isSupported()) {
66
+            this._bootstrapOlm();
67
+
68
+            this._conf.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, this._onEndpointMessageReceived.bind(this));
69
+            this._conf.on(JitsiConferenceEvents.CONFERENCE_JOINED, this._onConferenceJoined.bind(this));
70
+            this._conf.on(JitsiConferenceEvents.CONFERENCE_LEFT, this._onConferenceLeft.bind(this));
71
+            this._conf.on(JitsiConferenceEvents.USER_LEFT, this._onParticipantLeft.bind(this));
72
+        } else {
73
+            this._init.reject(new Error('Olm not supported'));
74
+        }
75
+    }
76
+
77
+    /**
78
+     * Indicates if olm is supported on the current platform.
79
+     *
80
+     * @returns {boolean}
81
+     */
82
+    static isSupported() {
83
+        return typeof window.Olm !== 'undefined';
84
+    }
85
+
86
+    /**
87
+     * Updates the current participant key and distributes it to all participants in the conference
88
+     * by sending a key-info message.
89
+     *
90
+     * @param {Uint8Array|boolean} key - The new key.
91
+     * @retrns {Promise<Number>}
92
+     */
93
+    async updateKey(key) {
94
+        // Store it locally for new sessions.
95
+        this._key = key;
96
+        this._keyIndex++;
97
+
98
+        const promises = [];
99
+
100
+        // Broadcast it.
101
+        for (const participant of this._conf.getParticipants()) {
102
+            const pId = participant.getId();
103
+            const olmData = this._getParticipantOlmData(participant);
104
+
105
+            // TODO: skip those who don't support E2EE.
106
+
107
+            if (!olmData.session) {
108
+                logger.warn(`Tried to send key to participant ${pId} but we have no session`);
109
+
110
+                // eslint-disable-next-line no-continue
111
+                continue;
112
+            }
113
+
114
+            const uuid = uuidv4();
115
+            const data = {
116
+                [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
117
+                olm: {
118
+                    type: OLM_MESSAGE_TYPES.KEY_INFO,
119
+                    data: {
120
+                        ciphertext: this._encryptKeyInfo(olmData.session),
121
+                        uuid
122
+                    }
123
+                }
124
+            };
125
+            const d = new Deferred();
126
+
127
+            d.setRejectTimeout(REQ_TIMEOUT);
128
+            d.catch(() => {
129
+                this._reqs.delete(uuid);
130
+            });
131
+            this._reqs.set(uuid, d);
132
+            promises.push(d);
133
+
134
+            this._sendMessage(data, pId);
135
+        }
136
+
137
+        await Promise.allSettled(promises);
138
+
139
+        // TODO: retry failed ones?
140
+
141
+        return this._keyIndex;
142
+    }
143
+
144
+    /**
145
+     * Internal helper to bootstrap the olm library.
146
+     *
147
+     * @returns {Promise<void>}
148
+     * @private
149
+     */
150
+    async _bootstrapOlm() {
151
+        logger.debug('Initializing Olm...');
152
+
153
+        try {
154
+            await Olm.init();
155
+
156
+            this._olmAccount = new Olm.Account();
157
+            this._olmAccount.create();
158
+
159
+            const idKeys = JSON.parse(this._olmAccount.identity_keys());
160
+
161
+            this._idKey = idKeys.curve25519;
162
+
163
+            logger.debug('Olm initialized!');
164
+            this._init.resolve();
165
+        } catch (e) {
166
+            logger.error('Failed to initialize Olm', e);
167
+            this._init.reject(e);
168
+        }
169
+
170
+    }
171
+
172
+    /**
173
+     * Internal helper for encrypting the current key information for a given participant.
174
+     *
175
+     * @param {Olm.Session} session - Participant's session.
176
+     * @returns {string} - The encrypted text with the key information.
177
+     * @private
178
+     */
179
+    _encryptKeyInfo(session) {
180
+        const keyInfo = {};
181
+
182
+        if (this._key !== undefined) {
183
+            keyInfo.key = this._key ? base64js.fromByteArray(this._key) : false;
184
+            keyInfo.keyIndex = this._keyIndex;
185
+        }
186
+
187
+        return session.encrypt(JSON.stringify(keyInfo));
188
+    }
189
+
190
+    /**
191
+     * Internal helper for getting the olm related data associated with a participant.
192
+     *
193
+     * @param {JitsiParticipant} participant - Participant whose data wants to be extracted.
194
+     * @returns {Object}
195
+     * @private
196
+     */
197
+    _getParticipantOlmData(participant) {
198
+        participant[kOlmData] = participant[kOlmData] || {};
199
+
200
+        return participant[kOlmData];
201
+    }
202
+
203
+    /**
204
+     * Handles the conference joined event. Upon joining a conference, the participant
205
+     * who just joined will start new olm sessions with every other participant.
206
+     *
207
+     * @private
208
+     */
209
+    async _onConferenceJoined() {
210
+        logger.debug('Conference joined');
211
+
212
+        await this._init;
213
+
214
+        const promises = [];
215
+
216
+        // Establish a 1-to-1 Olm session with every participant in the conference.
217
+        // We are forcing the last user to join the conference to start the exchange
218
+        // so we can send some pre-established secrets in the ACK.
219
+        for (const participant of this._conf.getParticipants()) {
220
+            promises.push(this._sendSessionInit(participant));
221
+        }
222
+
223
+        await Promise.allSettled(promises);
224
+
225
+        // TODO: retry failed ones.
226
+        // TODO: skip participants which don't support E2EE.
227
+    }
228
+
229
+    /**
230
+     * Handles leaving the conference, cleaning up olm sessions.
231
+     *
232
+     * @private
233
+     */
234
+    async _onConferenceLeft() {
235
+        logger.debug('Conference left');
236
+
237
+        await this._init;
238
+
239
+        for (const participant of this._conf.getParticipants()) {
240
+            this._onParticipantLeft(participant.getId(), participant);
241
+        }
242
+
243
+        if (this._olmAccount) {
244
+            this._olmAccount.free();
245
+            this._olmAccount = undefined;
246
+        }
247
+    }
248
+
249
+    /**
250
+     * Main message handler. Handles 1-to-1 messages received from other participants
251
+     * and send the appropriate replies.
252
+     *
253
+     * @private
254
+     */
255
+    async _onEndpointMessageReceived(participant, payload) {
256
+        if (payload[JITSI_MEET_MUC_TYPE] !== OLM_MESSAGE_TYPE) {
257
+            return;
258
+        }
259
+
260
+        if (!payload.olm) {
261
+            logger.warn('Incorrectly formatted message');
262
+
263
+            return;
264
+        }
265
+
266
+        await this._init;
267
+
268
+        const msg = payload.olm;
269
+        const pId = participant.getId();
270
+        const olmData = this._getParticipantOlmData(participant);
271
+
272
+        switch (msg.type) {
273
+        case OLM_MESSAGE_TYPES.SESSION_INIT: {
274
+            if (olmData.session) {
275
+                logger.warn(`Participant ${pId} already has a session`);
276
+
277
+                this._sendError(participant, 'Session already established');
278
+            } else {
279
+                // Create a session for communicating with this participant.
280
+
281
+                const session = new Olm.Session();
282
+
283
+                session.create_outbound(this._olmAccount, msg.data.idKey, msg.data.otKey);
284
+                olmData.session = session;
285
+
286
+                // Send ACK
287
+                const ack = {
288
+                    [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
289
+                    olm: {
290
+                        type: OLM_MESSAGE_TYPES.SESSION_ACK,
291
+                        data: {
292
+                            ciphertext: this._encryptKeyInfo(session),
293
+                            uuid: msg.data.uuid
294
+                        }
295
+                    }
296
+                };
297
+
298
+                this._sendMessage(ack, pId);
299
+            }
300
+            break;
301
+        }
302
+        case OLM_MESSAGE_TYPES.SESSION_ACK: {
303
+            if (olmData.session) {
304
+                logger.warn(`Participant ${pId} already has a session`);
305
+
306
+                this._sendError(participant, 'No session found');
307
+            } else if (msg.data.uuid === olmData.pendingSessionUuid) {
308
+                const { ciphertext } = msg.data;
309
+                const d = this._reqs.get(msg.data.uuid);
310
+                const session = new Olm.Session();
311
+
312
+                session.create_inbound(this._olmAccount, ciphertext.body);
313
+
314
+                // Remove OT keys that have been used to setup this session.
315
+                this._olmAccount.remove_one_time_keys(session);
316
+
317
+                // Decrypt first message.
318
+                const data = session.decrypt(ciphertext.type, ciphertext.body);
319
+
320
+                olmData.session = session;
321
+                olmData.pendingSessionUuid = undefined;
322
+
323
+                logger.debug(`Olm session established with ${pId}`);
324
+                this.eventEmitter.emit(OlmAdapterEvents.PARTICIPANT_E2EE_CHANNEL_READY, pId);
325
+
326
+                this._reqs.delete(msg.data.uuid);
327
+                d.resolve();
328
+
329
+                const json = safeJsonParse(data);
330
+
331
+                if (json.key) {
332
+                    const key = base64js.toByteArray(json.key);
333
+                    const keyIndex = json.keyIndex;
334
+
335
+                    olmData.lastKey = key;
336
+                    this.eventEmitter.emit(OlmAdapterEvents.PARTICIPANT_KEY_UPDATED, pId, key, keyIndex);
337
+                }
338
+            } else {
339
+                logger.warn('Received ACK with the wrong UUID');
340
+
341
+                this._sendError(participant, 'Invalid UUID');
342
+            }
343
+            break;
344
+        }
345
+        case OLM_MESSAGE_TYPES.ERROR: {
346
+            logger.error(msg.data.error);
347
+
348
+            break;
349
+        }
350
+        case OLM_MESSAGE_TYPES.KEY_INFO: {
351
+            if (olmData.session) {
352
+                const { ciphertext } = msg.data;
353
+                const data = olmData.session.decrypt(ciphertext.type, ciphertext.body);
354
+                const json = safeJsonParse(data);
355
+
356
+                if (json.key !== undefined && json.keyIndex !== undefined) {
357
+                    const key = json.key ? base64js.toByteArray(json.key) : false;
358
+                    const keyIndex = json.keyIndex;
359
+
360
+                    if (!isEqual(olmData.lastKey, key)) {
361
+                        olmData.lastKey = key;
362
+                        this.eventEmitter.emit(OlmAdapterEvents.PARTICIPANT_KEY_UPDATED, pId, key, keyIndex);
363
+                    }
364
+
365
+                    // Send ACK.
366
+                    const ack = {
367
+                        [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
368
+                        olm: {
369
+                            type: OLM_MESSAGE_TYPES.KEY_INFO_ACK,
370
+                            data: {
371
+                                ciphertext: this._encryptKeyInfo(olmData.session),
372
+                                uuid: msg.data.uuid
373
+                            }
374
+                        }
375
+                    };
376
+
377
+                    this._sendMessage(ack, pId);
378
+                }
379
+            } else {
380
+                logger.debug(`Received key info message from ${pId} but we have no session for them!`);
381
+
382
+                this._sendError(participant, 'No session found while processing key-info');
383
+            }
384
+            break;
385
+        }
386
+        case OLM_MESSAGE_TYPES.KEY_INFO_ACK: {
387
+            if (olmData.session) {
388
+                const { ciphertext } = msg.data;
389
+                const data = olmData.session.decrypt(ciphertext.type, ciphertext.body);
390
+                const json = safeJsonParse(data);
391
+
392
+                if (json.key !== undefined && json.keyIndex !== undefined) {
393
+                    const key = json.key ? base64js.toByteArray(json.key) : false;
394
+                    const keyIndex = json.keyIndex;
395
+
396
+                    if (!isEqual(olmData.lastKey, key)) {
397
+                        olmData.lastKey = key;
398
+                        this.eventEmitter.emit(OlmAdapterEvents.PARTICIPANT_KEY_UPDATED, pId, key, keyIndex);
399
+                    }
400
+                }
401
+
402
+                const d = this._reqs.get(msg.data.uuid);
403
+
404
+                this._reqs.delete(msg.data.uuid);
405
+                d.resolve();
406
+            } else {
407
+                logger.debug(`Received key info ack message from ${pId} but we have no session for them!`);
408
+
409
+                this._sendError(participant, 'No session found while processing key-info-ack');
410
+            }
411
+            break;
412
+        }
413
+        }
414
+
415
+    }
416
+
417
+    /**
418
+     * Handles a participant leaving. When a participant leaves their olm session is destroyed.
419
+     *
420
+     * @private
421
+     */
422
+    _onParticipantLeft(id, participant) {
423
+        logger.debug(`Participant ${id} left`);
424
+
425
+        const olmData = this._getParticipantOlmData(participant);
426
+
427
+        if (olmData.session) {
428
+            olmData.session.free();
429
+            olmData.session = undefined;
430
+        }
431
+    }
432
+
433
+    /**
434
+     * Builds and sends an error message to the target participant.
435
+     *
436
+     * @param {JitsiParticipant} participant - The target participant.
437
+     * @param {string} error - The error message.
438
+     * @returns {void}
439
+     */
440
+    _sendError(participant, error) {
441
+        const pId = participant.getId();
442
+        const err = {
443
+            [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
444
+            olm: {
445
+                type: OLM_MESSAGE_TYPES.ERROR,
446
+                data: {
447
+                    error
448
+                }
449
+            }
450
+        };
451
+
452
+        this._sendMessage(err, pId);
453
+    }
454
+
455
+    /**
456
+     * Internal helper to send the given object to the given participant ID.
457
+     * This function merely exists so the transport can be easily swapped.
458
+     * Currently messages are transmitted via XMPP MUC private messages.
459
+     *
460
+     * @param {object} data - The data that will be sent to the target participant.
461
+     * @param {string} participantId - ID of the target participant.
462
+     */
463
+    _sendMessage(data, participantId) {
464
+        this._conf.sendMessage(data, participantId);
465
+    }
466
+
467
+    /**
468
+     * Builds and sends the session-init request to the target participant.
469
+     *
470
+     * @param {JitsiParticipant} participant - Participant to whom we'll send the request.
471
+     * @returns {Promise} - The promise will be resolved when the session-ack is received.
472
+     * @private
473
+     */
474
+    _sendSessionInit(participant) {
475
+        const pId = participant.getId();
476
+        const olmData = this._getParticipantOlmData(participant);
477
+
478
+        if (olmData.session) {
479
+            logger.warn(`Tried to send session-init to ${pId} but we already have a session`);
480
+
481
+            return Promise.reject();
482
+        }
483
+
484
+        if (olmData.pendingSessionUuid !== undefined) {
485
+            logger.warn(`Tried to send session-init to ${pId} but we already have a pending session`);
486
+
487
+            return Promise.reject();
488
+        }
489
+
490
+        // Generate a One Time Key.
491
+        this._olmAccount.generate_one_time_keys(1);
492
+
493
+        const otKeys = JSON.parse(this._olmAccount.one_time_keys());
494
+        const otKey = Object.values(otKeys.curve25519)[0];
495
+
496
+        if (!otKey) {
497
+            return Promise.reject(new Error('No one-time-keys generated'));
498
+        }
499
+
500
+        // Mark the OT keys (one really) as published so they are not reused.
501
+        this._olmAccount.mark_keys_as_published();
502
+
503
+        const uuid = uuidv4();
504
+        const init = {
505
+            [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
506
+            olm: {
507
+                type: OLM_MESSAGE_TYPES.SESSION_INIT,
508
+                data: {
509
+                    idKey: this._idKey,
510
+                    otKey,
511
+                    uuid
512
+                }
513
+            }
514
+        };
515
+
516
+        const d = new Deferred();
517
+
518
+        d.setRejectTimeout(REQ_TIMEOUT);
519
+        d.catch(() => {
520
+            this._reqs.delete(uuid);
521
+            olmData.pendingSessionUuid = undefined;
522
+        });
523
+        this._reqs.set(uuid, d);
524
+
525
+        this._sendMessage(init, pId);
526
+
527
+        // Store the UUID for matching with the ACK.
528
+        olmData.pendingSessionUuid = uuid;
529
+
530
+        return d;
531
+    }
532
+}
533
+
534
+OlmAdapter.events = OlmAdapterEvents;
535
+
536
+/**
537
+ * Helper to ensure JSON parsing always returns an object.
538
+ *
539
+ * @param {string} data - The data that needs to be parsed.
540
+ * @returns {object} - Parsed data or empty object in case of failure.
541
+ */
542
+function safeJsonParse(data) {
543
+    try {
544
+        return JSON.parse(data);
545
+    } catch (e) {
546
+        return {};
547
+    }
548
+}

+ 25
- 28
modules/e2ee/Worker.js View File

@@ -23,10 +23,7 @@ function polyFillEncodedFrameMetadata(encodedFrame, controller) {
23 23
 
24 24
 // We use a ringbuffer of keys so we can change them and still decode packets that were
25 25
 // encrypted with an old key.
26
-// In the future when we dont rely on a globally shared key we will actually use it. For
27
-// now set the size to 1 which means there is only a single key. This causes some
28
-// glitches when changing the key but its ok.
29
-const keyRingSize = 1;
26
+const keyRingSize = 3;
30 27
 
31 28
 // We use a 96 bit IV for AES GCM. This is signalled in plain together with the
32 29
 // packet. See https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams
@@ -62,9 +59,6 @@ const unencryptedBytes = {
62 59
 // We currently do not enforce a minimum length of 16 bytes either.
63 60
 let _keySalt;
64 61
 
65
-// Raw keyBytes used to derive the key.
66
-let _keyBytes;
67
-
68 62
 /**
69 63
  * Derives a AES-GCM key from the input using PBKDF2
70 64
  * The key length can be configured above and should be either 128 or 256 bits.
@@ -86,7 +80,8 @@ async function deriveKey(keyBytes, salt) {
86 80
 }
87 81
 
88 82
 
89
-/** Per-participant context holding the cryptographic keys and
83
+/**
84
+ * Per-participant context holding the cryptographic keys and
90 85
  * encode/decode functions
91 86
  */
92 87
 class Context {
@@ -128,10 +123,11 @@ class Context {
128 123
     /**
129 124
      * Sets a key and starts using it for encrypting.
130 125
      * @param {CryptoKey} key
126
+     * @param {Number} keyIndex
131 127
      */
132
-    setKey(key) {
133
-        this._currentKeyIndex++;
134
-        this._cryptoKeyRing[this._currentKeyIndex % this._cryptoKeyRing.length] = key;
128
+    setKey(key, keyIndex) {
129
+        this._currentKeyIndex = keyIndex % this._cryptoKeyRing.length;
130
+        this._cryptoKeyRing[this._currentKeyIndex] = key;
135 131
     }
136 132
 
137 133
     /**
@@ -204,7 +200,7 @@ class Context {
204 200
      * 9) Enqueue the encrypted frame for sending.
205 201
      */
206 202
     encodeFunction(encodedFrame, controller) {
207
-        const keyIndex = this._currentKeyIndex % this._cryptoKeyRing.length;
203
+        const keyIndex = this._currentKeyIndex;
208 204
 
209 205
         if (this._cryptoKeyRing[keyIndex]) {
210 206
             const iv = this.makeIV(encodedFrame.getMetadata().synchronizationSource, encodedFrame.timestamp);
@@ -307,8 +303,7 @@ class Context {
307 303
                     controller.enqueue(encodedFrame);
308 304
                 }
309 305
             });
310
-        } else if (keyIndex >= this._cryptoKeyRing.length
311
-                && this._cryptoKeyRing[this._currentKeyIndex % this._cryptoKeyRing.length]) {
306
+        } else if (keyIndex >= this._cryptoKeyRing.length && this._cryptoKeyRing[this._currentKeyIndex]) {
312 307
             // If we are encrypting but don't have a key for the remote drop the frame.
313 308
             // This is a heuristic since we don't know whether a packet is encrypted,
314 309
             // do not have a checksum and do not have signaling for whether a remote participant does
@@ -346,9 +341,6 @@ onmessage = async event => {
346 341
             }))
347 342
             .pipeThrough(transformStream)
348 343
             .pipeTo(writableStream);
349
-        if (_keyBytes) {
350
-            context.setKey(await context.deriveKey(_keyBytes, _keySalt));
351
-        }
352 344
     } else if (operation === 'decode') {
353 345
         const { readableStream, writableStream, participantId } = event.data;
354 346
 
@@ -366,18 +358,23 @@ onmessage = async event => {
366 358
             }))
367 359
             .pipeThrough(transformStream)
368 360
             .pipeTo(writableStream);
369
-        if (_keyBytes) {
370
-            context.setKey(await context.deriveKey(_keyBytes, _keySalt));
371
-        }
372 361
     } else if (operation === 'setKey') {
373
-        _keyBytes = event.data.key;
374
-        contexts.forEach(async context => {
375
-            if (_keyBytes) {
376
-                context.setKey(await context.deriveKey(_keyBytes, _keySalt));
377
-            } else {
378
-                context.setKey(false);
379
-            }
380
-        });
362
+        const { participantId, key, keyIndex } = event.data;
363
+
364
+        if (!contexts.has(participantId)) {
365
+            contexts.set(participantId, new Context(participantId));
366
+        }
367
+        const context = contexts.get(participantId);
368
+
369
+        if (key) {
370
+            context.setKey(await context.deriveKey(key, _keySalt), keyIndex);
371
+        } else {
372
+            context.setKey(false, keyIndex);
373
+        }
374
+    } else if (operation === 'cleanup') {
375
+        const { participantId } = event.data;
376
+
377
+        contexts.delete(participantId);
381 378
     } else {
382 379
         console.error('e2ee worker', operation);
383 380
     }

+ 43
- 0
modules/util/Deferred.js View File

@@ -0,0 +1,43 @@
1
+
2
+/**
3
+ * Promise-like object which can be passed around for resolving it later. It
4
+ * implements the "thenable" interface, so it can be used wherever a Promise
5
+ * could be used.
6
+ *
7
+ * In addition a "reject on timeout" functionality is provided.
8
+ */
9
+export default class Deferred {
10
+    /**
11
+     * Instantiates a Deferred object.
12
+     */
13
+    constructor() {
14
+        this.promise = new Promise((resolve, reject) => {
15
+            this.resolve = (...args) => {
16
+                this.clearRejectTimeout();
17
+                resolve(...args);
18
+            };
19
+            this.reject = (...args) => {
20
+                this.clearRejectTimeout();
21
+                reject(...args);
22
+            };
23
+        });
24
+        this.then = this.promise.then.bind(this.promise);
25
+        this.catch = this.promise.catch.bind(this.promise);
26
+    }
27
+
28
+    /**
29
+     * Clears the reject timeout.
30
+     */
31
+    clearRejectTimeout() {
32
+        clearTimeout(this._timeout);
33
+    }
34
+
35
+    /**
36
+     * Rejects the promise after the given timeout.
37
+     */
38
+    setRejectTimeout(ms) {
39
+        this._timeout = setTimeout(() => {
40
+            this.reject(new Error('timeout'));
41
+        }, ms);
42
+    }
43
+}

+ 2
- 1
modules/xmpp/xmpp.js View File

@@ -8,6 +8,7 @@ import * as JitsiConnectionErrors from '../../JitsiConnectionErrors';
8 8
 import * as JitsiConnectionEvents from '../../JitsiConnectionEvents';
9 9
 import XMPPEvents from '../../service/xmpp/XMPPEvents';
10 10
 import browser from '../browser';
11
+import { E2EEncryption } from '../e2ee/E2EEncryption';
11 12
 import GlobalOnErrorHandler from '../util/GlobalOnErrorHandler';
12 13
 import Listenable from '../util/Listenable';
13 14
 import RandomUtil from '../util/RandomUtil';
@@ -175,7 +176,7 @@ export default class XMPP extends Listenable {
175 176
             this.caps.addFeature('urn:xmpp:rayo:client:1');
176 177
         }
177 178
 
178
-        if (browser.supportsInsertableStreams() && !(this.options.testing && this.options.testing.disableE2EE)) {
179
+        if (E2EEncryption.isSupported(this.options)) {
179 180
             this.caps.addFeature('https://jitsi.org/meet/e2ee');
180 181
         }
181 182
     }

+ 17
- 6
package-lock.json View File

@@ -2019,8 +2019,7 @@
2019 2019
     "base64-js": {
2020 2020
       "version": "1.3.1",
2021 2021
       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
2022
-      "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==",
2023
-      "dev": true
2022
+      "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
2024 2023
     },
2025 2024
     "base64id": {
2026 2025
       "version": "2.0.0",
@@ -5553,6 +5552,11 @@
5553 5552
       "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
5554 5553
       "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
5555 5554
     },
5555
+    "lodash.debounce": {
5556
+      "version": "4.0.8",
5557
+      "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
5558
+      "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
5559
+    },
5556 5560
     "lodash.isequal": {
5557 5561
       "version": "4.5.0",
5558 5562
       "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
@@ -8449,10 +8453,9 @@
8449 8453
       "dev": true
8450 8454
     },
8451 8455
     "uuid": {
8452
-      "version": "3.4.0",
8453
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
8454
-      "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
8455
-      "dev": true
8456
+      "version": "8.1.0",
8457
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz",
8458
+      "integrity": "sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg=="
8456 8459
     },
8457 8460
     "v8-compile-cache": {
8458 8461
       "version": "2.0.3",
@@ -8970,6 +8973,14 @@
8970 8973
       "requires": {
8971 8974
         "ansi-colors": "^3.0.0",
8972 8975
         "uuid": "^3.3.2"
8976
+      },
8977
+      "dependencies": {
8978
+        "uuid": {
8979
+          "version": "3.4.0",
8980
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
8981
+          "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
8982
+          "dev": true
8983
+        }
8973 8984
       }
8974 8985
     },
8975 8986
     "webpack-sources": {

+ 3
- 0
package.json View File

@@ -20,14 +20,17 @@
20 20
     "@jitsi/sdp-interop": "1.0.3",
21 21
     "@jitsi/sdp-simulcast": "0.3.0",
22 22
     "async": "0.9.0",
23
+    "base64-js": "1.3.1",
23 24
     "current-executing-script": "0.1.3",
24 25
     "jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#5ec92357570dc8f0b7ffc1528820721c84c6af8b",
25 26
     "lodash.clonedeep": "4.5.0",
27
+    "lodash.debounce": "4.0.8",
26 28
     "lodash.isequal": "4.5.0",
27 29
     "sdp-transform": "2.3.0",
28 30
     "strophe.js": "1.3.4",
29 31
     "strophejs-plugin-disco": "0.0.2",
30 32
     "strophejs-plugin-stream-management": "github:jitsi/strophejs-plugin-stream-management#001cf02bef2357234e1ac5d163611b4d60bf2b6a",
33
+    "uuid": "8.1.0",
31 34
     "webrtc-adapter": "7.5.0"
32 35
   },
33 36
   "devDependencies": {

Loading…
Cancel
Save