From 7ed448780db67a5548716ea4858dff6b1c8be61b Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Tue, 19 Nov 2024 08:59:13 -0400 Subject: [PATCH] feat: remove deferred class usage from OlmAdapter --- globals.d.ts | 4 + modules/browser/BrowserCapabilities.js | 3 - .../e2ee/{E2EEContext.js => E2EEContext.ts} | 117 ++++-- modules/e2ee/E2EEncryption.ts | 4 +- modules/e2ee/ManagedKeyHandler.ts | 154 +++++--- modules/e2ee/OlmAdapter.ts | 350 ++++++++++-------- modules/xmpp/strophe.util.ts | 1 + 7 files changed, 390 insertions(+), 243 deletions(-) rename modules/e2ee/{E2EEContext.js => E2EEContext.ts} (61%) diff --git a/globals.d.ts b/globals.d.ts index 3a0c3b31e9..57696d4a55 100644 --- a/globals.d.ts +++ b/globals.d.ts @@ -8,4 +8,8 @@ declare global { RTCRtpScriptTransform: Window.RTCRtpScriptTransform; onrtctransform: Window.onrtctransform; } + + declare class RTCRtpScriptTransform { + constructor(worker: Worker, options?: any); + } } diff --git a/modules/browser/BrowserCapabilities.js b/modules/browser/BrowserCapabilities.js index 33ac0ded4a..142428a305 100644 --- a/modules/browser/BrowserCapabilities.js +++ b/modules/browser/BrowserCapabilities.js @@ -20,9 +20,6 @@ const FROZEN_MACOS_VERSION = '10.15.7'; * Implements browser capabilities for lib-jitsi-meet. */ export default class BrowserCapabilities extends BrowserDetection { - isWebKitBased() { - throw new Error('Method not implemented.'); - } /** * Tells whether or not the MediaStream/tt> is removed from the PeerConnection and disposed on video * mute (in order to turn off the camera device). This is needed on Firefox because of the following bug diff --git a/modules/e2ee/E2EEContext.js b/modules/e2ee/E2EEContext.ts similarity index 61% rename from modules/e2ee/E2EEContext.js rename to modules/e2ee/E2EEContext.ts index d73d5596cb..bb6e7790f9 100644 --- a/modules/e2ee/E2EEContext.js +++ b/modules/e2ee/E2EEContext.ts @@ -1,11 +1,28 @@ /* global RTCRtpScriptTransform */ -import { getLogger } from '@jitsi/logger'; +import { getLogger } from "@jitsi/logger"; + +// Extend the RTCRtpReceiver interface due to lack of support of streams +interface CustomRTCRtpReceiver extends RTCRtpReceiver { + createEncodedStreams?: () => { + readable: ReadableStream; + writable: WritableStream; + }; + transform: RTCRtpScriptTransform; +} + +interface CustomRTCRtpSender extends RTCRtpSender { + createEncodedStreams?: () => { + readable: ReadableStream; + writable: WritableStream; + }; + transform: RTCRtpScriptTransform; +} const logger = getLogger(__filename); // Flag to set on senders / receivers to avoid setting up the encryption transform // more than once. -const kJitsiE2EE = Symbol('kJitsiE2EE'); +const kJitsiE2EE = Symbol("kJitsiE2EE"); /** * Context encapsulating the cryptography bits required for E2EE. @@ -20,18 +37,20 @@ const kJitsiE2EE = Symbol('kJitsiE2EE'); * - allow for the key to be rotated frequently. */ export default class E2EEcontext { - // private _worker: Worker; + private _worker: Worker; /** * Build a new E2EE context instance, which will be used in a given conference. */ constructor() { // Determine the URL for the worker script. Relative URLs are relative to // the entry point, not the script that launches the worker. - let baseUrl = ''; - const ljm = document.querySelector('script[src*="lib-jitsi-meet"]');// as HTMLImageElement; + let baseUrl = ""; + const ljm = document.querySelector( + 'script[src*="lib-jitsi-meet"]' + ); // as HTMLImageElement; if (ljm) { - const idx = ljm.src.lastIndexOf('/'); + const idx = ljm.src.lastIndexOf("/"); baseUrl = `${ljm.src.substring(0, idx)}/`; } @@ -41,19 +60,19 @@ export default class E2EEcontext { // If there is no baseUrl then we create the worker in a normal way // as you cant load scripts inside blobs from relative paths. // See: https://www.html5rocks.com/en/tutorials/workers/basics/#toc-inlineworkers-loadingscripts - if (baseUrl && baseUrl !== '/') { + if (baseUrl && baseUrl !== "/") { // Initialize the E2EE worker. In order to avoid CORS issues, start the worker and have it // synchronously load the JS. - const workerBlob - = new Blob([ `importScripts("${workerUrl}");` ], { type: 'application/javascript' }); + const workerBlob = new Blob([`importScripts("${workerUrl}");`], { + type: "application/javascript", + }); workerUrl = window.URL.createObjectURL(workerBlob); } - this._worker = new Worker(workerUrl, { name: 'E2EE Worker' }); - - this._worker.onerror = e => logger.error(e); + this._worker = new Worker(workerUrl, { name: "E2EE Worker" }); + this._worker.onerror = (e) => logger.error(e); } /** @@ -62,10 +81,10 @@ export default class E2EEcontext { * * @param {string} participantId - The participant that just left. */ - cleanup(participantId) { + cleanup(participantId: string) { this._worker.postMessage({ - operation: 'cleanup', - participantId + operation: "cleanup", + participantId, }); } @@ -75,7 +94,7 @@ export default class E2EEcontext { */ cleanupAll() { this._worker.postMessage({ - operation: 'cleanupAll' + operation: "cleanupAll", }); } @@ -87,7 +106,11 @@ export default class E2EEcontext { * @param {string} kind - The kind of track this receiver belongs to. * @param {string} participantId - The participant id that this receiver belongs to. */ - handleReceiver(receiver, kind, participantId) { + handleReceiver( + receiver: CustomRTCRtpReceiver, + kind: string, + participantId: string + ) { if (receiver[kJitsiE2EE]) { return; } @@ -95,20 +118,26 @@ export default class E2EEcontext { if (window.RTCRtpScriptTransform) { const options = { - operation: 'decode', - participantId + operation: "decode", + participantId, }; - receiver.transform = new RTCRtpScriptTransform(this._worker, options); + receiver.transform = new RTCRtpScriptTransform( + this._worker, + options + ); } else { const receiverStreams = receiver.createEncodedStreams(); - this._worker.postMessage({ - operation: 'decode', - readableStream: receiverStreams.readable, - writableStream: receiverStreams.writable, - participantId - }, [ receiverStreams.readable, receiverStreams.writable ]); + this._worker.postMessage( + { + operation: "decode", + readableStream: receiverStreams.readable, + writableStream: receiverStreams.writable, + participantId, + }, + [receiverStreams.readable, receiverStreams.writable] + ); } } @@ -120,7 +149,11 @@ export default class E2EEcontext { * @param {string} kind - The kind of track this sender belongs to. * @param {string} participantId - The participant id that this sender belongs to. */ - handleSender(sender, kind, participantId) { + handleSender( + sender: CustomRTCRtpSender, + kind: string, + participantId: string + ) { if (sender[kJitsiE2EE]) { return; } @@ -128,20 +161,23 @@ export default class E2EEcontext { if (window.RTCRtpScriptTransform) { const options = { - operation: 'encode', - participantId + operation: "encode", + participantId, }; sender.transform = new RTCRtpScriptTransform(this._worker, options); } else { const senderStreams = sender.createEncodedStreams(); - this._worker.postMessage({ - operation: 'encode', - readableStream: senderStreams.readable, - writableStream: senderStreams.writable, - participantId - }, [ senderStreams.readable, senderStreams.writable ]); + this._worker.postMessage( + { + operation: "encode", + readableStream: senderStreams.readable, + writableStream: senderStreams.writable, + participantId, + }, + [senderStreams.readable, senderStreams.writable] + ); } } @@ -153,13 +189,18 @@ export default class E2EEcontext { * @param {Uint8Array} pqKey - olm key for the given participant. * @param {Number} keyIndex - the key index. */ - setKey(participantId, olmKey, pqKey, index) { + setKey( + participantId: string, + olmKey: Uint8Array, + pqKey: Uint8Array, + index: Number + ) { this._worker.postMessage({ - operation: 'setKey', + operation: "setKey", olmKey, pqKey, index, - participantId + participantId, }); } } diff --git a/modules/e2ee/E2EEncryption.ts b/modules/e2ee/E2EEncryption.ts index c08c9b0247..8d809e63ca 100644 --- a/modules/e2ee/E2EEncryption.ts +++ b/modules/e2ee/E2EEncryption.ts @@ -1,5 +1,3 @@ -import base64js from "base64-js"; - import browser from "../browser"; import { ManagedKeyHandler } from "./ManagedKeyHandler"; @@ -53,7 +51,7 @@ export class E2EEncryption { * @param {boolean} enabled - whether E2EE should be enabled or not. * @returns {void} */ - async setEnabled(enabled) { + async setEnabled(enabled: boolean): Promise { await this._keyHandler.setEnabled(enabled); } diff --git a/modules/e2ee/ManagedKeyHandler.ts b/modules/e2ee/ManagedKeyHandler.ts index cc6207fdb6..69d6c7eebe 100644 --- a/modules/e2ee/ManagedKeyHandler.ts +++ b/modules/e2ee/ManagedKeyHandler.ts @@ -1,14 +1,14 @@ /// -import { getLogger } from '@jitsi/logger'; -import { debounce } from 'lodash-es'; +import { getLogger } from "@jitsi/logger"; +import { debounce } from "lodash-es"; -import * as JitsiConferenceEvents from '../../JitsiConferenceEvents'; +import * as JitsiConferenceEvents from "../../JitsiConferenceEvents"; -import { KeyHandler} from './KeyHandler'; -import { OlmAdapter } from './OlmAdapter'; -import { importKey, ratchet } from './crypto-utils'; -import JitsiParticipant from '../../JitsiParticipant'; +import { KeyHandler } from "./KeyHandler"; +import { OlmAdapter } from "./OlmAdapter"; +import { importKey, ratchet } from "./crypto-utils"; +import JitsiParticipant from "../../JitsiParticipant"; const logger = getLogger(__filename); @@ -43,42 +43,46 @@ export class ManagedKeyHandler extends KeyHandler { // Olm signalling events. this._olmAdapter.on( OlmAdapter.events.PARTICIPANT_KEY_UPDATED, - this._onParticipantKeyUpdated.bind(this)); + this._onParticipantKeyUpdated.bind(this) + ); this._olmAdapter.on( OlmAdapter.events.GENERATE_KEYS, - this._onKeyGeneration.bind(this)); + this._onKeyGeneration.bind(this) + ); this._olmAdapter.on( OlmAdapter.events.PARTICIPANT_SAS_READY, - this._onParticipantSasReady.bind(this)); + this._onParticipantSasReady.bind(this) + ); this._olmAdapter.on( OlmAdapter.events.PARTICIPANT_SAS_AVAILABLE, - this._onParticipantSasAvailable.bind(this)); + this._onParticipantSasAvailable.bind(this) + ); this._olmAdapter.on( OlmAdapter.events.PARTICIPANT_VERIFICATION_COMPLETED, - this._onParticipantVerificationCompleted.bind(this)); + this._onParticipantVerificationCompleted.bind(this) + ); this.conference.on( JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED, - this._onParticipantPropertyChanged.bind(this)); + this._onParticipantPropertyChanged.bind(this) + ); this.conference.on( JitsiConferenceEvents.USER_JOINED, - this._onParticipantJoined.bind(this)); + this._onParticipantJoined.bind(this) + ); this.conference.on( JitsiConferenceEvents.USER_LEFT, - this._onParticipantLeft.bind(this)); - this.conference.on( - JitsiConferenceEvents.CONFERENCE_JOINED, - () => { - this._conferenceJoined = true; - }); + this._onParticipantLeft.bind(this) + ); + this.conference.on(JitsiConferenceEvents.CONFERENCE_JOINED, () => { + this._conferenceJoined = true; + }); } - - /** * When E2EE is enabled it initializes sessions and sets the key. * Cleans up the sessions when disabled. @@ -87,7 +91,6 @@ export class ManagedKeyHandler extends KeyHandler { * @returns {boolean} */ async _setEnabled(enabled: boolean): Promise { - if (!enabled) { this._olmAdapter.clearAllParticipantsSessions(); return false; @@ -96,21 +99,23 @@ export class ManagedKeyHandler extends KeyHandler { try { this._onKeyGeneration(); await this._olmAdapter.initSessions(); - console.log('CHECK: all olm sessions should be established now!!!!!!'); - const mediaKeyIndex = await this._olmAdapter.updateKey(this._olmKey, this._pqKey); - + + logger.debug("All olm sessions should be established now!"); + + const mediaKeyIndex = await this._olmAdapter.updateKey( + this._olmKey, + this._pqKey + ); + // Set our key so we begin encrypting. this.setKey(this._olmKey, this._pqKey, mediaKeyIndex); - - } catch(e) { + } catch (e) { console.log(`_setEnabled got error ${e}`); return false; } // Generate a random key in case we are enabling. - return true; - } /** @@ -122,18 +127,24 @@ export class ManagedKeyHandler extends KeyHandler { * @param {*} newValue - The property's new value. * @private */ - async _onParticipantPropertyChanged(participant: JitsiParticipant, name: string, oldValue, newValue) { - + async _onParticipantPropertyChanged( + participant: JitsiParticipant, + name: string, + oldValue, + newValue + ) { if (newValue !== oldValue) { switch (name) { - case 'e2ee.idKey': - logger.debug(`Participant ${participant.getId()} updated their id key: ${newValue}`); - break; - case 'e2ee.enabled': - if (!newValue && this.enabled) { - this._olmAdapter.clearParticipantSession(participant); - } - break; + case "e2ee.idKey": + logger.debug( + `Participant ${participant.getId()} updated their id key: ${newValue}` + ); + break; + case "e2ee.enabled": + if (!newValue && this.enabled) { + this._olmAdapter.clearParticipantSession(participant); + } + break; } } } @@ -167,10 +178,12 @@ export class ManagedKeyHandler extends KeyHandler { * @private */ async _rotateKeyImpl() { - this._onKeyGeneration(); - const index = await this._olmAdapter.updateKey(this._olmKey, this._pqKey); + const index = await this._olmAdapter.updateKey( + this._olmKey, + this._pqKey + ); this.setKey(this._olmKey, this._pqKey, index); } @@ -183,7 +196,7 @@ export class ManagedKeyHandler extends KeyHandler { * @private */ async _ratchetKeyImpl() { - logger.debug('Ratchetting keys'); + logger.debug("Ratchetting keys"); const olmMaterial = await importKey(this._olmKey); this._olmKey = await ratchet(olmMaterial); @@ -191,9 +204,12 @@ export class ManagedKeyHandler extends KeyHandler { const pqMaterial = await importKey(this._pqKey); this._pqKey = await ratchet(pqMaterial); - const index = await this._olmAdapter.updateCurrentMediaKey(this._olmKey, this._pqKey); + const index = await this._olmAdapter.updateCurrentMediaKey( + this._olmKey, + this._pqKey + ); - this.setKey(this._olmKey,this._pqKey, index); + this.setKey(this._olmKey, this._pqKey, index); } /** @@ -204,9 +220,26 @@ export class ManagedKeyHandler extends KeyHandler { * @param {Number} index - The new key's index. * @private */ - _onParticipantKeyUpdated(id: string, olmKey: Uint8Array, pqKey: Uint8Array, index: number) { - logger.info('CHECKPOINT: _onParticipantKeyUpdated called setKey with id', id, 'olm key', olmKey, - 'pq key', pqKey, 'index', index); + _onParticipantKeyUpdated( + id: string, + olmKey: Uint8Array, + pqKey: Uint8Array, + index: number + ) { + logger.debug( + "CHECKPOINT: _onParticipantKeyUpdated called setKey with id", + id, + "olm key", + olmKey, + "pq key", + pqKey, + "index", + index, + "my olm key", + this._olmKey, + "my pq key", + this._pqKey + ); this.e2eeCtx.setKey(id, olmKey, pqKey, index); } @@ -231,7 +264,11 @@ export class ManagedKeyHandler extends KeyHandler { * @private */ _onParticipantSasReady(pId: string, sas: Uint8Array) { - this.conference.eventEmitter.emit(JitsiConferenceEvents.E2EE_VERIFICATION_READY, pId, sas); + this.conference.eventEmitter.emit( + JitsiConferenceEvents.E2EE_VERIFICATION_READY, + pId, + sas + ); } /** @@ -241,10 +278,12 @@ export class ManagedKeyHandler extends KeyHandler { * @private */ _onParticipantSasAvailable(pId: string) { - this.conference.eventEmitter.emit(JitsiConferenceEvents.E2EE_VERIFICATION_AVAILABLE, pId); + this.conference.eventEmitter.emit( + JitsiConferenceEvents.E2EE_VERIFICATION_AVAILABLE, + pId + ); } - /** * Handles the SAS completed event. * @@ -252,8 +291,17 @@ export class ManagedKeyHandler extends KeyHandler { * @param {boolean} success - Wheter the verification was succesfull. * @private */ - _onParticipantVerificationCompleted(pId: string, success: boolean, message) { - this.conference.eventEmitter.emit(JitsiConferenceEvents.E2EE_VERIFICATION_COMPLETED, pId, success, message); + _onParticipantVerificationCompleted( + pId: string, + success: boolean, + message + ) { + this.conference.eventEmitter.emit( + JitsiConferenceEvents.E2EE_VERIFICATION_COMPLETED, + pId, + success, + message + ); } /** diff --git a/modules/e2ee/OlmAdapter.ts b/modules/e2ee/OlmAdapter.ts index fd2973e0cd..fe4743a82b 100644 --- a/modules/e2ee/OlmAdapter.ts +++ b/modules/e2ee/OlmAdapter.ts @@ -9,7 +9,6 @@ import { isEqual } from "lodash-es"; import { v4 as uuidv4 } from "uuid"; import * as JitsiConferenceEvents from "../../JitsiConferenceEvents"; -import Deferred from "../util/Deferred"; import Listenable from "../util/Listenable"; import { FEATURE_E2EE, JITSI_MEET_MUC_TYPE } from "../xmpp/xmpp"; @@ -76,11 +75,14 @@ const OlmAdapterEvents = { export class OlmAdapter extends Listenable { private readonly _conf: JitsiConference; private _kem: KEM; - private _init: boolean; + private _init: Promise; private _mediaKeyOlm: Uint8Array; private _mediaKeyPQ: Uint8Array; private _mediaKeyIndex: number; - private _reqs: Map; + private _reqs: Map< + Uint8Array, + { resolve: (args?: unknown) => void; reject?: (args?: unknown) => void } + >; private _publicKey: Uint8Array; private _privateKey: Uint8Array; private _olmAccount: any; @@ -93,6 +95,9 @@ export class OlmAdapter extends Listenable { PARTICIPANT_VERIFICATION_COMPLETED: string; GENERATE_KEYS: string; }; + // Used to lock session initializations while initSession was called but not finished yet. + private _sessionInitializationInProgress = null; + /** * Creates an adapter instance for the given conference. */ @@ -106,14 +111,11 @@ export class OlmAdapter extends Listenable { this._reqs = new Map(); this._publicKey = undefined; this._privateKey = undefined; - this._init = false; - } - async enableOLM(): Promise { + this._sessionInitializationInProgress = null; + this._init = this._bootstrapOlm(); + if (OlmAdapter.isSupported()) { - if (!(await this._bootstrapOlm())) { - return false; - } this._conf.on( JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, this._onEndpointMessageReceived.bind(this) @@ -124,81 +126,108 @@ export class OlmAdapter extends Listenable { ); this._conf.on( JitsiConferenceEvents.USER_LEFT, - this._onParticipantLeft2.bind(this) + this._onParticipantLeft.bind(this) ); this._conf.on( JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED, this._onParticipantPropertyChanged.bind(this) ); - return true; } else { - return false; + this._init = Promise.reject(new Error("Olm not supported")); } } + /** - * Returns the current participants conference ID. + * Initializes the Olm library and sets up the account. * - * @returns {string} + * @returns {Promise} + * @private + */ + async _bootstrapOlm(): Promise { + if (!OlmAdapter.isSupported()) { + throw new Error("Olm is not supported on this platform."); + } + + try { + await window.Olm.init(); + + this._olmAccount = new window.Olm.Account(); + this._olmAccount.create(); + + this._idKeys = safeJsonParse(this._olmAccount.identity_keys()); + + logger.debug( + `Olm ${window.Olm.get_library_version().join(".")} initialized` + ); + + await this._initializeKemAndKeys(); + this._onIdKeysReady(this._idKeys); + } catch (e) { + logger.error("Failed to initialize Olm", e); + throw e; + } + } + + /** + * Returns the current participants conference ID. */ - get myId() { + get myId(): string { return this._conf.myUserId(); } async sendKeyInfoToAll() { // Broadcast it. + logger.debug( + `Send key info to all called ${{ + participants: this._conf.getParticipants(), + }}` + ); + const promises = []; - const localParticipantId = this._conf.myUserId(); for (const participant of this._conf.getParticipants()) { - if ( - participant.hasFeature(FEATURE_E2EE) && - localParticipantId < participant.getId() - ) { - const pId = participant.getId(); - const olmData = this._getParticipantOlmData(participant); + const pId = participant.getId(); + const olmData = this._getParticipantOlmData(participant); - // TODO: skip those who don't support E2EE. - if (!olmData.session || !olmData.pqSessionKey) { - logger.warn(`Tried to send KEY_INFO to participant ${participant.getDisplayName()} + // TODO: skip those who don't support E2EE. + if (!olmData.session || !olmData.pqSessionKey) { + logger.warn(`Tried to send KEY_INFO to participant ${participant.getDisplayName()} but we have no session ${olmData.session} and ${olmData.pqSessionKey}`); - // eslint-disable-next-line no-continue - continue; - } - const uuid = uuidv4(); + continue; + } - const { ciphertextStr, ivStr } = await this._encryptKeyInfoPQ( - olmData.pqSessionKey - ); + const uuid = uuidv4(); + const { ciphertextStr, ivStr } = await this._encryptKeyInfoPQ( + olmData.pqSessionKey + ); - const data = { - [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE, - olm: { - type: OLM_MESSAGE_TYPES.KEY_INFO, - data: { - ciphertext: this._encryptKeyInfo(olmData.session), - pqCiphertext: ciphertextStr, - iv: ivStr, - uuid, - }, + const data = { + [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE, + olm: { + type: OLM_MESSAGE_TYPES.KEY_INFO, + data: { + ciphertext: this._encryptKeyInfo(olmData.session), + pqCiphertext: ciphertextStr, + iv: ivStr, + uuid, }, - }; - const d = new Deferred(); + }, + }; - d.setRejectTimeout(REQ_TIMEOUT); - d.catch(() => { - this._reqs.delete(uuid); - }); - this._reqs.set(uuid, d); - promises.push(d); + const sessionPromise = new Promise((resolve, reject) => { + // Saving resolve function to be able to resolve this function later. + this._reqs.set(uuid, { resolve, reject }); + }); - logger.info( - `updateKey: sent KEY_INFO to ${participant.getDisplayName()}` - ); + promises.push(sessionPromise); - this._sendMessage(data, pId); - } + this._sendMessage(data, pId); + + logger.debug( + `updateKey: sent KEY_INFO to ${participant.getDisplayName()}` + ); } await Promise.allSettled(promises); @@ -208,26 +237,43 @@ export class OlmAdapter extends Listenable { * Starts new olm sessions with every other participant that has the participantId "smaller" the localParticipantId. */ async initSessions() { - if (this._init) { - throw new Error("initSessions called multiple times"); - } else { - this._init = await this.enableOLM(); - if (!this._init) throw new Error("initSessions couldn't init olm"); - - const promises = []; - const localParticipantId = this._conf.myUserId(); - - for (const participant of this._conf.getParticipants()) { - if ( - participant.hasFeature(FEATURE_E2EE) && - localParticipantId < participant.getId() - ) { - promises.push(this._sendSessionInit(participant)); - } - } + logger.debug("initSessions called"); - await Promise.allSettled(promises); + if (this._sessionInitializationInProgress) { + return this._sessionInitializationInProgress; } + + this._sessionInitializationInProgress = (async () => { + try { + // Wait for Olm library to initialize + await this._init; + + const localParticipantId = this._conf.myUserId(); + const participants = this._conf.getParticipants(); + + const promises = participants + .filter( + (participant) => + participant.hasFeature(FEATURE_E2EE) && + localParticipantId < participant.getId() + ) + .map((participant) => + this._sendSessionInit(participant).catch((error) => { + logger.warn( + `Failed to initialize session with ${participant.getId()}:`, + error + ); + }) + ); + + await Promise.all(promises); + } finally { + // Clean the session initialization state when promise solved or rejected + this._sessionInitializationInProgress = null; + } + })(); + + return this._sessionInitializationInProgress; } /** @@ -248,8 +294,7 @@ export class OlmAdapter extends Listenable { * @retrns {Promise} */ async updateKey(key: Uint8Array, pqkey: Uint8Array): Promise { - console.log('Key update us called'); - await this.updateCurrentMediaKey(key, pqkey); + this.updateCurrentMediaKey(key, pqkey); this._mediaKeyIndex++; await this.sendKeyInfoToAll(); @@ -264,7 +309,7 @@ export class OlmAdapter extends Listenable { * @param {Uint8Array} key2 - The second key. * @returns {Uint8Array} */ - async deriveKey(key1: Uint8Array, key2: Uint8Array): Promise { + deriveKey(key1: Uint8Array, key2: Uint8Array): Uint8Array { if (key1 === undefined || key1.length === 0) { throw new Error("deriveKey: olm key is undefined"); } @@ -287,10 +332,10 @@ export class OlmAdapter extends Listenable { /** * Updates the current participant key. * @param {Uint8Array} olmKey - The new key. - * @param {Uint8Array} pqKey - The new key. + * @param {Uint8Array} pqKey - The new PQ key. * @returns {number} */ - async updateCurrentMediaKey(olmKey, pqKey) { + updateCurrentMediaKey(olmKey: Uint8Array, pqKey: Uint8Array): number { this._mediaKeyOlm = olmKey; this._mediaKeyPQ = pqKey; @@ -302,9 +347,7 @@ export class OlmAdapter extends Listenable { * */ clearParticipantSession(participant: JitsiParticipant) { - console.log('clearParticipantSession for participant', participant); const olmData = this._getParticipantOlmData(participant); - console.log('clearParticipantSession olmData', olmData); if (olmData.session) { olmData.session.free(); @@ -422,31 +465,6 @@ export class OlmAdapter extends Listenable { } } - /** - * Internal helper to bootstrap the olm library. - * - * @returns {Promise} - * @private - */ - async _bootstrapOlm() { - try { - await window.Olm.init(); - - this._olmAccount = new window.Olm.Account(); - this._olmAccount.create(); - - this._idKeys = _safeJsonParse(this._olmAccount.identity_keys()); - - // Should create keys and key on bootstrap. - await this._initializeKemAndKeys(); - this._onIdKeysReady(this._idKeys); - return true; - } catch (e) { - logger.error("Failed to initialize Olm", e); - return false; - } - } - /** * Starts the verification process for the given participant as described here * https://spec.matrix.org/latest/client-server-api/#short-authentication-string-sas-verification @@ -541,7 +559,7 @@ export class OlmAdapter extends Listenable { * @private */ _onParticipantE2EEChannelReady(id) { - logger.info( + logger.debug( `CHECK: E2EE channel with participant ${id} is ready. Ready for KEY_INFO` ); } @@ -582,7 +600,7 @@ export class OlmAdapter extends Listenable { throw new Error("[KEY_ENCRYPTION]: pqSessionKey is undefined"); } if (mediaKey === undefined || mediaKey.length === 0) { - throw new Error("[KEY_ENCRYPTION]:media key is undefined"); + throw new Error("[KEY_ENCRYPTION]: mediaKey is undefined"); } try { @@ -660,6 +678,7 @@ export class OlmAdapter extends Listenable { * @private */ _getParticipantOlmData(participant: JitsiParticipant) { + logger.debug({ test: participant }); participant[kOlmData] = participant[kOlmData] || {}; return participant[kOlmData]; @@ -673,7 +692,7 @@ export class OlmAdapter extends Listenable { async _onConferenceLeft() { await this._init; for (const participant of this._conf.getParticipants()) { - this._onParticipantLeft2(participant); + this._onParticipantLeft(participant); } if (this._olmAccount) { @@ -701,16 +720,23 @@ export class OlmAdapter extends Listenable { return; } - await this._init; + if (!this._init) { + throw new Error("Olm not initialized"); + } const msg = payload.olm; const pId = participant.getId(); const olmData = this._getParticipantOlmData(participant); const peerName = participant.getDisplayName(); + logger.debug(`Message received, ${JSON.stringify(msg)}`); + switch (msg.type) { case OLM_MESSAGE_TYPES.SESSION_INIT: { - logger.info('CHECK: Got SESSION_INIT from id', pId); + logger.debug( + "CHECK: Got SESSION_INIT from participant", + participant.getDisplayName() + ); if (olmData.session) { logger.error( @@ -739,6 +765,7 @@ export class OlmAdapter extends Listenable { const participantEncapsulationKey: Uint8Array = base64js.toByteArray(msg.data.publicKey); + // Encapsulate public key that is going to be used by the target participant to decapsulate sharedSecret const { ciphertext, sharedSecret } = await this._encapsulateKey( participantEncapsulationKey @@ -753,11 +780,12 @@ export class OlmAdapter extends Listenable { `SESSION_INIT failed for ${peerName}` ); } + const publicKeyString = base64js.fromByteArray( this._publicKey ); - // Send ACK + // Send PQ_SESSION_INIT const ack = { [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE, olm: { @@ -774,8 +802,9 @@ export class OlmAdapter extends Listenable { } break; } + case OLM_MESSAGE_TYPES.PQ_SESSION_INIT: { - logger.info('CHECK: Got PQ_SESSION_INIT from id', pId); + logger.debug("CHECK: Got PQ_SESSION_INIT from id", pId); if (olmData.pqSessionKey) { logger.error( @@ -795,7 +824,7 @@ export class OlmAdapter extends Listenable { msg.data.pqCiphertext ); - const { sharedSecret: decapsilatedSecret } = + const { sharedSecret: decapsulatedSecret } = await this._decapsulateKey( pqCiphertext, this._privateKey @@ -807,8 +836,8 @@ export class OlmAdapter extends Listenable { const { ciphertext, sharedSecret } = await this._encapsulateKey(participantPublicKey64); - olmData.pqSessionKey = await this.deriveKey( - decapsilatedSecret, + olmData.pqSessionKey = this.deriveKey( + decapsulatedSecret, sharedSecret ); @@ -838,7 +867,7 @@ export class OlmAdapter extends Listenable { break; } case OLM_MESSAGE_TYPES.PQ_SESSION_ACK: { - logger.info('CHECK: Got PQ_SESSION_ACK from id', pId); + logger.debug("CHECK: Got PQ_SESSION_ACK from id", pId); if (olmData.pqSessionKey) { logger.error( @@ -855,15 +884,15 @@ export class OlmAdapter extends Listenable { msg.data.pqCiphertext ); - const { sharedSecret: decapsilatedSecret } = + const { sharedSecret: decapsulatedSecret } = await this._decapsulateKey( pqCiphertext, this._privateKey ); - olmData.pqSessionKey = await this.deriveKey( + olmData.pqSessionKey = this.deriveKey( olmData._kemSecret, - decapsilatedSecret + decapsulatedSecret ); } catch (error) { logger.error(`PQ_SESSION_ACK failed for ${peerName}`); @@ -894,7 +923,7 @@ export class OlmAdapter extends Listenable { break; } case OLM_MESSAGE_TYPES.SESSION_ACK: { - logger.info('CHECK: Got SESSION_ACK from id', pId); + logger.debug("CHECK: Got SESSION_ACK from id", pId); if (olmData.session) { logger.warn( @@ -907,7 +936,7 @@ export class OlmAdapter extends Listenable { ); } else if (msg.data.uuid === olmData.pendingSessionUuid) { const { ciphertext } = msg.data; - const d = this._reqs.get(msg.data.uuid); + const requestPromise = this._reqs.get(msg.data.uuid); const session = new window.Olm.Session(); session.create_inbound(this._olmAccount, ciphertext.body); @@ -919,8 +948,10 @@ export class OlmAdapter extends Listenable { this._onParticipantE2EEChannelReady(peerName); + requestPromise.resolve(); + this._reqs.delete(msg.data.uuid); - d.resolve(); + logger.debug(`CHECK: RESOLVE SESSION ACK ${msg.data.uuid}`); } else { logger.error(`SESSION_ACK wrong UUID for ${peerName}`); @@ -937,7 +968,7 @@ export class OlmAdapter extends Listenable { break; } case OLM_MESSAGE_TYPES.KEY_INFO: { - console.log('CHECK: Got KEY_INFO from id', pId); + logger.debug("CHECK: Got KEY_INFO from id", pId); if (olmData.session && olmData.pqSessionKey) { const { ciphertext, pqCiphertext, iv } = msg.data; @@ -951,7 +982,7 @@ export class OlmAdapter extends Listenable { iv, olmData.pqSessionKey ); - + if ( json.encryptionKey !== undefined && pqKey !== undefined && @@ -963,10 +994,17 @@ export class OlmAdapter extends Listenable { const keyIndex = json.index; if (!isEqual(olmData.lastKey, key)) { + logger.debug( + "CHECK: KEY_INFO will setKeys for id", + pId, + "olm key", + key, + "pq key", + pqKey, + "and index", + keyIndex + ); - console.log('CHECK: KEY_INFO will setKeys for id', pId, 'olm key', key, - 'pq key', pqKey, 'and index', keyIndex); - olmData.lastKey = key; this.eventEmitter.emit( OlmAdapterEvents.PARTICIPANT_KEY_UPDATED, @@ -1011,7 +1049,7 @@ export class OlmAdapter extends Listenable { break; } case OLM_MESSAGE_TYPES.KEY_INFO_ACK: { - logger.info('CHECK: Got KEY_INFO_ACK from id', pId); + logger.debug("CHECK: Got KEY_INFO_ACK from id", pId); if (olmData.session && olmData.pqSessionKey) { const { ciphertext, pqCiphertext, iv } = msg.data; @@ -1040,7 +1078,6 @@ export class OlmAdapter extends Listenable { if (!isEqual(olmData.lastKey, key)) { olmData.lastKey = key; - this.eventEmitter.emit( OlmAdapterEvents.PARTICIPANT_KEY_UPDATED, pId, @@ -1050,11 +1087,13 @@ export class OlmAdapter extends Listenable { ); } } - const d = this._reqs.get(msg.data.uuid); + const sessionPromise = this._reqs.get(msg.data.uuid); + + sessionPromise.resolve(); this._reqs.delete(msg.data.uuid); - d.resolve(); + logger.debug("CHECK: RESOLVE KEY_INFO_ACK"); } else { logger.error( `Received KEY_INFO_ACK from ${peerName} but we have no session for them!` @@ -1392,8 +1431,8 @@ export class OlmAdapter extends Listenable { * * @private */ - _onParticipantLeft2(participant: JitsiParticipant) { - console.log('_onParticipantLeft2 for participant', participant); + _onParticipantLeft(participant: JitsiParticipant) { + logger.debug("_onParticipantLeft for participant", participant); this.clearParticipantSession(participant); } @@ -1420,6 +1459,10 @@ export class OlmAdapter extends Listenable { "_onParticipantPropertyChanged is called before init" ); } + + logger.debug( + "send key info from _onParticipantPropertyChanged" + ); await this.sendKeyInfoToAll(); } break; @@ -1466,6 +1509,9 @@ export class OlmAdapter extends Listenable { * @param {string} participantId - ID of the target participant. */ _sendMessage(data, participantId) { + logger.debug( + `_sendMessage called ${JSON.stringify({ data, participantId })}` + ); this._conf.sendMessage(data, participantId); } @@ -1476,7 +1522,7 @@ export class OlmAdapter extends Listenable { * @returns {Promise} - The promise will be resolved when the session-ack is received. * @private */ - _sendSessionInit(participant: JitsiParticipant) { + async _sendSessionInit(participant: JitsiParticipant) { const pId = participant.getId(); const olmData = this._getParticipantOlmData(participant); @@ -1484,14 +1530,18 @@ export class OlmAdapter extends Listenable { logger.warn(`Tried to send session-init to ${participant.getDisplayName()} but we already have a session`); - return Promise.reject(); + throw new Error( + `Already have a session with participant ${participant.getDisplayName()} ` + ); } if (olmData.pendingSessionUuid !== undefined) { logger.warn(`Tried to send session-init to ${participant.getDisplayName()} but we already have a pending session`); - return Promise.reject(); + throw new Error( + `Already have a pending session with participant ${participant.getDisplayName()}` + ); } try { @@ -1513,6 +1563,7 @@ export class OlmAdapter extends Listenable { const publicKeyString = base64js.fromByteArray(this._publicKey); const uuid = uuidv4(); + const init = { [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE, olm: { @@ -1526,21 +1577,28 @@ export class OlmAdapter extends Listenable { }, }; - const d = new Deferred(); - - d.setRejectTimeout(REQ_TIMEOUT); - d.catch(() => { - this._reqs.delete(uuid); - olmData.pendingSessionUuid = undefined; + const sessionPromise = new Promise((resolve, reject) => { + this._reqs.set(uuid, { resolve, reject }); }); - this._reqs.set(uuid, d); - this._sendMessage(init, pId); + const timeoutPromise = new Promise((_, reject) => + setTimeout( + () => reject(new Error("Session init request timed out")), + REQ_TIMEOUT + ) + ); - // Store the UUID for matching with the ACK. + this._sendMessage(init, pId); olmData.pendingSessionUuid = uuid; - return d; + // Simulates timeout with deferred object but using promises + return Promise.race([sessionPromise, timeoutPromise]).catch( + (error) => { + this._reqs.delete(uuid); + olmData.pendingSessionUuid = undefined; + throw error; + } + ); } catch (e) { logger.error( `_sendSessionInit failed for ${participant.getDisplayName()} with ${e}` diff --git a/modules/xmpp/strophe.util.ts b/modules/xmpp/strophe.util.ts index 6658e9dd6e..07310288d1 100644 --- a/modules/xmpp/strophe.util.ts +++ b/modules/xmpp/strophe.util.ts @@ -51,6 +51,7 @@ export default function() { // Strophe log entry about secondary request timeout does not mean that // it's a final failure(the request will be restarted), so we lower it's // level here to a warning. + // This line can be commented out if you are having perfomance problems while debugging on Chrome. logger.trace('Strophe', level, msg); if (typeof msg === 'string' && msg.indexOf('Request ') !== -1