Browse Source

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
master
Saúl Ibarra Corretgé 4 years ago
parent
commit
cb6fbb0f03

+ 7
- 8
conference.js View File

@@ -113,6 +113,7 @@ import {
113 113
 import { getJitsiMeetGlobalNS } from './react/features/base/util';
114 114
 import { showDesktopPicker } from './react/features/desktop-picker';
115 115
 import { appendSuffix } from './react/features/display-name';
116
+import { setE2EEKey } from './react/features/e2ee';
116 117
 import {
117 118
     maybeOpenFeedbackDialog,
118 119
     submitFeedback
@@ -470,11 +471,6 @@ export default {
470 471
      */
471 472
     localVideo: null,
472 473
 
473
-    /**
474
-     * The key used for End-To-End Encryption.
475
-     */
476
-    e2eeKey: undefined,
477
-
478 474
     /**
479 475
      * Creates local media tracks and connects to a room. Will show error
480 476
      * dialogs in case accessing the local microphone and/or camera failed. Will
@@ -1202,11 +1198,14 @@ export default {
1202 1198
             items[key] = param[1];
1203 1199
         }
1204 1200
 
1205
-        this.e2eeKey = items.e2eekey;
1201
+        if (typeof items.e2eekey !== undefined) {
1202
+            APP.store.dispatch(setE2EEKey(items.e2eekey));
1206 1203
 
1207
-        logger.debug(`New E2EE key: ${this.e2eeKey}`);
1204
+            // Clean URL in browser history.
1205
+            const cleanUrl = window.location.href.split('#')[0];
1208 1206
 
1209
-        this._room.setE2EEKey(this.e2eeKey);
1207
+            history.replaceState(history.state, document.title, cleanUrl);
1208
+        }
1210 1209
     },
1211 1210
 
1212 1211
     /**

+ 2
- 1
interface_config.js View File

@@ -51,7 +51,8 @@ var interfaceConfig = {
51 51
         'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
52 52
         'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
53 53
         'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
54
-        'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone'
54
+        'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone',
55
+        'e2ee'
55 56
     ],
56 57
 
57 58
     SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ],

+ 5
- 0
lang/main.json View File

@@ -175,6 +175,9 @@
175 175
         "dismiss": "Dismiss",
176 176
         "displayNameRequired": "Hi! What’s your name?",
177 177
         "done": "Done",
178
+        "e2eeDescription": "<p>End-to-End Encryption is currently <strong>EXPERIMENTAL</strong>. Please see <a href='https://jitsi.org/blog/e2ee/' target='_blank'>this post</a> for details.</p><br/><p>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.</p>",
179
+        "e2eeLabel": "Key",
180
+        "e2eeTitle": "End-to-End Encryption",
178 181
         "enterDisplayName": "Please enter your name here",
179 182
         "error": "Error",
180 183
         "externalInstallationMsg": "You need to install our desktop sharing extension.",
@@ -595,6 +598,7 @@
595 598
             "chat": "Toggle chat window",
596 599
             "document": "Toggle shared document",
597 600
             "download": "Download our apps",
601
+            "e2ee": "End-to-End Encryption",
598 602
             "feedback": "Leave feedback",
599 603
             "fullScreen": "Toggle full screen",
600 604
             "hangup": "Leave the call",
@@ -638,6 +642,7 @@
638 642
         "documentClose": "Close shared document",
639 643
         "documentOpen": "Open shared document",
640 644
         "download": "Download our apps",
645
+        "e2ee": "End-to-End Encryption",
641 646
         "enterFullScreen": "View full screen",
642 647
         "enterTileView": "Enter tile view",
643 648
         "exitFullScreen": "Exit full screen",

+ 14
- 0
react/features/analytics/AnalyticsEvents.js View File

@@ -257,6 +257,20 @@ export function createDeviceChangedEvent(mediaType, deviceType) {
257 257
     };
258 258
 }
259 259
 
260
+/**
261
+ * Creates an event indicating that an action related to E2EE occurred.
262
+ *
263
+ * @param {string} action - The action which occurred.
264
+ * @returns {Object} The event in a format suitable for sending via
265
+ * sendAnalytics.
266
+ */
267
+export function createE2EEEvent(action) {
268
+    return {
269
+        action,
270
+        actionSubject: 'e2ee'
271
+    };
272
+}
273
+
260 274
 /**
261 275
  * Creates an event which specifies that the feedback dialog has been opened.
262 276
  *

+ 5
- 0
react/features/base/conference/reducer.js View File

@@ -31,6 +31,7 @@ import { isRoomValid } from './functions';
31 31
 
32 32
 const DEFAULT_STATE = {
33 33
     conference: undefined,
34
+    e2eeSupported: undefined,
34 35
     joining: undefined,
35 36
     leaving: undefined,
36 37
     locked: undefined,
@@ -175,6 +176,7 @@ function _conferenceFailed(state, { conference, error }) {
175 176
     return assign(state, {
176 177
         authRequired,
177 178
         conference: undefined,
179
+        e2eeSupported: undefined,
178 180
         error,
179 181
         joining: undefined,
180 182
         leaving: undefined,
@@ -226,6 +228,9 @@ function _conferenceJoined(state, { conference }) {
226 228
          * @type {JitsiConference}
227 229
          */
228 230
         conference,
231
+
232
+        e2eeSupported: conference.isE2EESupported(),
233
+
229 234
         joining: undefined,
230 235
         leaving: undefined,
231 236
 

+ 8
- 0
react/features/e2ee/actionTypes.js View File

@@ -0,0 +1,8 @@
1
+/**
2
+ * The type of the action which signals the E2EE key has changed.
3
+ *
4
+ * {
5
+ *     type: SET_E2EE_KEY
6
+ * }
7
+ */
8
+export const SET_E2EE_KEY = 'SET_E2EE_KEY';

+ 16
- 0
react/features/e2ee/actions.js View File

@@ -0,0 +1,16 @@
1
+// @flow
2
+
3
+import { SET_E2EE_KEY } from './actionTypes';
4
+
5
+/**
6
+ * Dispatches an action to set the E2EE key.
7
+ *
8
+ * @param {string|undefined} key - The new key to be used for E2EE.
9
+ * @returns {Object}
10
+ */
11
+export function setE2EEKey(key: ?string) {
12
+    return {
13
+        type: SET_E2EE_KEY,
14
+        key
15
+    };
16
+}

+ 76
- 0
react/features/e2ee/components/E2EEButton.js View File

@@ -0,0 +1,76 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { createE2EEEvent, sendAnalytics } from '../../analytics';
6
+import { openDialog } from '../../base/dialog';
7
+import { translate } from '../../base/i18n';
8
+import { IconRoomUnlock } from '../../base/icons';
9
+import { connect } from '../../base/redux';
10
+import { AbstractButton, BetaTag } from '../../base/toolbox';
11
+import type { AbstractButtonProps } from '../../base/toolbox';
12
+
13
+import E2EEDialog from './E2EEDialog';
14
+
15
+
16
+type Props = AbstractButtonProps & {
17
+
18
+    /**
19
+     * The redux {@code dispatch} function.
20
+     */
21
+    dispatch: Function
22
+
23
+};
24
+
25
+/**
26
+ * Button that open a dialog to set the E2EE key.
27
+ */
28
+class E2EEButton extends AbstractButton<Props, *> {
29
+    accessibilityLabel = 'toolbar.accessibilityLabel.e2ee';
30
+    icon = IconRoomUnlock;
31
+    label = 'toolbar.e2ee';
32
+    tooltip = 'toolbar.e2ee';
33
+
34
+    /**
35
+     * Helper function to be implemented by subclasses, which returns
36
+     * a React Element to display (a beta tag) at the end of the button.
37
+     *
38
+     * @override
39
+     * @protected
40
+     * @returns {ReactElement}
41
+     */
42
+    _getElementAfter() {
43
+        return <BetaTag />;
44
+    }
45
+
46
+    /**
47
+     * Handles clicking / pressing the button, and opens the E2EE dialog.
48
+     *
49
+     * @protected
50
+     * @returns {void}
51
+     */
52
+    _handleClick() {
53
+        sendAnalytics(createE2EEEvent('dialog.open'));
54
+        this.props.dispatch(openDialog(E2EEDialog));
55
+    }
56
+}
57
+
58
+/**
59
+ * Maps (parts of) the redux state to the associated props for this component.
60
+ *
61
+ * @param {Object} state - The Redux state.
62
+ * @param {Props} ownProps - The own props of the Component.
63
+ * @private
64
+ * @returns {Props}
65
+ */
66
+export function mapStateToProps(state: Object, ownProps: Props) {
67
+    const { e2eeSupported } = state['features/base/conference'];
68
+    const { visible = Boolean(e2eeSupported) } = ownProps;
69
+
70
+    return {
71
+        visible
72
+    };
73
+}
74
+
75
+
76
+export default translate(connect(mapStateToProps)(E2EEButton));

+ 142
- 0
react/features/e2ee/components/E2EEDialog.js View File

@@ -0,0 +1,142 @@
1
+/* @flow */
2
+
3
+import React, { Component } from 'react';
4
+import type { Dispatch } from 'redux';
5
+import { FieldTextStateless as TextField } from '@atlaskit/field-text';
6
+
7
+import { createE2EEEvent, sendAnalytics } from '../../analytics';
8
+import { Dialog } from '../../base/dialog';
9
+import { translate, translateToHTML } from '../../base/i18n';
10
+import { connect } from '../../base/redux';
11
+
12
+import { setE2EEKey } from '../actions';
13
+
14
+
15
+type Props = {
16
+
17
+    /**
18
+     * The current E2EE key.
19
+     */
20
+    _key: string,
21
+
22
+    /**
23
+     * The redux {@code dispatch} function.
24
+     */
25
+    dispatch: Dispatch<any>,
26
+
27
+    /**
28
+     * Invoked to obtain translated strings.
29
+     */
30
+    t: Function
31
+};
32
+
33
+type State = {
34
+
35
+    /**
36
+     * The current E2EE key.
37
+     */
38
+    key: string
39
+};
40
+
41
+/**
42
+ * Implements a React {@code Component} for displaying a dialog with a field
43
+ * for setting the E2EE key.
44
+ *
45
+ * @extends Component
46
+ */
47
+class E2EEDialog extends Component<Props, State> {
48
+    /**
49
+     * Initializes a new {@code E2EEDialog  } instance.
50
+     *
51
+     * @param {Object} props - The read-only properties with which the new
52
+     * instance is to be initialized.
53
+     */
54
+    constructor(props: Props) {
55
+        super(props);
56
+
57
+        this.state = {
58
+            key: this.props._key
59
+        };
60
+
61
+        // Bind event handlers so they are only bound once for every instance.
62
+        this._onKeyChange = this._onKeyChange.bind(this);
63
+        this._onSubmit = this._onSubmit.bind(this);
64
+    }
65
+
66
+    /**
67
+     * Implements React's {@link Component#render()}.
68
+     *
69
+     * @inheritdoc
70
+     * @returns {ReactElement}
71
+     */
72
+    render() {
73
+        const { t } = this.props;
74
+
75
+        return (
76
+            <Dialog
77
+                isModal = { false }
78
+                onSubmit = { this._onSubmit }
79
+                titleKey = 'dialog.e2eeTitle'
80
+                width = 'small'>
81
+                <div className = 'e2ee-destription'>
82
+                    { translateToHTML(t, 'dialog.e2eeDescription') }
83
+                </div>
84
+                <TextField
85
+                    autoFocus = { true }
86
+                    compact = { true }
87
+                    label = { t('dialog.e2eeLabel') }
88
+                    name = 'e2eeKey'
89
+                    onChange = { this._onKeyChange }
90
+                    shouldFitContainer = { true }
91
+                    type = 'password'
92
+                    value = { this.state.key } />
93
+            </Dialog>);
94
+    }
95
+
96
+    _onKeyChange: (Object) => void;
97
+
98
+    /**
99
+     * Updates the entered key.
100
+     *
101
+     * @param {Object} event - The DOM event triggered from the entered value having changed.
102
+     * @private
103
+     * @returns {void}
104
+     */
105
+    _onKeyChange(event) {
106
+        this.setState({ key: event.target.value.trim() });
107
+    }
108
+
109
+    _onSubmit: () => boolean;
110
+
111
+    /**
112
+     * Dispatches an action to update the E2EE key.
113
+     *
114
+     * @private
115
+     * @returns {boolean}
116
+     */
117
+    _onSubmit() {
118
+        const { key } = this.state;
119
+
120
+        sendAnalytics(createE2EEEvent(`key.${key ? 'set' : 'unset'}`));
121
+        this.props.dispatch(setE2EEKey(key));
122
+
123
+        return true;
124
+    }
125
+}
126
+
127
+/**
128
+ * Maps (parts of) the Redux state to the associated props for this component.
129
+ *
130
+ * @param {Object} state - The Redux state.
131
+ * @private
132
+ * @returns {Props}
133
+ */
134
+function mapStateToProps(state) {
135
+    const { e2eeKey } = state['features/e2ee'];
136
+
137
+    return {
138
+        _key: e2eeKey || ''
139
+    };
140
+}
141
+
142
+export default translate(connect(mapStateToProps)(E2EEDialog));

+ 2
- 0
react/features/e2ee/components/index.js View File

@@ -0,0 +1,2 @@
1
+export { default as E2EEButton } from './E2EEButton';
2
+export { default as E2EEDialog } from './E2EEDialog';

+ 6
- 0
react/features/e2ee/index.js View File

@@ -0,0 +1,6 @@
1
+export * from './actions';
2
+export * from './actionTypes';
3
+export * from './components';
4
+
5
+import './middleware';
6
+import './reducer';

+ 5
- 0
react/features/e2ee/logger.js View File

@@ -0,0 +1,5 @@
1
+// @flow
2
+
3
+import { getLogger } from '../base/logging/functions';
4
+
5
+export default getLogger('features/e2ee');

+ 43
- 0
react/features/e2ee/middleware.js View File

@@ -0,0 +1,43 @@
1
+// @flow
2
+
3
+import { getCurrentConference } from '../base/conference';
4
+import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
5
+
6
+import { SET_E2EE_KEY } from './actionTypes';
7
+import { setE2EEKey } from './actions';
8
+import logger from './logger';
9
+
10
+/**
11
+ * Middleware that captures actions related to E2EE.
12
+ *
13
+ * @param {Store} store - The redux store.
14
+ * @returns {Function}
15
+ */
16
+MiddlewareRegistry.register(({ getState }) => next => action => {
17
+    switch (action.type) {
18
+    case SET_E2EE_KEY: {
19
+        const conference = getCurrentConference(getState);
20
+
21
+        if (conference) {
22
+            logger.debug(`New E2EE key: ${action.key}`);
23
+            conference.setE2EEKey(action.key);
24
+        }
25
+
26
+        break;
27
+    }
28
+    }
29
+
30
+    return next(action);
31
+});
32
+
33
+/**
34
+ * Set up state change listener to perform maintenance tasks when the conference
35
+ * is left or failed.
36
+ */
37
+StateListenerRegistry.register(
38
+    state => getCurrentConference(state),
39
+    (conference, { dispatch }, previousConference) => {
40
+        if (previousConference) {
41
+            dispatch(setE2EEKey(undefined));
42
+        }
43
+    });

+ 29
- 0
react/features/e2ee/reducer.js View File

@@ -0,0 +1,29 @@
1
+// @flow
2
+
3
+import { ReducerRegistry } from '../base/redux';
4
+
5
+import { SET_E2EE_KEY } from './actionTypes';
6
+
7
+const DEFAULT_STATE = {
8
+
9
+    /**
10
+     * E2EE key.
11
+     */
12
+    e2eeKey: undefined
13
+};
14
+
15
+/**
16
+ * Reduces the Redux actions of the feature features/e2ee.
17
+ */
18
+ReducerRegistry.register('features/e2ee', (state = DEFAULT_STATE, action) => {
19
+    switch (action.type) {
20
+    case SET_E2EE_KEY:
21
+        return {
22
+            ...state,
23
+            e2eeKey: action.key
24
+        };
25
+
26
+    default:
27
+        return state;
28
+    }
29
+});

+ 5
- 0
react/features/toolbox/components/web/Toolbox.js View File

@@ -33,6 +33,7 @@ import { OverflowMenuItem } from '../../../base/toolbox';
33 33
 import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks';
34 34
 import { VideoBlurButton } from '../../../blur';
35 35
 import { ChatCounter, toggleChat } from '../../../chat';
36
+import { E2EEButton } from '../../../e2ee';
36 37
 import { SharedDocumentButton } from '../../../etherpad';
37 38
 import { openFeedbackDialog } from '../../../feedback';
38 39
 import {
@@ -1012,6 +1013,10 @@ class Toolbox extends Component<Props, State> {
1012 1013
                     key = 'stats'
1013 1014
                     onClick = { this._onToolbarOpenSpeakerStats }
1014 1015
                     text = { t('toolbar.speakerStats') } />,
1016
+            this._shouldShowButton('e2ee')
1017
+                && <E2EEButton
1018
+                    key = 'e2ee'
1019
+                    showLabel = { true } />,
1015 1020
             this._shouldShowButton('feedback')
1016 1021
                 && _feedbackConfigured
1017 1022
                 && <OverflowMenuItem

Loading…
Cancel
Save