From 7cafa205ee8d7a68bc8b3bdff5ec282977792df0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Thu, 7 May 2020 11:54:02 +0200 Subject: [PATCH] e2ee: stage 2 Adapt to E2EE changes in lib-jitsi-meet. Notably: --- 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. --- --- Makefile | 10 +- app.js | 7 + conference.js | 31 ---- css/_e2ee.scss | 27 +--- lang/main.json | 6 +- modules/API/API.js | 8 +- package-lock.json | 21 ++- package.json | 3 +- react/features/e2ee/actionTypes.js | 6 +- react/features/e2ee/actions.js | 12 +- react/features/e2ee/components/E2EESection.js | 142 ++++++------------ react/features/e2ee/middleware.js | 14 +- react/features/e2ee/reducer.js | 12 +- webpack.config.js | 7 +- 14 files changed, 118 insertions(+), 188 deletions(-) diff --git a/Makefile b/Makefile index c70d3527d..5f889c3db 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ CLEANCSS = ./node_modules/.bin/cleancss DEPLOY_DIR = libs LIBJITSIMEET_DIR = node_modules/lib-jitsi-meet/ LIBFLAC_DIR = node_modules/libflacjs/dist/min/ +OLM_DIR = node_modules/olm RNNOISE_WASM_DIR = node_modules/rnnoise-wasm/dist/ NODE_SASS = ./node_modules/.bin/node-sass NPM = npm @@ -22,7 +23,7 @@ clean: rm -fr $(BUILD_DIR) .NOTPARALLEL: -deploy: deploy-init deploy-appbundle deploy-rnnoise-binary deploy-lib-jitsi-meet deploy-libflac deploy-css deploy-local +deploy: deploy-init deploy-appbundle deploy-rnnoise-binary deploy-lib-jitsi-meet deploy-libflac deploy-olm deploy-css deploy-local deploy-init: rm -fr $(DEPLOY_DIR) @@ -70,6 +71,11 @@ deploy-libflac: $(LIBFLAC_DIR)/libflac4-1.3.2.min.js.mem \ $(DEPLOY_DIR) +deploy-olm: + cp \ + $(OLM_DIR)/olm.wasm \ + $(DEPLOY_DIR) + deploy-rnnoise-binary: cp \ $(RNNOISE_WASM_DIR)/rnnoise.wasm \ @@ -84,7 +90,7 @@ deploy-local: ([ ! -x deploy-local.sh ] || ./deploy-local.sh) .NOTPARALLEL: -dev: deploy-init deploy-css deploy-rnnoise-binary deploy-lib-jitsi-meet deploy-libflac +dev: deploy-init deploy-css deploy-rnnoise-binary deploy-lib-jitsi-meet deploy-libflac deploy-olm $(WEBPACK_DEV_SERVER) --detect-circular-deps source-package: diff --git a/app.js b/app.js index 7fcba1ed8..24eec6c2b 100644 --- a/app.js +++ b/app.js @@ -4,6 +4,8 @@ import 'jquery'; import 'jquery-contextmenu'; import 'jQuery-Impromptu'; +import 'olm'; + import conference from './conference'; import API from './modules/API'; import UI from './modules/UI/UI'; @@ -11,6 +13,11 @@ import keyboardshortcut from './modules/keyboardshortcut/keyboardshortcut'; import remoteControl from './modules/remotecontrol/RemoteControl'; import translation from './modules/translation/translation'; +// Initialize Olm as early as possible. +if (window.Olm) { + window.Olm.init(); +} + window.APP = { API, conference, diff --git a/conference.js b/conference.js index 52417daa7..804303aa7 100644 --- a/conference.js +++ b/conference.js @@ -110,7 +110,6 @@ import { } from './react/features/base/util'; import { showDesktopPicker } from './react/features/desktop-picker'; import { appendSuffix } from './react/features/display-name'; -import { setE2EEKey } from './react/features/e2ee'; import { maybeOpenFeedbackDialog, submitFeedback @@ -746,8 +745,6 @@ export default { this.roomName = roomName; - window.addEventListener('hashchange', this.onHashChange.bind(this), false); - try { // Initialize the device list first. This way, when creating tracks // based on preferred devices, loose label matching can be done in @@ -1239,34 +1236,6 @@ export default { })); }, - /** - * Handled location hash change events. - */ - onHashChange() { - const items = {}; - const parts = window.location.hash.substr(1).split('&'); - - for (const part of parts) { - const param = part.split('='); - const key = param[0]; - - if (!key) { - continue; // eslint-disable-line no-continue - } - - items[key] = param[1]; - } - - if (typeof items.e2eekey !== 'undefined') { - APP.store.dispatch(setE2EEKey(items.e2eekey)); - - // Clean URL in browser history. - const cleanUrl = window.location.href.split('#')[0]; - - history.replaceState(history.state, document.title, cleanUrl); - } - }, - /** * Exposes a Command(s) API on this instance. It is necessitated by (1) the * desire to keep room private to this instance and (2) the need of other diff --git a/css/_e2ee.scss b/css/_e2ee.scss index 9a3a9650a..eadd577d8 100644 --- a/css/_e2ee.scss +++ b/css/_e2ee.scss @@ -1,7 +1,6 @@ #e2ee-section { - .title { - font-weight: 700; - } + display: flex; + flex-direction: column; .description { font-size: 13px; @@ -13,29 +12,15 @@ } } - .key-field { - align-items: center; + .control-row { display: flex; flex-direction: row; + justify-content: space-between; + margin-top: 15px; label { font-size: 14px; - font-weight: 700; - } - - input { - background-color: inherit; - border: none; - color: inherit; - flex: 1; - padding: 0 5px; - } - - a { - color: #6FB1EA; - cursor: pointer; - font-size: 14px; - text-decoration: none; + font-weight: bold; } } } \ No newline at end of file diff --git a/lang/main.json b/lang/main.json index c41550062..6f024e2b5 100644 --- a/lang/main.json +++ b/lang/main.json @@ -197,10 +197,7 @@ "displayNameRequired": "Hi! What’s your name?", "done": "Done", "e2eeDescription": "End-to-End Encryption is currently EXPERIMENTAL. Please keep in mind that turning on end-to-end encryption will effectively disable server-side provided services such as: recording, live streaming and phone participation. Also keep in mind that the meeting will only work for people joining from browsers with support for insertable streams.", - "e2eeLabel": "E2EE key", - "e2eeNoKey": "None", - "e2eeToggleSet": "Set key", - "e2eeSet": "Set", + "e2eeLabel": "Enable End-to-End Encryption", "e2eeWarning": "WARNING: Not all participants in this meeting seem to have support for End-to-End encryption. If you enable it they won't be able to see nor hear you.", "enterDisplayName": "Please enter your name here", "error": "Error", @@ -697,7 +694,6 @@ "document": "Toggle shared document", "download": "Download our apps", "embedMeeting": "Embed meeting", - "e2ee": "End-to-End Encryption", "feedback": "Leave feedback", "fullScreen": "Toggle full screen", "grantModerator": "Grant Moderator", diff --git a/modules/API/API.js b/modules/API/API.js index 272ff0f63..22e8b276c 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -19,7 +19,7 @@ import { processExternalDeviceRequest } from '../../react/features/device-selection/functions'; import { isEnabled as isDropboxEnabled } from '../../react/features/dropbox'; -import { setE2EEKey } from '../../react/features/e2ee'; +import { toggleE2EE } from '../../react/features/e2ee/actions'; import { invite } from '../../react/features/invite'; import { toggleLobbyMode } from '../../react/features/lobby/actions.web'; import { RECORDING_TYPES } from '../../react/features/recording/constants'; @@ -191,9 +191,9 @@ function initCommands() { logger.error('Failed sending endpoint text message', err); } }, - 'e2ee-key': key => { - logger.debug('Set E2EE key command received'); - APP.store.dispatch(setE2EEKey(key)); + 'toggle-e2ee': enabled => { + logger.debug('Toggle E2EE key command received'); + APP.store.dispatch(toggleE2EE(enabled)); }, 'set-video-quality': frameHeight => { logger.debug('Set video quality command received'); diff --git a/package-lock.json b/package-lock.json index fe42e096f..771ab9470 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10952,21 +10952,24 @@ } }, "lib-jitsi-meet": { - "version": "github:jitsi/lib-jitsi-meet#94318fce12c855aefefdf8586bc8772065b505c9", - "from": "github:jitsi/lib-jitsi-meet#94318fce12c855aefefdf8586bc8772065b505c9", + "version": "github:jitsi/lib-jitsi-meet#735c30ec4f1c17b81f68c4b6684489e05115aeb2", + "from": "github:jitsi/lib-jitsi-meet#735c30ec4f1c17b81f68c4b6684489e05115aeb2", "requires": { "@jitsi/js-utils": "1.0.0", "@jitsi/sdp-interop": "1.0.3", "@jitsi/sdp-simulcast": "0.3.0", "async": "0.9.0", + "base64-js": "1.3.1", "current-executing-script": "0.1.3", "jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#5ec92357570dc8f0b7ffc1528820721c84c6af8b", "lodash.clonedeep": "4.5.0", + "lodash.debounce": "4.0.8", "lodash.isequal": "4.5.0", "sdp-transform": "2.3.0", "strophe.js": "1.3.4", "strophejs-plugin-disco": "0.0.2", "strophejs-plugin-stream-management": "github:jitsi/strophejs-plugin-stream-management#001cf02bef2357234e1ac5d163611b4d60bf2b6a", + "uuid": "8.1.0", "webrtc-adapter": "7.5.0" }, "dependencies": { @@ -10984,6 +10987,11 @@ "version": "0.7.3", "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz", "integrity": "sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ==" + }, + "uuid": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz", + "integrity": "sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==" } } }, @@ -11046,6 +11054,11 @@ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" + }, "lodash.flatten": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", @@ -12940,6 +12953,10 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, + "olm": { + "version": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz", + "integrity": "sha512-kumW7B+xWMdiGSU0BrECOd+9GnhvsnnHP6qTHGPIcHTL2F0m8sYlP08hkEpN7uX/TlnHCwqpkaZXPQ0GYtVe8A==" + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", diff --git a/package.json b/package.json index 4d1056152..a65dc8264 100644 --- a/package.json +++ b/package.json @@ -56,11 +56,12 @@ "jquery-i18next": "1.2.1", "js-md5": "0.6.1", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#94318fce12c855aefefdf8586bc8772065b505c9", + "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#735c30ec4f1c17b81f68c4b6684489e05115aeb2", "libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d", "lodash": "4.17.19", "moment": "2.19.4", "moment-duration-format": "2.2.2", + "olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz", "pixelmatch": "5.1.0", "react": "16.9", "react-dom": "16.9", diff --git a/react/features/e2ee/actionTypes.js b/react/features/e2ee/actionTypes.js index 2cce6661d..25051106a 100644 --- a/react/features/e2ee/actionTypes.js +++ b/react/features/e2ee/actionTypes.js @@ -1,8 +1,8 @@ /** - * The type of the action which signals the E2EE key has changed. + * The type of the action which signals that E2EE needs to be enabled / disabled. * * { - * type: SET_E2EE_KEY + * type: TOGGLE_E2EE * } */ -export const SET_E2EE_KEY = 'SET_E2EE_KEY'; +export const TOGGLE_E2EE = 'TOGGLE_E2EE'; diff --git a/react/features/e2ee/actions.js b/react/features/e2ee/actions.js index 2cfa96b97..4beb1629f 100644 --- a/react/features/e2ee/actions.js +++ b/react/features/e2ee/actions.js @@ -1,16 +1,16 @@ // @flow -import { SET_E2EE_KEY } from './actionTypes'; +import { TOGGLE_E2EE } from './actionTypes'; /** - * Dispatches an action to set the E2EE key. + * Dispatches an action to enable / disable E2EE. * - * @param {string|undefined} key - The new key to be used for E2EE. + * @param {boolean} enabled - Whether E2EE is to be enabled or not. * @returns {Object} */ -export function setE2EEKey(key: ?string) { +export function toggleE2EE(enabled: boolean) { return { - type: SET_E2EE_KEY, - key + type: TOGGLE_E2EE, + enabled }; } diff --git a/react/features/e2ee/components/E2EESection.js b/react/features/e2ee/components/E2EESection.js index c38d4f778..f7edaf3ce 100644 --- a/react/features/e2ee/components/E2EESection.js +++ b/react/features/e2ee/components/E2EESection.js @@ -6,22 +6,23 @@ import type { Dispatch } from 'redux'; import { createE2EEEvent, sendAnalytics } from '../../analytics'; import { translate } from '../../base/i18n'; import { getParticipants } from '../../base/participants'; +import { Switch } from '../../base/react'; import { connect } from '../../base/redux'; -import { setE2EEKey } from '../actions'; +import { toggleE2EE } from '../actions'; type Props = { + /** + * Whether E2EE is currently enabled or not. + */ + _enabled: boolean, + /** * Indicates whether all participants in the conference currently support E2EE. */ _everyoneSupportsE2EE: boolean, - /** - * The current E2EE key. - */ - _key: string, - /** * The redux {@code dispatch} function. */ @@ -36,19 +37,14 @@ type Props = { type State = { /** - * True if the key is being edited. + * True if the switch is toggled on. */ - editing: boolean, + enabled: boolean, /** * True if the section description should be expanded, false otherwise. */ - expand: boolean, - - /** - * The current E2EE key. - */ - key: string + expand: boolean }; /** @@ -58,30 +54,38 @@ type State = { * @extends Component */ class E2EESection extends Component { - fieldRef: Object; + /** + * Implements React's {@link Component#getDerivedStateFromProps()}. + * + * @inheritdoc + */ + static getDerivedStateFromProps(props: Props, state: Object) { + if (props._enabled !== state.enabled) { + + return { + enabled: props._enabled + }; + } + + return null; + } /** - * Initializes a new {@code E2EEDialog } instance. + * Instantiates a new component. * - * @param {Object} props - The read-only properties with which the new - * instance is to be initialized. + * @inheritdoc */ constructor(props: Props) { super(props); - this.fieldRef = React.createRef(); - this.state = { - editing: false, - expand: false, - key: this.props._key + enabled: false, + expand: false }; // Bind event handlers so they are only bound once for every instance. this._onExpand = this._onExpand.bind(this); - this._onKeyChange = this._onKeyChange.bind(this); - this._onSet = this._onSet.bind(this); - this._onToggleSetKey = this._onToggleSetKey.bind(this); + this._onToggle = this._onToggle.bind(this); } /** @@ -92,7 +96,7 @@ class E2EESection extends Component { */ render() { const { _everyoneSupportsE2EE, t } = this.props; - const { editing, expand } = this.state; + const { enabled, expand } = this.state; const description = t('dialog.e2eeDescription'); return ( @@ -112,25 +116,13 @@ class E2EESection extends Component { { t('dialog.e2eeWarning') } } -
+
- - { editing && - { t('dialog.e2eeSet') } - } - { !editing && - { t('dialog.e2eeToggleSet') } - } +
); @@ -149,65 +141,23 @@ class E2EESection extends Component { }); } - _onKeyChange: (Object) => void; + _onToggle: () => void; /** - * Updates the entered key. - * - * @param {Object} event - The DOM event triggered from the entered value having changed. - * @private - * @returns {void} - */ - _onKeyChange(event) { - this.setState({ key: event.target.value.trim() }); - } - - _onKeyDown: (Object) => void; - - /** - * Handler for the keydown event on the form, preventing the closing of the dialog. - * - * @param {Object} event - The DOM event triggered by keydown events. - * @returns {void} - */ - _onKeyDown(event) { - if (event.key === 'Enter') { - event.preventDefault(); - } - } - - _onSet: () => void; - - /** - * Dispatches an action to set/unset the E2EE key. + * Callback to be invoked when the user toggles E2EE on or off. * * @private * @returns {void} */ - _onSet() { - const { key } = this.state; - - sendAnalytics(createE2EEEvent(`key.${key ? 'set' : 'unset'}`)); - this.props.dispatch(setE2EEKey(key)); + _onToggle() { + const newValue = !this.state.enabled; this.setState({ - editing: false + enabled: newValue }); - } - _onToggleSetKey: () => void; - - /** - * Sets the section into edit mode so then the user can set the key. - * - * @returns {void} - */ - _onToggleSetKey() { - this.setState({ - editing: true - }, () => { - this.fieldRef.current.focus(); - }); + sendAnalytics(createE2EEEvent(`enabled.${String(newValue)}`)); + this.props.dispatch(toggleE2EE(newValue)); } } @@ -219,12 +169,12 @@ class E2EESection extends Component { * @returns {Props} */ function mapStateToProps(state) { - const { e2eeKey } = state['features/e2ee']; + const { enabled } = state['features/e2ee']; const participants = getParticipants(state).filter(p => !p.local); return { - _everyoneSupportsE2EE: participants.every(p => Boolean(p.e2eeSupported)), - _key: e2eeKey || '' + _enabled: enabled, + _everyoneSupportsE2EE: participants.every(p => Boolean(p.e2eeSupported)) }; } diff --git a/react/features/e2ee/middleware.js b/react/features/e2ee/middleware.js index 1d1f9d4b1..0510ca369 100644 --- a/react/features/e2ee/middleware.js +++ b/react/features/e2ee/middleware.js @@ -4,8 +4,8 @@ import { getCurrentConference } from '../base/conference'; import { getLocalParticipant, participantUpdated } from '../base/participants'; import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux'; -import { SET_E2EE_KEY } from './actionTypes'; -import { setE2EEKey } from './actions'; +import { TOGGLE_E2EE } from './actionTypes'; +import { toggleE2EE } from './actions'; import logger from './logger'; /** @@ -16,18 +16,18 @@ import logger from './logger'; */ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { switch (action.type) { - case SET_E2EE_KEY: { + case TOGGLE_E2EE: { const conference = getCurrentConference(getState); if (conference) { - logger.debug(`New E2EE key: ${action.key}`); - conference.setE2EEKey(action.key); + logger.debug(`E2EE will be ${action.enabled ? 'enabled' : 'disabled'}`); + conference.toggleE2EE(action.enabled); // Broadccast that we enabled / disabled E2EE. const participant = getLocalParticipant(getState); dispatch(participantUpdated({ - e2eeEnabled: Boolean(action.key), + e2eeEnabled: action.enabled, id: participant.id, local: true })); @@ -48,6 +48,6 @@ StateListenerRegistry.register( state => getCurrentConference(state), (conference, { dispatch }, previousConference) => { if (previousConference) { - dispatch(setE2EEKey(undefined)); + dispatch(toggleE2EE(false)); } }); diff --git a/react/features/e2ee/reducer.js b/react/features/e2ee/reducer.js index 2750abbfb..cba87327b 100644 --- a/react/features/e2ee/reducer.js +++ b/react/features/e2ee/reducer.js @@ -2,14 +2,10 @@ import { ReducerRegistry } from '../base/redux'; -import { SET_E2EE_KEY } from './actionTypes'; +import { TOGGLE_E2EE } from './actionTypes'; const DEFAULT_STATE = { - - /** - * E2EE key. - */ - e2eeKey: undefined + enabled: false }; /** @@ -17,10 +13,10 @@ const DEFAULT_STATE = { */ ReducerRegistry.register('features/e2ee', (state = DEFAULT_STATE, action) => { switch (action.type) { - case SET_E2EE_KEY: + case TOGGLE_E2EE: return { ...state, - e2eeKey: action.key + enabled: action.enabled }; default: diff --git a/webpack.config.js b/webpack.config.js index 0453130e4..6f358f9c0 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -140,7 +140,10 @@ const config = { // Allow the use of the real filename of the module being executed. By // default Webpack does not leak path-related information and provides a // value that is a mock (/index.js). - __filename: true + __filename: true, + + // Provide an empty 'fs' module. + fs: 'empty' }, optimization: { concatenateModules: minimize, @@ -187,7 +190,7 @@ module.exports = [ entry: { 'app.bundle': './app.js' }, - performance: getPerformanceHints(4 * 1024 * 1024) + performance: getPerformanceHints(4.5 * 1024 * 1024) }), Object.assign({}, config, { entry: {