ソースを参照

feat(e2ee) add ability to verify participants using a SAS mechanism

It implements SAS verification as per the Matrix spec, adapted to our environment.
tags/v0.0.2
tmoldovan8x8 3年前
コミット
751b363080
コミッターのメールアドレスに関連付けられたアカウントが存在しません

+ 34
- 0
JitsiConference.js ファイルの表示

@@ -3896,6 +3896,40 @@ JitsiConference.prototype.setMediaEncryptionKey = function(keyInfo) {
3896 3896
     this._e2eEncryption.setEncryptionKey(keyInfo);
3897 3897
 };
3898 3898
 
3899
+/**
3900
+ * Starts the participant verification process.
3901
+ *
3902
+ * @param {string} participantId The participant which will be marked as verified.
3903
+ * @returns {void}
3904
+ */
3905
+JitsiConference.prototype.startVerification = function(participantId) {
3906
+    const participant = this.getParticipantById(participantId);
3907
+
3908
+    if (!participant) {
3909
+        return;
3910
+    }
3911
+
3912
+    this._e2eEncryption.startVerification(participant);
3913
+};
3914
+
3915
+/**
3916
+ * Marks the given participant as verified. After this is done, MAC verification will
3917
+ * be performed and an event will be emitted with the result.
3918
+ *
3919
+ * @param {string} participantId The participant which will be marked as verified.
3920
+ * @param {boolean} isVerified - whether the verification was succesfull.
3921
+ * @returns {void}
3922
+ */
3923
+JitsiConference.prototype.markParticipantVerified = function(participantId, isVerified) {
3924
+    const participant = this.getParticipantById(participantId);
3925
+
3926
+    if (!participant) {
3927
+        return;
3928
+    }
3929
+
3930
+    this._e2eEncryption.markParticipantVerified(participant, isVerified);
3931
+};
3932
+
3899 3933
 /**
3900 3934
  * Returns <tt>true</tt> if lobby support is enabled in the backend.
3901 3935
  *

+ 6
- 0
JitsiConferenceEvents.spec.ts ファイルの表示

@@ -23,6 +23,9 @@ describe( "/JitsiConferenceEvents members", () => {
23 23
         DOMINANT_SPEAKER_CHANGED,
24 24
         CONFERENCE_CREATED_TIMESTAMP,
25 25
         DTMF_SUPPORT_CHANGED,
26
+        E2EE_VERIFICATION_AVAILABLE,
27
+        E2EE_VERIFICATION_READY,
28
+        E2EE_VERIFICATION_COMPLETED,
26 29
         ENDPOINT_MESSAGE_RECEIVED,
27 30
         ENDPOINT_STATS_RECEIVED,
28 31
         JVB121_STATUS,
@@ -228,6 +231,9 @@ describe( "/JitsiConferenceEvents members", () => {
228 231
         expect( JitsiConferenceEvents.BREAKOUT_ROOMS_MOVE_TO_ROOM ).toBe( 'conference.breakout-rooms.move-to-room' );
229 232
         expect( JitsiConferenceEvents.BREAKOUT_ROOMS_UPDATED ).toBe( 'conference.breakout-rooms.updated' );
230 233
         expect( JitsiConferenceEvents.METADATA_UPDATED ).toBe( 'conference.metadata.updated' );
234
+        expect( JitsiConferenceEvents.E2EE_VERIFICATION_READY ).toBe( 'conference.e2ee.verification.ready' );
235
+        expect( JitsiConferenceEvents.E2EE_VERIFICATION_COMPLETED ).toBe( 'conference.e2ee.verification.completed' );
236
+        expect( JitsiConferenceEvents.E2EE_VERIFICATION_AVAILABLE ).toBe( 'conference.e2ee.verification.available' );
231 237
     } );
232 238
 
233 239
 it( "unknown members", () => {

+ 10
- 1
JitsiConferenceEvents.ts ファイルの表示

@@ -453,7 +453,13 @@ export enum JitsiConferenceEvents {
453 453
     /**
454 454
      * Event fired when the conference metadata is updated.
455 455
      */
456
-    METADATA_UPDATED = 'conference.metadata.updated'
456
+    METADATA_UPDATED = 'conference.metadata.updated',
457
+
458
+    E2EE_VERIFICATION_AVAILABLE = 'conference.e2ee.verification.available',
459
+
460
+    E2EE_VERIFICATION_READY = 'conference.e2ee.verification.ready',
461
+
462
+    E2EE_VERIFICATION_COMPLETED = 'conference.e2ee.verification.completed'
457 463
 };
458 464
 
459 465
 // exported for backward compatibility
@@ -478,6 +484,9 @@ export const CONFERENCE_CREATED_TIMESTAMP = JitsiConferenceEvents.CONFERENCE_CRE
478 484
 export const DTMF_SUPPORT_CHANGED = JitsiConferenceEvents.DTMF_SUPPORT_CHANGED;
479 485
 export const ENDPOINT_MESSAGE_RECEIVED = JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED;
480 486
 export const ENDPOINT_STATS_RECEIVED = JitsiConferenceEvents.ENDPOINT_STATS_RECEIVED;
487
+export const E2EE_VERIFICATION_AVAILABLE = JitsiConferenceEvents.E2EE_VERIFICATION_AVAILABLE;
488
+export const E2EE_VERIFICATION_READY = JitsiConferenceEvents.E2EE_VERIFICATION_READY;
489
+export const E2EE_VERIFICATION_COMPLETED = JitsiConferenceEvents.E2EE_VERIFICATION_COMPLETED;
481 490
 export const JVB121_STATUS = JitsiConferenceEvents.JVB121_STATUS;
482 491
 export const KICKED = JitsiConferenceEvents.KICKED;
483 492
 export const PARTICIPANT_KICKED = JitsiConferenceEvents.PARTICIPANT_KICKED;

+ 8
- 0
modules/e2ee/E2EEErrors.ts ファイルの表示

@@ -0,0 +1,8 @@
1
+export enum E2EEErrors {
2
+    E2EE_SAS_KEYS_MAC_MISMATCH = 'e2ee.sas.keys-mac-mismatch',
3
+    E2EE_SAS_MAC_MISMATCH = 'e2ee.sas.mac-mismatch',
4
+    E2EE_SAS_MISSING_KEY =  'e2ee.sas.missing-key',
5
+    E2EE_SAS_COMMITMENT_MISMATCHED =  'e2ee.sas.commitment-mismatched',
6
+    E2EE_SAS_CHANNEL_VERIFICATION_FAILED = 'e2ee.sas.channel-verification-failed',
7
+    E2EE_SAS_INVALID_SAS_VERIFICATION =  'e2ee.sas.invalid-sas-verification',
8
+}

+ 21
- 0
modules/e2ee/E2EEncryption.js ファイルの表示

@@ -71,4 +71,25 @@ export class E2EEncryption {
71 71
     setEncryptionKey(keyInfo) {
72 72
         this._keyHandler.setKey(keyInfo);
73 73
     }
74
+
75
+    /**
76
+     * Starts the verification process of the participant
77
+     *
78
+     * @param {Participant} - participant to be verified.
79
+     * @returns {void}
80
+     */
81
+    startVerification(participant) {
82
+        this._keyHandler.sasVerification?.startVerification(participant);
83
+    }
84
+
85
+    /**
86
+     * Marks the channel as verified
87
+     *
88
+     * @param {Participant} - participant to be verified.
89
+     * @param {boolean} isVerified - whether the verification was succesfull.
90
+     * @returns {void}
91
+     */
92
+    markParticipantVerified(participant, isVerified) {
93
+        this._keyHandler.sasVerification?.markParticipantVerified(participant, isVerified);
94
+    }
74 95
 }

+ 54
- 0
modules/e2ee/ManagedKeyHandler.js ファイルの表示

@@ -36,6 +36,18 @@ export class ManagedKeyHandler extends KeyHandler {
36 36
             OlmAdapter.events.PARTICIPANT_KEY_UPDATED,
37 37
             this._onParticipantKeyUpdated.bind(this));
38 38
 
39
+        this._olmAdapter.on(
40
+            OlmAdapter.events.PARTICIPANT_SAS_READY,
41
+            this._onParticipantSasReady.bind(this));
42
+
43
+        this._olmAdapter.on(
44
+            OlmAdapter.events.PARTICIPANT_SAS_AVAILABLE,
45
+            this._onParticipantSasAvailable.bind(this));
46
+
47
+        this._olmAdapter.on(
48
+            OlmAdapter.events.PARTICIPANT_VERIFICATION_COMPLETED,
49
+            this._onParticipantVerificationCompleted.bind(this));
50
+
39 51
         this.conference.on(
40 52
             JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
41 53
             this._onParticipantPropertyChanged.bind(this));
@@ -52,6 +64,15 @@ export class ManagedKeyHandler extends KeyHandler {
52 64
                 });
53 65
     }
54 66
 
67
+    /**
68
+     * Returns the sasVerficiation object.
69
+     *
70
+     * @returns {Object}
71
+     */
72
+    get sasVerification() {
73
+        return this._olmAdapter;
74
+    }
75
+
55 76
     /**
56 77
      * When E2EE is enabled it initializes sessions and sets the key.
57 78
      * Cleans up the sessions when disabled.
@@ -167,6 +188,39 @@ export class ManagedKeyHandler extends KeyHandler {
167 188
         this.e2eeCtx.setKey(id, key, index);
168 189
     }
169 190
 
191
+    /**
192
+     * Handles the SAS ready event.
193
+     *
194
+     * @param {string} pId - The participant ID.
195
+     * @param {Uint8Array} sas - The bytes from sas.generate_bytes..
196
+     * @private
197
+     */
198
+    _onParticipantSasReady(pId, sas) {
199
+        this.conference.eventEmitter.emit(JitsiConferenceEvents.E2EE_VERIFICATION_READY, pId, sas);
200
+    }
201
+
202
+    /**
203
+     * Handles the sas available event.
204
+     *
205
+     * @param {string} pId - The participant ID.
206
+     * @private
207
+     */
208
+    _onParticipantSasAvailable(pId) {
209
+        this.conference.eventEmitter.emit(JitsiConferenceEvents.E2EE_VERIFICATION_AVAILABLE, pId);
210
+    }
211
+
212
+
213
+    /**
214
+     * Handles the SAS completed event.
215
+     *
216
+     * @param {string} pId - The participant ID.
217
+     * @param {boolean} success - Wheter the verification was succesfull.
218
+     * @private
219
+     */
220
+    _onParticipantVerificationCompleted(pId, success, message) {
221
+        this.conference.eventEmitter.emit(JitsiConferenceEvents.E2EE_VERIFICATION_COMPLETED, pId, success, message);
222
+    }
223
+
170 224
     /**
171 225
      * Generates a new 256 bit random key.
172 226
      *

+ 477
- 25
modules/e2ee/OlmAdapter.js ファイルの表示

@@ -10,6 +10,9 @@ import Deferred from '../util/Deferred';
10 10
 import Listenable from '../util/Listenable';
11 11
 import { FEATURE_E2EE, JITSI_MEET_MUC_TYPE } from '../xmpp/xmpp';
12 12
 
13
+import { E2EEErrors } from './E2EEErrors';
14
+import { generateSas } from './SAS';
15
+
13 16
 const logger = getLogger(__filename);
14 17
 
15 18
 const REQ_TIMEOUT = 5 * 1000;
@@ -19,15 +22,25 @@ const OLM_MESSAGE_TYPES = {
19 22
     KEY_INFO: 'key-info',
20 23
     KEY_INFO_ACK: 'key-info-ack',
21 24
     SESSION_ACK: 'session-ack',
22
-    SESSION_INIT: 'session-init'
25
+    SESSION_INIT: 'session-init',
26
+    SAS_START: 'sas-start',
27
+    SAS_ACCEPT: 'sas-accept',
28
+    SAS_KEY: 'sas-key',
29
+    SAS_MAC: 'sas-mac'
23 30
 };
24 31
 
32
+const OLM_SAS_NUM_BYTES = 6;
33
+const OLM_KEY_VERIFICATION_MAC_INFO = 'Jitsi-KEY_VERIFICATION_MAC';
34
+const OLM_KEY_VERIFICATION_MAC_KEY_IDS = 'Jitsi-KEY_IDS';
35
+
25 36
 const kOlmData = Symbol('OlmData');
26 37
 
27 38
 const OlmAdapterEvents = {
28
-    OLM_ID_KEY_READY: 'olm.id_key_ready',
29 39
     PARTICIPANT_E2EE_CHANNEL_READY: 'olm.participant_e2ee_channel_ready',
30
-    PARTICIPANT_KEY_UPDATED: 'olm.partitipant_key_updated'
40
+    PARTICIPANT_SAS_AVAILABLE: 'olm.participant_sas_available',
41
+    PARTICIPANT_SAS_READY: 'olm.participant_sas_ready',
42
+    PARTICIPANT_KEY_UPDATED: 'olm.partitipant_key_updated',
43
+    PARTICIPANT_VERIFICATION_COMPLETED: 'olm.participant_verification_completed'
31 44
 };
32 45
 
33 46
 /**
@@ -59,8 +72,8 @@ export class OlmAdapter extends Listenable {
59 72
 
60 73
         this._conf = conference;
61 74
         this._init = new Deferred();
62
-        this._key = undefined;
63
-        this._keyIndex = -1;
75
+        this._mediaKey = undefined;
76
+        this._mediaKeyIndex = -1;
64 77
         this._reqs = new Map();
65 78
         this._sessionInitialization = undefined;
66 79
 
@@ -77,6 +90,15 @@ export class OlmAdapter extends Listenable {
77 90
         }
78 91
     }
79 92
 
93
+    /**
94
+     * Returns the current participants conference ID.
95
+     *
96
+     * @returns {string}
97
+     */
98
+    get myId() {
99
+        return this._conf.myUserId();
100
+    }
101
+
80 102
     /**
81 103
      * Starts new olm sessions with every other participant that has the participantId "smaller" the localParticipantId.
82 104
      */
@@ -124,8 +146,8 @@ export class OlmAdapter extends Listenable {
124 146
      */
125 147
     async updateKey(key) {
126 148
         // Store it locally for new sessions.
127
-        this._key = key;
128
-        this._keyIndex++;
149
+        this._mediaKey = key;
150
+        this._mediaKeyIndex++;
129 151
 
130 152
         // Broadcast it.
131 153
         const promises = [];
@@ -169,7 +191,7 @@ export class OlmAdapter extends Listenable {
169 191
 
170 192
         // TODO: retry failed ones?
171 193
 
172
-        return this._keyIndex;
194
+        return this._mediaKeyIndex;
173 195
     }
174 196
 
175 197
     /**
@@ -177,10 +199,10 @@ export class OlmAdapter extends Listenable {
177 199
      * @param {Uint8Array|boolean} key - The new key.
178 200
      * @returns {number}
179 201
     */
180
-    updateCurrentKey(key) {
181
-        this._key = key;
202
+    updateCurrentMediaKey(key) {
203
+        this._mediaKey = key;
182 204
 
183
-        return this._keyIndex;
205
+        return this._mediaKeyIndex;
184 206
     }
185 207
 
186 208
     /**
@@ -196,7 +218,6 @@ export class OlmAdapter extends Listenable {
196 218
         }
197 219
     }
198 220
 
199
-
200 221
     /**
201 222
      * Frees the olmData sessions for all participants.
202 223
      *
@@ -207,6 +228,48 @@ export class OlmAdapter extends Listenable {
207 228
         }
208 229
     }
209 230
 
231
+    /**
232
+     * Sends sacMac if channel verification waas successful.
233
+     *
234
+     */
235
+    markParticipantVerified(participant, isVerified) {
236
+        const olmData = this._getParticipantOlmData(participant);
237
+
238
+        const pId = participant.getId();
239
+
240
+        if (!isVerified) {
241
+            olmData.sasVerification = undefined;
242
+            logger.warn(`Verification failed for participant ${pId}`);
243
+            this.eventEmitter.emit(
244
+                OlmAdapterEvents.PARTICIPANT_VERIFICATION_COMPLETED,
245
+                pId,
246
+                false,
247
+                E2EEErrors.E2EE_SAS_CHANNEL_VERIFICATION_FAILED);
248
+
249
+            return;
250
+        }
251
+
252
+        if (!olmData.sasVerification) {
253
+            logger.warn(`Participant ${pId} does not have valid sasVerification`);
254
+            this.eventEmitter.emit(
255
+                OlmAdapterEvents.PARTICIPANT_VERIFICATION_COMPLETED,
256
+                pId,
257
+                false,
258
+                E2EEErrors.E2EE_SAS_INVALID_SAS_VERIFICATION);
259
+
260
+            return;
261
+        }
262
+
263
+        const { sas, sasMacSent } = olmData.sasVerification;
264
+
265
+        if (sas && sas.is_their_key_set() && !sasMacSent) {
266
+            this._sendSasMac(participant);
267
+
268
+            // Mark the MAC as sent so we don't send it multiple times.
269
+            olmData.sasVerification.sasMacSent = true;
270
+        }
271
+    }
272
+
210 273
     /**
211 274
      * Internal helper to bootstrap the olm library.
212 275
      *
@@ -222,29 +285,99 @@ export class OlmAdapter extends Listenable {
222 285
             this._olmAccount = new Olm.Account();
223 286
             this._olmAccount.create();
224 287
 
225
-            const idKeys = JSON.parse(this._olmAccount.identity_keys());
226
-
227
-            this._idKey = idKeys.curve25519;
288
+            this._idKeys = JSON.parse(this._olmAccount.identity_keys());
228 289
 
229 290
             logger.debug(`Olm ${Olm.get_library_version().join('.')} initialized`);
230 291
             this._init.resolve();
231
-            this._onIdKeyReady(this._idKey);
292
+            this._onIdKeysReady(this._idKeys);
232 293
         } catch (e) {
233 294
             logger.error('Failed to initialize Olm', e);
234 295
             this._init.reject(e);
235 296
         }
297
+    }
298
+
299
+    /**
300
+     * Starts the verification process for the given participant as described here
301
+     * https://spec.matrix.org/latest/client-server-api/#short-authentication-string-sas-verification
302
+     *
303
+     *    |                                 |
304
+          | m.key.verification.start        |
305
+          |-------------------------------->|
306
+          |                                 |
307
+          |       m.key.verification.accept |
308
+          |<--------------------------------|
309
+          |                                 |
310
+          | m.key.verification.key          |
311
+          |-------------------------------->|
312
+          |                                 |
313
+          |          m.key.verification.key |
314
+          |<--------------------------------|
315
+          |                                 |
316
+          | m.key.verification.mac          |
317
+          |-------------------------------->|
318
+          |                                 |
319
+          |          m.key.verification.mac |
320
+          |<--------------------------------|
321
+          |                                 |
322
+     *
323
+     * @param {JitsiParticipant} participant - The target participant.
324
+     * @returns {Promise<void>}
325
+     * @private
326
+     */
327
+    startVerification(participant) {
328
+        const pId = participant.getId();
329
+        const olmData = this._getParticipantOlmData(participant);
330
+
331
+        if (!olmData.session) {
332
+            logger.warn(`Tried to start verification with participant ${pId} but we have no session`);
333
+
334
+            return;
335
+        }
336
+
337
+        if (olmData.sasVerification) {
338
+            logger.warn(`There is already a verification in progress with participant ${pId}`);
339
+
340
+            return;
341
+        }
342
+
343
+        olmData.sasVerification = {
344
+            sas: new Olm.SAS(),
345
+            transactionId: uuidv4()
346
+        };
236 347
 
348
+        const startContent = {
349
+            transactionId: olmData.sasVerification.transactionId
350
+        };
351
+
352
+        olmData.sasVerification.startContent = startContent;
353
+        olmData.sasVerification.isInitiator = true;
354
+
355
+        const startMessage = {
356
+            [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
357
+            olm: {
358
+                type: OLM_MESSAGE_TYPES.SAS_START,
359
+                data: startContent
360
+            }
361
+        };
362
+
363
+        this._sendMessage(startMessage, pId);
237 364
     }
238 365
 
239 366
     /**
240 367
      * Publishes our own Olmn id key in presence.
241 368
      * @private
242 369
      */
243
-    _onIdKeyReady(idKey) {
244
-        logger.debug(`Olm id key ready: ${idKey}`);
370
+    _onIdKeysReady(idKeys) {
371
+        logger.debug(`Olm id key ready: ${idKeys}`);
245 372
 
246 373
         // Publish it in presence.
247
-        this._conf.setLocalParticipantProperty('e2ee.idKey', idKey);
374
+        for (const keyType in idKeys) {
375
+            if (idKeys.hasOwnProperty(keyType)) {
376
+                const key = idKeys[keyType];
377
+
378
+                this._conf.setLocalParticipantProperty(`e2ee.idKey.${keyType}`, key);
379
+            }
380
+        }
248 381
     }
249 382
 
250 383
     /**
@@ -265,9 +398,9 @@ export class OlmAdapter extends Listenable {
265 398
     _encryptKeyInfo(session) {
266 399
         const keyInfo = {};
267 400
 
268
-        if (this._key !== undefined) {
269
-            keyInfo.key = this._key ? base64js.fromByteArray(this._key) : false;
270
-            keyInfo.keyIndex = this._keyIndex;
401
+        if (this._mediaKey !== undefined) {
402
+            keyInfo.key = this._mediaKey ? base64js.fromByteArray(this._mediaKey) : false;
403
+            keyInfo.keyIndex = this._mediaKeyIndex;
271 404
         }
272 405
 
273 406
         return session.encrypt(JSON.stringify(keyInfo));
@@ -470,6 +603,266 @@ export class OlmAdapter extends Listenable {
470 603
             }
471 604
             break;
472 605
         }
606
+        case OLM_MESSAGE_TYPES.SAS_START: {
607
+            if (!olmData.session) {
608
+                logger.debug(`Received sas init message from ${pId} but we have no session for them!`);
609
+
610
+                this._sendError(participant, 'No session found while processing sas-init');
611
+
612
+                return;
613
+            }
614
+
615
+            if (olmData.sasVerification?.sas) {
616
+                logger.warn(`SAS already created for participant ${pId}`);
617
+                this.eventEmitter.emit(
618
+                    OlmAdapterEvents.PARTICIPANT_VERIFICATION_COMPLETED,
619
+                    pId,
620
+                    false,
621
+                    E2EEErrors.E2EE_SAS_INVALID_SAS_VERIFICATION);
622
+
623
+                return;
624
+            }
625
+
626
+            const { transactionId } = msg.data;
627
+
628
+            const sas = new Olm.SAS();
629
+
630
+            olmData.sasVerification = {
631
+                sas,
632
+                transactionId,
633
+                isInitiator: false
634
+            };
635
+
636
+            const pubKey = olmData.sasVerification.sas.get_pubkey();
637
+            const commitment = this._computeCommitment(pubKey, msg.data);
638
+
639
+            /* The first phase of the verification process, the Key agreement phase
640
+                https://spec.matrix.org/latest/client-server-api/#short-authentication-string-sas-verification
641
+            */
642
+            const acceptMessage = {
643
+                [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
644
+                olm: {
645
+                    type: OLM_MESSAGE_TYPES.SAS_ACCEPT,
646
+                    data: {
647
+                        transactionId,
648
+                        commitment
649
+                    }
650
+                }
651
+            };
652
+
653
+            this._sendMessage(acceptMessage, pId);
654
+            break;
655
+        }
656
+        case OLM_MESSAGE_TYPES.SAS_ACCEPT: {
657
+            if (!olmData.session) {
658
+                logger.debug(`Received sas accept message from ${pId} but we have no session for them!`);
659
+
660
+                this._sendError(participant, 'No session found while processing sas-accept');
661
+
662
+                return;
663
+            }
664
+
665
+            const { commitment, transactionId } = msg.data;
666
+
667
+
668
+            if (!olmData.sasVerification) {
669
+                logger.warn(`SAS_ACCEPT Participant ${pId} does not have valid sasVerification`);
670
+                this.eventEmitter.emit(
671
+                    OlmAdapterEvents.PARTICIPANT_VERIFICATION_COMPLETED,
672
+                    pId,
673
+                    false,
674
+                    E2EEErrors.E2EE_SAS_INVALID_SAS_VERIFICATION);
675
+
676
+                return;
677
+            }
678
+
679
+            if (olmData.sasVerification.sasCommitment) {
680
+                logger.debug(`Already received sas commitment message from ${pId}!`);
681
+
682
+                this._sendError(participant, 'Already received sas commitment message from ${pId}!');
683
+
684
+                return;
685
+            }
686
+
687
+            olmData.sasVerification.sasCommitment = commitment;
688
+
689
+            const pubKey = olmData.sasVerification.sas.get_pubkey();
690
+
691
+            // Send KEY.
692
+            const keyMessage = {
693
+                [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
694
+                olm: {
695
+                    type: OLM_MESSAGE_TYPES.SAS_KEY,
696
+                    data: {
697
+                        key: pubKey,
698
+                        transactionId
699
+                    }
700
+                }
701
+            };
702
+
703
+            this._sendMessage(keyMessage, pId);
704
+
705
+            olmData.sasVerification.keySent = true;
706
+            break;
707
+        }
708
+        case OLM_MESSAGE_TYPES.SAS_KEY: {
709
+            if (!olmData.session) {
710
+                logger.debug(`Received sas key message from ${pId} but we have no session for them!`);
711
+
712
+                this._sendError(participant, 'No session found while processing sas-key');
713
+
714
+                return;
715
+            }
716
+
717
+            if (!olmData.sasVerification) {
718
+                logger.warn(`SAS_KEY Participant ${pId} does not have valid sasVerification`);
719
+                this.eventEmitter.emit(
720
+                    OlmAdapterEvents.PARTICIPANT_VERIFICATION_COMPLETED,
721
+                    pId,
722
+                    false,
723
+                    E2EEErrors.E2EE_SAS_INVALID_SAS_VERIFICATION);
724
+
725
+                return;
726
+            }
727
+
728
+            const { isInitiator, sas, sasCommitment, startContent, keySent } = olmData.sasVerification;
729
+
730
+            if (sas.is_their_key_set()) {
731
+                logger.warn('SAS already has their key!');
732
+
733
+                return;
734
+            }
735
+
736
+            const { key: theirKey, transactionId } = msg.data;
737
+
738
+            if (sasCommitment) {
739
+                const commitment = this._computeCommitment(theirKey, startContent);
740
+
741
+                if (sasCommitment !== commitment) {
742
+                    this._sendError(participant, 'OlmAdapter commitments mismatched');
743
+                    this.eventEmitter.emit(
744
+                        OlmAdapterEvents.PARTICIPANT_VERIFICATION_COMPLETED,
745
+                        pId,
746
+                        false,
747
+                        E2EEErrors.E2EE_SAS_COMMITMENT_MISMATCHED);
748
+                    olmData.sasVerification.free();
749
+
750
+                    return;
751
+                }
752
+            }
753
+
754
+            sas.set_their_key(theirKey);
755
+
756
+            const pubKey = sas.get_pubkey();
757
+
758
+            const myInfo = `${this.myId}|${pubKey}`;
759
+            const theirInfo = `${pId}|${theirKey}`;
760
+
761
+            const info = isInitiator ? `${myInfo}|${theirInfo}` : `${theirInfo}|${myInfo}`;
762
+
763
+            const sasBytes = sas.generate_bytes(info, OLM_SAS_NUM_BYTES);
764
+            const generatedSas = generateSas(sasBytes);
765
+
766
+            this.eventEmitter.emit(OlmAdapterEvents.PARTICIPANT_SAS_READY, pId, generatedSas);
767
+
768
+            if (keySent) {
769
+                return;
770
+            }
771
+
772
+            const keyMessage = {
773
+                [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
774
+                olm: {
775
+                    type: OLM_MESSAGE_TYPES.SAS_KEY,
776
+                    data: {
777
+                        key: pubKey,
778
+                        transactionId
779
+                    }
780
+                }
781
+            };
782
+
783
+            this._sendMessage(keyMessage, pId);
784
+
785
+            olmData.sasVerification.keySent = true;
786
+            break;
787
+        }
788
+        case OLM_MESSAGE_TYPES.SAS_MAC: {
789
+            if (!olmData.session) {
790
+                logger.debug(`Received sas mac message from ${pId} but we have no session for them!`);
791
+
792
+                this._sendError(participant, 'No session found while processing sas-mac');
793
+
794
+                return;
795
+            }
796
+
797
+            const { keys, mac, transactionId } = msg.data;
798
+
799
+            if (!mac || !keys) {
800
+                logger.warn('Invalid SAS MAC message');
801
+
802
+                return;
803
+            }
804
+
805
+            if (!olmData.sasVerification) {
806
+                logger.warn(`SAS_MAC Participant ${pId} does not have valid sasVerification`);
807
+
808
+                return;
809
+            }
810
+
811
+            const sas = olmData.sasVerification.sas;
812
+
813
+            // Verify the received MACs.
814
+            const baseInfo = `${OLM_KEY_VERIFICATION_MAC_INFO}${pId}${this.myId}${transactionId}`;
815
+            const keysMac = sas.calculate_mac(
816
+                Object.keys(mac).sort().join(','), // eslint-disable-line newline-per-chained-call
817
+                baseInfo + OLM_KEY_VERIFICATION_MAC_KEY_IDS
818
+            );
819
+
820
+            if (keysMac !== keys) {
821
+                logger.error('SAS verification error: keys MAC mismatch');
822
+                this.eventEmitter.emit(
823
+                    OlmAdapterEvents.PARTICIPANT_VERIFICATION_COMPLETED,
824
+                    pId,
825
+                    false,
826
+                    E2EEErrors.E2EE_SAS_KEYS_MAC_MISMATCH);
827
+
828
+                return;
829
+            }
830
+
831
+            if (!olmData.ed25519) {
832
+                logger.warn('SAS verification error: Missing ed25519 key');
833
+
834
+                this.eventEmitter.emit(
835
+                    OlmAdapterEvents.PARTICIPANT_VERIFICATION_COMPLETED,
836
+                    pId,
837
+                    false,
838
+                    E2EEErrors.E2EE_SAS_MISSING_KEY);
839
+
840
+                return;
841
+            }
842
+
843
+            for (const [ keyInfo, computedMac ] of Object.entries(mac)) {
844
+                const ourComputedMac = sas.calculate_mac(
845
+                    olmData.ed25519,
846
+                    baseInfo + keyInfo
847
+                );
848
+
849
+                if (computedMac !== ourComputedMac) {
850
+                    logger.error('SAS verification error: MAC mismatch');
851
+                    this.eventEmitter.emit(
852
+                        OlmAdapterEvents.PARTICIPANT_VERIFICATION_COMPLETED,
853
+                        pId,
854
+                        false,
855
+                        E2EEErrors.E2EE_SAS_MAC_MISMATCH);
856
+
857
+                    return;
858
+                }
859
+            }
860
+
861
+            logger.info(`SAS MAC verified for participant ${pId}`);
862
+            this.eventEmitter.emit(OlmAdapterEvents.PARTICIPANT_VERIFICATION_COMPLETED, pId, true);
863
+
864
+            break;
865
+        }
473 866
         }
474 867
     }
475 868
 
@@ -494,11 +887,13 @@ export class OlmAdapter extends Listenable {
494 887
     * @private
495 888
     */
496 889
     async _onParticipantPropertyChanged(participant, name, oldValue, newValue) {
890
+        const participantId = participant.getId();
891
+        const olmData = this._getParticipantOlmData(participant);
892
+
497 893
         switch (name) {
498 894
         case 'e2ee.enabled':
499 895
             if (newValue && this._conf.isE2EEEnabled()) {
500 896
                 const localParticipantId = this._conf.myUserId();
501
-                const participantId = participant.getId();
502 897
                 const participantFeatures = await participant.getFeatures();
503 898
 
504 899
                 if (participantFeatures.has(FEATURE_E2EE) && localParticipantId < participantId) {
@@ -507,7 +902,6 @@ export class OlmAdapter extends Listenable {
507 902
                     }
508 903
                     await this._sendSessionInit(participant);
509 904
 
510
-                    const olmData = this._getParticipantOlmData(participant);
511 905
                     const uuid = uuidv4();
512 906
 
513 907
                     const d = new Deferred();
@@ -534,6 +928,10 @@ export class OlmAdapter extends Listenable {
534 928
                 }
535 929
             }
536 930
             break;
931
+        case 'e2ee.idKey.ed25519':
932
+            olmData.ed25519 = newValue;
933
+            this.eventEmitter.emit(OlmAdapterEvents.PARTICIPANT_SAS_AVAILABLE, participantId);
934
+            break;
537 935
         }
538 936
     }
539 937
 
@@ -613,7 +1011,7 @@ export class OlmAdapter extends Listenable {
613 1011
             olm: {
614 1012
                 type: OLM_MESSAGE_TYPES.SESSION_INIT,
615 1013
                 data: {
616
-                    idKey: this._idKey,
1014
+                    idKey: this._idKeys.curve25519,
617 1015
                     otKey,
618 1016
                     uuid
619 1017
                 }
@@ -636,6 +1034,60 @@ export class OlmAdapter extends Listenable {
636 1034
 
637 1035
         return d;
638 1036
     }
1037
+
1038
+    /**
1039
+     * Builds and sends the SAS MAC message to the given participant.
1040
+     * The second phase of the verification process, the Key verification phase
1041
+        https://spec.matrix.org/latest/client-server-api/#short-authentication-string-sas-verification
1042
+     */
1043
+    _sendSasMac(participant) {
1044
+        const pId = participant.getId();
1045
+        const olmData = this._getParticipantOlmData(participant);
1046
+        const { sas, transactionId } = olmData.sasVerification;
1047
+
1048
+        // Calculate and send MAC with the keys to be verified.
1049
+        const mac = {};
1050
+        const keyList = [];
1051
+        const baseInfo = `${OLM_KEY_VERIFICATION_MAC_INFO}${this.myId}${pId}${transactionId}`;
1052
+
1053
+        const deviceKeyId = `ed25519:${pId}`;
1054
+
1055
+        mac[deviceKeyId] = sas.calculate_mac(
1056
+            this._idKeys.ed25519,
1057
+            baseInfo + deviceKeyId);
1058
+        keyList.push(deviceKeyId);
1059
+
1060
+        const keys = sas.calculate_mac(
1061
+            keyList.sort().join(','),
1062
+            baseInfo + OLM_KEY_VERIFICATION_MAC_KEY_IDS
1063
+        );
1064
+
1065
+        const macMessage = {
1066
+            [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
1067
+            olm: {
1068
+                type: OLM_MESSAGE_TYPES.SAS_MAC,
1069
+                data: {
1070
+                    keys,
1071
+                    mac,
1072
+                    transactionId
1073
+                }
1074
+            }
1075
+        };
1076
+
1077
+        this._sendMessage(macMessage, pId);
1078
+    }
1079
+
1080
+    /**
1081
+     * Computes the commitment.
1082
+     */
1083
+    _computeCommitment(pubKey, data) {
1084
+        const olmUtil = new Olm.Utility();
1085
+        const commitment = olmUtil.sha256(pubKey + JSON.stringify(data));
1086
+
1087
+        olmUtil.free();
1088
+
1089
+        return commitment;
1090
+    }
639 1091
 }
640 1092
 
641 1093
 /**

+ 137
- 0
modules/e2ee/SAS.js ファイルの表示

@@ -0,0 +1,137 @@
1
+/* eslint-disable no-bitwise */
2
+/* eslint-disable no-mixed-operators */
3
+
4
+/**
5
+ * Generates a SAS composed of decimal numbers.
6
+ * Borrowed from the Matrix JS SDK.
7
+ *
8
+ * @param {Uint8Array} sasBytes - The bytes from sas.generate_bytes.
9
+ * @returns Array<number>
10
+ */
11
+function generateDecimalSas(sasBytes) {
12
+    /**
13
+     *      +--------+--------+--------+--------+--------+
14
+     *      | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
15
+     *      +--------+--------+--------+--------+--------+
16
+     * bits: 87654321 87654321 87654321 87654321 87654321
17
+     *       \____________/\_____________/\____________/
18
+     *         1st number    2nd number     3rd number
19
+     */
20
+    return [
21
+        (sasBytes[0] << 5 | sasBytes[1] >> 3) + 1000,
22
+        ((sasBytes[1] & 0x7) << 10 | sasBytes[2] << 2 | sasBytes[3] >> 6) + 1000,
23
+        ((sasBytes[3] & 0x3f) << 7 | sasBytes[4] >> 1) + 1000
24
+    ];
25
+}
26
+
27
+const emojiMapping = [
28
+    [ '🐶', 'dog' ],
29
+    [ '🐱', 'cat' ],
30
+    [ '🦁', 'lion' ],
31
+    [ '🐎', 'horse' ],
32
+    [ '🦄', 'unicorn' ],
33
+    [ '🐷', 'pig' ],
34
+    [ '🐘', 'elephant' ],
35
+    [ '🐰', 'rabbit' ],
36
+    [ '🐼', 'panda' ],
37
+    [ '🐓', 'rooster' ],
38
+    [ '🐧', 'penguin' ],
39
+    [ '🐢', 'turtle' ],
40
+    [ '🐟', 'fish' ],
41
+    [ '🐙', 'octopus' ],
42
+    [ '🦋', 'butterfly' ],
43
+    [ '🌷', 'flower' ],
44
+    [ '🌳', 'tree' ],
45
+    [ '🌵', 'cactus' ],
46
+    [ '🍄', 'mushroom' ],
47
+    [ '🌏', 'globe' ],
48
+    [ '🌙', 'moon' ],
49
+    [ '☁️', 'cloud' ],
50
+    [ '🔥', 'fire' ],
51
+    [ '🍌', 'banana' ],
52
+    [ '🍎', 'apple' ],
53
+    [ '🍓', 'strawberry' ],
54
+    [ '🌽', 'corn' ],
55
+    [ '🍕', 'pizza' ],
56
+    [ '🎂', 'cake' ],
57
+    [ '❤️', 'heart' ],
58
+    [ '🙂', 'smiley' ],
59
+    [ '🤖', 'robot' ],
60
+    [ '🎩', 'hat' ],
61
+    [ '👓', 'glasses' ],
62
+    [ '🔧', 'spanner' ],
63
+    [ '🎅', 'santa' ],
64
+    [ '👍', 'thumbs up' ],
65
+    [ '☂️', 'umbrella' ],
66
+    [ '⌛', 'hourglass' ],
67
+    [ '⏰', 'clock' ],
68
+    [ '🎁', 'gift' ],
69
+    [ '💡', 'light bulb' ],
70
+    [ '📕', 'book' ],
71
+    [ '✏️', 'pencil' ],
72
+    [ '📎', 'paperclip' ],
73
+    [ '✂️', 'scissors' ],
74
+    [ '🔒', 'lock' ],
75
+    [ '🔑', 'key' ],
76
+    [ '🔨', 'hammer' ],
77
+    [ '☎️', 'telephone' ],
78
+    [ '🏁', 'flag' ],
79
+    [ '🚂', 'train' ],
80
+    [ '🚲', 'bicycle' ],
81
+    [ '✈️', 'aeroplane' ],
82
+    [ '🚀', 'rocket' ],
83
+    [ '🏆', 'trophy' ],
84
+    [ '⚽', 'ball' ],
85
+    [ '🎸', 'guitar' ],
86
+    [ '🎺', 'trumpet' ],
87
+    [ '🔔', 'bell' ],
88
+    [ '⚓️', 'anchor' ],
89
+    [ '🎧', 'headphones' ],
90
+    [ '📁', 'folder' ],
91
+    [ '📌', 'pin' ]
92
+];
93
+
94
+/**
95
+ * Generates a SAS composed of defimal numbers.
96
+ * Borrowed from the Matrix JS SDK.
97
+ *
98
+ * @param {Uint8Array} sasBytes - The bytes from sas.generate_bytes.
99
+ * @returns Array<number>
100
+ */
101
+function generateEmojiSas(sasBytes) {
102
+    // Just like base64.
103
+    const emojis = [
104
+        sasBytes[0] >> 2,
105
+        (sasBytes[0] & 0x3) << 4 | sasBytes[1] >> 4,
106
+        (sasBytes[1] & 0xf) << 2 | sasBytes[2] >> 6,
107
+        sasBytes[2] & 0x3f,
108
+        sasBytes[3] >> 2,
109
+        (sasBytes[3] & 0x3) << 4 | sasBytes[4] >> 4,
110
+        (sasBytes[4] & 0xf) << 2 | sasBytes[5] >> 6
111
+    ];
112
+
113
+    return emojis.map(num => emojiMapping[num]);
114
+}
115
+
116
+const sasGenerators = {
117
+    decimal: generateDecimalSas,
118
+    emoji: generateEmojiSas
119
+};
120
+
121
+/**
122
+ * Generates multiple SAS for the given bytes.
123
+ *
124
+ * @param {Uint8Array} sasBytes - The bytes from sas.generate_bytes.
125
+ * @returns {object}
126
+ */
127
+export function generateSas(sasBytes) {
128
+    const sas = {};
129
+
130
+    for (const method in sasGenerators) {
131
+        if (sasGenerators.hasOwnProperty(method)) {
132
+            sas[method] = sasGenerators[method](sasBytes);
133
+        }
134
+    }
135
+
136
+    return sas;
137
+}

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