diff --git a/config.js b/config.js index 4c822e9bd4..9f901a41cd 100644 --- a/config.js +++ b/config.js @@ -826,6 +826,10 @@ var config = { // format: 'flac' // + // }, + // e2ee: { + // labels, + // externallyManagedKey: false // }, // Options related to end-to-end (participant to participant) ping. diff --git a/modules/API/API.js b/modules/API/API.js index 6a286fa2ca..eeddba6af5 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -52,7 +52,7 @@ import { processExternalDeviceRequest } from '../../react/features/device-selection/functions'; import { isEnabled as isDropboxEnabled } from '../../react/features/dropbox'; -import { toggleE2EE } from '../../react/features/e2ee/actions'; +import { setMediaEncryptionKey, toggleE2EE } from '../../react/features/e2ee/actions'; import { setVolume } from '../../react/features/filmstrip'; import { invite } from '../../react/features/invite'; import { @@ -364,6 +364,9 @@ function initCommands() { logger.debug('Toggle E2EE key command received'); APP.store.dispatch(toggleE2EE(enabled)); }, + 'set-media-encryption-key': keyInfo => { + APP.store.dispatch(setMediaEncryptionKey(JSON.parse(keyInfo))); + }, 'set-video-quality': frameHeight => { logger.debug('Set video quality command received'); sendAnalytics(createApiEvent('set.video.quality')); diff --git a/modules/API/external/external_api.js b/modules/API/external/external_api.js index 43967b2034..c23ec2709b 100644 --- a/modules/API/external/external_api.js +++ b/modules/API/external/external_api.js @@ -50,6 +50,7 @@ const commands = { sendTones: 'send-tones', setFollowMe: 'set-follow-me', setLargeVideoParticipant: 'set-large-video-participant', + setMediaEncryptionKey: 'set-media-encryption-key', setParticipantVolume: 'set-participant-volume', setTileView: 'set-tile-view', setVideoQuality: 'set-video-quality', @@ -63,6 +64,7 @@ const commands = { toggleCamera: 'toggle-camera', toggleCameraMirror: 'toggle-camera-mirror', toggleChat: 'toggle-chat', + toggleE2EE: 'toggle-e2ee', toggleFilmStrip: 'toggle-film-strip', toggleModeration: 'toggle-moderation', toggleRaiseHand: 'toggle-raise-hand', @@ -1185,6 +1187,40 @@ export default class JitsiMeetExternalAPI extends EventEmitter { * @returns {void} */ stopRecording(mode) { - this.executeCommand('startRecording', mode); + this.executeCommand('stopRecording', mode); + } + + /** + * Sets e2ee enabled/disabled. + * + * @param {boolean} enabled - The new value for e2ee enabled. + * @returns {void} + */ + toggleE2EE(enabled) { + this.executeCommand('toggleE2EE', enabled); + } + + /** + * Sets the key and keyIndex for e2ee. + * + * @param {Object} keyInfo - Json containing key information. + * @param {CryptoKey} [keyInfo.encryptionKey] - The encryption key. + * @param {number} [keyInfo.index] - The index of the encryption key. + * @returns {void} + */ + async setMediaEncryptionKey(keyInfo) { + const { key, index } = keyInfo; + + if (key) { + const exportedKey = await crypto.subtle.exportKey('raw', key); + + this.executeCommand('setMediaEncryptionKey', JSON.stringify({ + exportedKey: Array.from(new Uint8Array(exportedKey)), + index })); + } else { + this.executeCommand('setMediaEncryptionKey', JSON.stringify({ + exportedKey: false, + index })); + } } } diff --git a/react/features/base/config/configWhitelist.js b/react/features/base/config/configWhitelist.js index bb202cdafc..2f3aa44d31 100644 --- a/react/features/base/config/configWhitelist.js +++ b/react/features/base/config/configWhitelist.js @@ -123,6 +123,7 @@ export default [ 'doNotFlipLocalVideo', 'dropbox', 'e2eeLabels', + 'e2ee', 'e2eping', 'enableDisplayNameInStats', 'enableEmailInStats', diff --git a/react/features/base/config/reducer.js b/react/features/base/config/reducer.js index 0a7ac85f08..a0a52295ce 100644 --- a/react/features/base/config/reducer.js +++ b/react/features/base/config/reducer.js @@ -314,6 +314,12 @@ function _translateLegacyConfig(oldValue: Object) { newValue.disableModeratorIndicator = interfaceConfig.DISABLE_FOCUS_INDICATOR; } + newValue.e2ee = newValue.e2ee || {}; + + if (oldValue.e2eeLabels) { + newValue.e2ee.e2eeLabels = oldValue.e2eeLabels; + } + return newValue; } diff --git a/react/features/base/participants/middleware.js b/react/features/base/participants/middleware.js index 844632c7fe..97bbebe501 100644 --- a/react/features/base/participants/middleware.js +++ b/react/features/base/participants/middleware.js @@ -332,18 +332,23 @@ StateListenerRegistry.register( */ function _e2eeUpdated({ getState, dispatch }, conference, participantId, newValue) { const e2eeEnabled = newValue === 'true'; - - const { maxMode } = getState()['features/e2ee'] || {}; - - if (maxMode !== MAX_MODE.THRESHOLD_EXCEEDED || !e2eeEnabled) { - dispatch(toggleE2EE(e2eeEnabled)); - } + const { e2ee = {} } = getState()['features/base/config']; dispatch(participantUpdated({ conference, id: participantId, e2eeEnabled })); + + if (e2ee.externallyManagedKey) { + return; + } + + const { maxMode } = getState()['features/e2ee'] || {}; + + if (maxMode !== MAX_MODE.THRESHOLD_EXCEEDED || !e2eeEnabled) { + dispatch(toggleE2EE(e2eeEnabled)); + } } /** diff --git a/react/features/e2ee/actionTypes.js b/react/features/e2ee/actionTypes.js index 6def85a50f..a5b12dd929 100644 --- a/react/features/e2ee/actionTypes.js +++ b/react/features/e2ee/actionTypes.js @@ -34,3 +34,12 @@ export const SET_EVERYONE_SUPPORT_E2EE = 'SET_EVERYONE_SUPPORT_E2EE'; * } */ export const SET_MAX_MODE = 'SET_MAX_MODE'; + +/** + * The type of the action which signals to set media encryption key for e2ee. + * + * { + * type: SET_MEDIA_ENCRYPTION_KEY + * } + */ +export const SET_MEDIA_ENCRYPTION_KEY = 'SET_MEDIA_ENCRYPTION_KEY'; diff --git a/react/features/e2ee/actions.js b/react/features/e2ee/actions.js index 1171b5a346..c3f17554d7 100644 --- a/react/features/e2ee/actions.js +++ b/react/features/e2ee/actions.js @@ -1,6 +1,11 @@ // @flow -import { SET_EVERYONE_ENABLED_E2EE, SET_EVERYONE_SUPPORT_E2EE, SET_MAX_MODE, TOGGLE_E2EE } from './actionTypes'; +import { + SET_EVERYONE_ENABLED_E2EE, + SET_EVERYONE_SUPPORT_E2EE, + SET_MAX_MODE, + SET_MEDIA_ENCRYPTION_KEY, + TOGGLE_E2EE } from './actionTypes'; /** * Dispatches an action to enable / disable E2EE. @@ -59,3 +64,21 @@ export function setE2EEMaxMode(maxMode: string) { maxMode }; } + +/** + * Dispatches an action to set media encryption key. + * + * @param {Object} keyInfo - Json containing key information. + * @param {string} [keyInfo.encryptionKey] - The exported encryption key. + * @param {number} [keyInfo.index] - The index of the encryption key. + * @returns {{ + * type: SET_MEDIA_ENCRYPTION_KEY, + * keyInfo: Object + * }} + */ +export function setMediaEncryptionKey(keyInfo: Object) { + return { + type: SET_MEDIA_ENCRYPTION_KEY, + keyInfo + }; +} diff --git a/react/features/e2ee/components/AbstractE2EELabel.js b/react/features/e2ee/components/AbstractE2EELabel.js index cbb85da2c0..af6418489c 100644 --- a/react/features/e2ee/components/AbstractE2EELabel.js +++ b/react/features/e2ee/components/AbstractE2EELabel.js @@ -27,8 +27,10 @@ export type Props = { * @returns {Props} */ export function _mapStateToProps(state: Object) { + const { e2ee = {} } = state['features/base/config']; + return { - _e2eeLabels: state['features/base/config'].e2eeLabels, + _e2eeLabels: e2ee.labels, _showLabel: state['features/e2ee'].everyoneEnabledE2EE }; } diff --git a/react/features/e2ee/middleware.js b/react/features/e2ee/middleware.js index 4590365fd5..c617eb5e08 100644 --- a/react/features/e2ee/middleware.js +++ b/react/features/e2ee/middleware.js @@ -17,7 +17,7 @@ import { import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux'; import { playSound, registerSound, unregisterSound } from '../base/sounds'; -import { TOGGLE_E2EE } from './actionTypes'; +import { SET_MEDIA_ENCRYPTION_KEY, TOGGLE_E2EE } from './actionTypes'; import { setE2EEMaxMode, setEveryoneEnabledE2EE, setEveryoneSupportE2EE, toggleE2EE } from './actions'; import { E2EE_OFF_SOUND_ID, E2EE_ON_SOUND_ID, MAX_MODE } from './constants'; import { isMaxModeReached, isMaxModeThresholdReached } from './functions'; @@ -31,6 +31,8 @@ import { E2EE_OFF_SOUND_FILE, E2EE_ON_SOUND_FILE } from './sounds'; * @returns {Function} */ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { + const conference = getCurrentConference(getState); + switch (action.type) { case APP_WILL_MOUNT: dispatch(registerSound( @@ -179,8 +181,6 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { } case TOGGLE_E2EE: { - const conference = getCurrentConference(getState); - if (conference && conference.isE2EEEnabled() !== action.enabled) { logger.debug(`E2EE will be ${action.enabled ? 'enabled' : 'disabled'}`); conference.toggleE2EE(action.enabled); @@ -201,6 +201,36 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { break; } + + case SET_MEDIA_ENCRYPTION_KEY: { + if (conference && conference.isE2EESupported()) { + const { exportedKey, index } = action.keyInfo; + + if (exportedKey) { + window.crypto.subtle.importKey( + 'raw', + new Uint8Array(exportedKey), + 'AES-GCM', + false, + [ 'encrypt', 'decrypt' ]) + .then( + encryptionKey => { + conference.setMediaEncryptionKey({ + encryptionKey, + index + }); + }) + .catch(error => logger.error('SET_MEDIA_ENCRYPTION_KEY error', error)); + } else { + conference.setMediaEncryptionKey({ + encryptionKey: false, + index + }); + } + } + + break; + } } return next(action); @@ -229,6 +259,12 @@ StateListenerRegistry.register( function _updateMaxMode(dispatch, getState) { const state = getState(); + const { e2ee = {} } = state['features/base/config']; + + if (e2ee.externallyManagedKey) { + return; + } + if (isMaxModeThresholdReached(state)) { dispatch(setE2EEMaxMode(MAX_MODE.THRESHOLD_EXCEEDED)); dispatch(toggleE2EE(false));