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.
---
This commit is contained in:
Saúl Ibarra Corretgé 2020-05-07 11:54:02 +02:00 committed by Saúl Ibarra Corretgé
parent 2b4f33bef8
commit 7cafa205ee
14 changed files with 118 additions and 188 deletions

View File

@ -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:

7
app.js
View File

@ -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,

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -197,10 +197,7 @@
"displayNameRequired": "Hi! Whats 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",

View File

@ -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');

21
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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';

View File

@ -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
};
}

View File

@ -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<Props, State> {
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<Props, State> {
*/
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<Props, State> {
{ t('dialog.e2eeWarning') }
</span>
}
<div className = 'key-field'>
<div className = 'control-row'>
<label>
{ t('dialog.e2eeLabel') }:
{ t('dialog.e2eeLabel') }
</label>
<input
disabled = { !editing }
name = 'e2eeKey'
onChange = { this._onKeyChange }
onKeyDown = { this._onKeyDown }
placeholder = { t('dialog.e2eeNoKey') }
ref = { this.fieldRef }
type = 'password'
value = { this.state.key } />
{ editing && <a onClick = { this._onSet }>
{ t('dialog.e2eeSet') }
</a> }
{ !editing && <a onClick = { this._onToggleSetKey }>
{ t('dialog.e2eeToggleSet') }
</a> }
<Switch
onValueChange = { this._onToggle }
value = { enabled } />
</div>
</div>
);
@ -149,65 +141,23 @@ class E2EESection extends Component<Props, State> {
});
}
_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<Props, State> {
* @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))
};
}

View File

@ -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));
}
});

View File

@ -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:

View File

@ -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: {