From cb6fbb0f039a690f7a1a9cafd50261394a9293c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Thu, 16 Apr 2020 13:26:44 +0200 Subject: [PATCH] e2ee: add UI elements * Add dialog to set the E2EE key * Use the Redux action / middleware to update the key even when set through the hash parameter * Cleanup URL after processing the key so it's not recorded in browser history --- conference.js | 15 +- interface_config.js | 3 +- lang/main.json | 5 + react/features/analytics/AnalyticsEvents.js | 14 ++ react/features/base/conference/reducer.js | 5 + react/features/e2ee/actionTypes.js | 8 + react/features/e2ee/actions.js | 16 ++ react/features/e2ee/components/E2EEButton.js | 76 ++++++++++ react/features/e2ee/components/E2EEDialog.js | 142 ++++++++++++++++++ react/features/e2ee/components/index.js | 2 + react/features/e2ee/index.js | 6 + react/features/e2ee/logger.js | 5 + react/features/e2ee/middleware.js | 43 ++++++ react/features/e2ee/reducer.js | 29 ++++ .../toolbox/components/web/Toolbox.js | 5 + 15 files changed, 365 insertions(+), 9 deletions(-) create mode 100644 react/features/e2ee/actionTypes.js create mode 100644 react/features/e2ee/actions.js create mode 100644 react/features/e2ee/components/E2EEButton.js create mode 100644 react/features/e2ee/components/E2EEDialog.js create mode 100644 react/features/e2ee/components/index.js create mode 100644 react/features/e2ee/index.js create mode 100644 react/features/e2ee/logger.js create mode 100644 react/features/e2ee/middleware.js create mode 100644 react/features/e2ee/reducer.js diff --git a/conference.js b/conference.js index f453fb2338..351fd99e00 100644 --- a/conference.js +++ b/conference.js @@ -113,6 +113,7 @@ import { import { getJitsiMeetGlobalNS } 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 @@ -470,11 +471,6 @@ export default { */ localVideo: null, - /** - * The key used for End-To-End Encryption. - */ - e2eeKey: undefined, - /** * Creates local media tracks and connects to a room. Will show error * dialogs in case accessing the local microphone and/or camera failed. Will @@ -1202,11 +1198,14 @@ export default { items[key] = param[1]; } - this.e2eeKey = items.e2eekey; + if (typeof items.e2eekey !== undefined) { + APP.store.dispatch(setE2EEKey(items.e2eekey)); - logger.debug(`New E2EE key: ${this.e2eeKey}`); + // Clean URL in browser history. + const cleanUrl = window.location.href.split('#')[0]; - this._room.setE2EEKey(this.e2eeKey); + history.replaceState(history.state, document.title, cleanUrl); + } }, /** diff --git a/interface_config.js b/interface_config.js index fa3e1fef1d..ba70079582 100644 --- a/interface_config.js +++ b/interface_config.js @@ -51,7 +51,8 @@ var interfaceConfig = { 'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording', 'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand', 'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts', - 'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone' + 'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone', + 'e2ee' ], SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ], diff --git a/lang/main.json b/lang/main.json index 3caa544ea2..9627930ff7 100644 --- a/lang/main.json +++ b/lang/main.json @@ -175,6 +175,9 @@ "dismiss": "Dismiss", "displayNameRequired": "Hi! What’s your name?", "done": "Done", + "e2eeDescription": "

End-to-End Encryption is currently EXPERIMENTAL. Please see this post for details.


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": "Key", + "e2eeTitle": "End-to-End Encryption", "enterDisplayName": "Please enter your name here", "error": "Error", "externalInstallationMsg": "You need to install our desktop sharing extension.", @@ -595,6 +598,7 @@ "chat": "Toggle chat window", "document": "Toggle shared document", "download": "Download our apps", + "e2ee": "End-to-End Encryption", "feedback": "Leave feedback", "fullScreen": "Toggle full screen", "hangup": "Leave the call", @@ -638,6 +642,7 @@ "documentClose": "Close shared document", "documentOpen": "Open shared document", "download": "Download our apps", + "e2ee": "End-to-End Encryption", "enterFullScreen": "View full screen", "enterTileView": "Enter tile view", "exitFullScreen": "Exit full screen", diff --git a/react/features/analytics/AnalyticsEvents.js b/react/features/analytics/AnalyticsEvents.js index 515226a3a3..c0db4f5857 100644 --- a/react/features/analytics/AnalyticsEvents.js +++ b/react/features/analytics/AnalyticsEvents.js @@ -257,6 +257,20 @@ export function createDeviceChangedEvent(mediaType, deviceType) { }; } +/** + * Creates an event indicating that an action related to E2EE occurred. + * + * @param {string} action - The action which occurred. + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. + */ +export function createE2EEEvent(action) { + return { + action, + actionSubject: 'e2ee' + }; +} + /** * Creates an event which specifies that the feedback dialog has been opened. * diff --git a/react/features/base/conference/reducer.js b/react/features/base/conference/reducer.js index d1ab669297..1b528dc715 100644 --- a/react/features/base/conference/reducer.js +++ b/react/features/base/conference/reducer.js @@ -31,6 +31,7 @@ import { isRoomValid } from './functions'; const DEFAULT_STATE = { conference: undefined, + e2eeSupported: undefined, joining: undefined, leaving: undefined, locked: undefined, @@ -175,6 +176,7 @@ function _conferenceFailed(state, { conference, error }) { return assign(state, { authRequired, conference: undefined, + e2eeSupported: undefined, error, joining: undefined, leaving: undefined, @@ -226,6 +228,9 @@ function _conferenceJoined(state, { conference }) { * @type {JitsiConference} */ conference, + + e2eeSupported: conference.isE2EESupported(), + joining: undefined, leaving: undefined, diff --git a/react/features/e2ee/actionTypes.js b/react/features/e2ee/actionTypes.js new file mode 100644 index 0000000000..2cce6661d7 --- /dev/null +++ b/react/features/e2ee/actionTypes.js @@ -0,0 +1,8 @@ +/** + * The type of the action which signals the E2EE key has changed. + * + * { + * type: SET_E2EE_KEY + * } + */ +export const SET_E2EE_KEY = 'SET_E2EE_KEY'; diff --git a/react/features/e2ee/actions.js b/react/features/e2ee/actions.js new file mode 100644 index 0000000000..2cfa96b974 --- /dev/null +++ b/react/features/e2ee/actions.js @@ -0,0 +1,16 @@ +// @flow + +import { SET_E2EE_KEY } from './actionTypes'; + +/** + * Dispatches an action to set the E2EE key. + * + * @param {string|undefined} key - The new key to be used for E2EE. + * @returns {Object} + */ +export function setE2EEKey(key: ?string) { + return { + type: SET_E2EE_KEY, + key + }; +} diff --git a/react/features/e2ee/components/E2EEButton.js b/react/features/e2ee/components/E2EEButton.js new file mode 100644 index 0000000000..f6296e95cd --- /dev/null +++ b/react/features/e2ee/components/E2EEButton.js @@ -0,0 +1,76 @@ +// @flow + +import React from 'react'; + +import { createE2EEEvent, sendAnalytics } from '../../analytics'; +import { openDialog } from '../../base/dialog'; +import { translate } from '../../base/i18n'; +import { IconRoomUnlock } from '../../base/icons'; +import { connect } from '../../base/redux'; +import { AbstractButton, BetaTag } from '../../base/toolbox'; +import type { AbstractButtonProps } from '../../base/toolbox'; + +import E2EEDialog from './E2EEDialog'; + + +type Props = AbstractButtonProps & { + + /** + * The redux {@code dispatch} function. + */ + dispatch: Function + +}; + +/** + * Button that open a dialog to set the E2EE key. + */ +class E2EEButton extends AbstractButton { + accessibilityLabel = 'toolbar.accessibilityLabel.e2ee'; + icon = IconRoomUnlock; + label = 'toolbar.e2ee'; + tooltip = 'toolbar.e2ee'; + + /** + * Helper function to be implemented by subclasses, which returns + * a React Element to display (a beta tag) at the end of the button. + * + * @override + * @protected + * @returns {ReactElement} + */ + _getElementAfter() { + return ; + } + + /** + * Handles clicking / pressing the button, and opens the E2EE dialog. + * + * @protected + * @returns {void} + */ + _handleClick() { + sendAnalytics(createE2EEEvent('dialog.open')); + this.props.dispatch(openDialog(E2EEDialog)); + } +} + +/** + * Maps (parts of) the redux state to the associated props for this component. + * + * @param {Object} state - The Redux state. + * @param {Props} ownProps - The own props of the Component. + * @private + * @returns {Props} + */ +export function mapStateToProps(state: Object, ownProps: Props) { + const { e2eeSupported } = state['features/base/conference']; + const { visible = Boolean(e2eeSupported) } = ownProps; + + return { + visible + }; +} + + +export default translate(connect(mapStateToProps)(E2EEButton)); diff --git a/react/features/e2ee/components/E2EEDialog.js b/react/features/e2ee/components/E2EEDialog.js new file mode 100644 index 0000000000..fba5f850fb --- /dev/null +++ b/react/features/e2ee/components/E2EEDialog.js @@ -0,0 +1,142 @@ +/* @flow */ + +import React, { Component } from 'react'; +import type { Dispatch } from 'redux'; +import { FieldTextStateless as TextField } from '@atlaskit/field-text'; + +import { createE2EEEvent, sendAnalytics } from '../../analytics'; +import { Dialog } from '../../base/dialog'; +import { translate, translateToHTML } from '../../base/i18n'; +import { connect } from '../../base/redux'; + +import { setE2EEKey } from '../actions'; + + +type Props = { + + /** + * The current E2EE key. + */ + _key: string, + + /** + * The redux {@code dispatch} function. + */ + dispatch: Dispatch, + + /** + * Invoked to obtain translated strings. + */ + t: Function +}; + +type State = { + + /** + * The current E2EE key. + */ + key: string +}; + +/** + * Implements a React {@code Component} for displaying a dialog with a field + * for setting the E2EE key. + * + * @extends Component + */ +class E2EEDialog extends Component { + /** + * Initializes a new {@code E2EEDialog } instance. + * + * @param {Object} props - The read-only properties with which the new + * instance is to be initialized. + */ + constructor(props: Props) { + super(props); + + this.state = { + key: this.props._key + }; + + // Bind event handlers so they are only bound once for every instance. + this._onKeyChange = this._onKeyChange.bind(this); + this._onSubmit = this._onSubmit.bind(this); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { t } = this.props; + + return ( + +
+ { translateToHTML(t, 'dialog.e2eeDescription') } +
+ +
); + } + + _onKeyChange: (Object) => 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() }); + } + + _onSubmit: () => boolean; + + /** + * Dispatches an action to update the E2EE key. + * + * @private + * @returns {boolean} + */ + _onSubmit() { + const { key } = this.state; + + sendAnalytics(createE2EEEvent(`key.${key ? 'set' : 'unset'}`)); + this.props.dispatch(setE2EEKey(key)); + + return true; + } +} + +/** + * Maps (parts of) the Redux state to the associated props for this component. + * + * @param {Object} state - The Redux state. + * @private + * @returns {Props} + */ +function mapStateToProps(state) { + const { e2eeKey } = state['features/e2ee']; + + return { + _key: e2eeKey || '' + }; +} + +export default translate(connect(mapStateToProps)(E2EEDialog)); diff --git a/react/features/e2ee/components/index.js b/react/features/e2ee/components/index.js new file mode 100644 index 0000000000..c4fb0277da --- /dev/null +++ b/react/features/e2ee/components/index.js @@ -0,0 +1,2 @@ +export { default as E2EEButton } from './E2EEButton'; +export { default as E2EEDialog } from './E2EEDialog'; diff --git a/react/features/e2ee/index.js b/react/features/e2ee/index.js new file mode 100644 index 0000000000..a29aa08e02 --- /dev/null +++ b/react/features/e2ee/index.js @@ -0,0 +1,6 @@ +export * from './actions'; +export * from './actionTypes'; +export * from './components'; + +import './middleware'; +import './reducer'; diff --git a/react/features/e2ee/logger.js b/react/features/e2ee/logger.js new file mode 100644 index 0000000000..ef2342ebd4 --- /dev/null +++ b/react/features/e2ee/logger.js @@ -0,0 +1,5 @@ +// @flow + +import { getLogger } from '../base/logging/functions'; + +export default getLogger('features/e2ee'); diff --git a/react/features/e2ee/middleware.js b/react/features/e2ee/middleware.js new file mode 100644 index 0000000000..dce19e8ad6 --- /dev/null +++ b/react/features/e2ee/middleware.js @@ -0,0 +1,43 @@ +// @flow + +import { getCurrentConference } from '../base/conference'; +import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux'; + +import { SET_E2EE_KEY } from './actionTypes'; +import { setE2EEKey } from './actions'; +import logger from './logger'; + +/** + * Middleware that captures actions related to E2EE. + * + * @param {Store} store - The redux store. + * @returns {Function} + */ +MiddlewareRegistry.register(({ getState }) => next => action => { + switch (action.type) { + case SET_E2EE_KEY: { + const conference = getCurrentConference(getState); + + if (conference) { + logger.debug(`New E2EE key: ${action.key}`); + conference.setE2EEKey(action.key); + } + + break; + } + } + + return next(action); +}); + +/** + * Set up state change listener to perform maintenance tasks when the conference + * is left or failed. + */ +StateListenerRegistry.register( + state => getCurrentConference(state), + (conference, { dispatch }, previousConference) => { + if (previousConference) { + dispatch(setE2EEKey(undefined)); + } + }); diff --git a/react/features/e2ee/reducer.js b/react/features/e2ee/reducer.js new file mode 100644 index 0000000000..2750abbfb5 --- /dev/null +++ b/react/features/e2ee/reducer.js @@ -0,0 +1,29 @@ +// @flow + +import { ReducerRegistry } from '../base/redux'; + +import { SET_E2EE_KEY } from './actionTypes'; + +const DEFAULT_STATE = { + + /** + * E2EE key. + */ + e2eeKey: undefined +}; + +/** + * Reduces the Redux actions of the feature features/e2ee. + */ +ReducerRegistry.register('features/e2ee', (state = DEFAULT_STATE, action) => { + switch (action.type) { + case SET_E2EE_KEY: + return { + ...state, + e2eeKey: action.key + }; + + default: + return state; + } +}); diff --git a/react/features/toolbox/components/web/Toolbox.js b/react/features/toolbox/components/web/Toolbox.js index cf4744a866..d25c70ac8e 100644 --- a/react/features/toolbox/components/web/Toolbox.js +++ b/react/features/toolbox/components/web/Toolbox.js @@ -33,6 +33,7 @@ import { OverflowMenuItem } from '../../../base/toolbox'; import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks'; import { VideoBlurButton } from '../../../blur'; import { ChatCounter, toggleChat } from '../../../chat'; +import { E2EEButton } from '../../../e2ee'; import { SharedDocumentButton } from '../../../etherpad'; import { openFeedbackDialog } from '../../../feedback'; import { @@ -1012,6 +1013,10 @@ class Toolbox extends Component { key = 'stats' onClick = { this._onToolbarOpenSpeakerStats } text = { t('toolbar.speakerStats') } />, + this._shouldShowButton('e2ee') + && , this._shouldShowButton('feedback') && _feedbackConfigured &&