init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
/**
* The type of the action which signals that E2EE needs to be enabled / disabled.
*
* {
* type: TOGGLE_E2EE
* }
*/
export const TOGGLE_E2EE = 'TOGGLE_E2EE';
/**
* The type of the action which signals to set new value E2EE maxMode.
*
* {
* type: SET_MAX_MODE
* }
*/
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';
export const START_VERIFICATION = 'START_VERIFICATION';
export const PARTICIPANT_VERIFIED = 'PARTICIPANT_VERIFIED';

View File

@@ -0,0 +1,85 @@
import {
PARTICIPANT_VERIFIED,
SET_MAX_MODE,
SET_MEDIA_ENCRYPTION_KEY,
START_VERIFICATION,
TOGGLE_E2EE } from './actionTypes';
/**
* Dispatches an action to enable / disable E2EE.
*
* @param {boolean} enabled - Whether E2EE is to be enabled or not.
* @returns {Object}
*/
export function toggleE2EE(enabled: boolean) {
return {
type: TOGGLE_E2EE,
enabled
};
}
/**
* Dispatches an action to set E2EE maxMode.
*
* @param {string} maxMode - The new value.
* @returns {Object}
*/
export function setE2EEMaxMode(maxMode: string) {
return {
type: SET_MAX_MODE,
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
};
}
/**
* Dispatches an action to start participant e2ee verification process.
*
* @param {string} pId - The participant id.
* @returns {{
* type: START_VERIFICATION,
* pId: string
* }}
*/
export function startVerification(pId: string) {
return {
type: START_VERIFICATION,
pId
};
}
/**
* Dispatches an action to set participant e2ee verification status.
*
* @param {string} pId - The participant id.
* @param {boolean} isVerified - The verification status.
* @returns {{
* type: PARTICIPANT_VERIFIED,
* pId: string,
* isVerified: boolean
* }}
*/
export function participantVerified(pId: string, isVerified: boolean) {
return {
type: PARTICIPANT_VERIFIED,
pId,
isVerified
};
}

View File

@@ -0,0 +1,59 @@
import React from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { IReduxState } from '../../app/types';
import { translate } from '../../base/i18n/functions';
import { IconE2EE } from '../../base/icons/svg';
import Label from '../../base/label/components/web/Label';
import { COLORS } from '../../base/label/constants';
import Tooltip from '../../base/tooltip/components/Tooltip';
export interface IProps extends WithTranslation {
/**
* Custom e2ee labels.
*/
_e2eeLabels?: any;
/**
* True if the label needs to be rendered, false otherwise.
*/
_showLabel?: boolean;
}
const E2EELabel = ({ _e2eeLabels, _showLabel, t }: IProps) => {
if (!_showLabel) {
return null;
}
const content = _e2eeLabels?.tooltip || t('e2ee.labelToolTip');
return (
<Tooltip
content = { content }
position = { 'bottom' }>
<Label
color = { COLORS.green }
icon = { IconE2EE } />
</Tooltip>
);
};
/**
* Maps (parts of) the redux state to the associated props of this {@code Component}.
*
* @param {Object} state - The redux state.
* @private
* @returns {IProps}
*/
export function _mapStateToProps(state: IReduxState) {
const { e2ee = {} } = state['features/base/config'];
return {
_e2eeLabels: e2ee.labels,
_showLabel: state['features/base/participants'].numberOfParticipantsDisabledE2EE === 0
};
}
export default translate(connect(_mapStateToProps)(E2EELabel));

View File

@@ -0,0 +1,174 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { createE2EEEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IReduxState, IStore } from '../../app/types';
import Switch from '../../base/ui/components/web/Switch';
import { toggleE2EE } from '../actions';
import { MAX_MODE } from '../constants';
import { doesEveryoneSupportE2EE } from '../functions';
interface IProps {
/**
* The resource for the description, computed based on the maxMode and whether the switch is toggled or not.
*/
_descriptionResource?: string;
/**
* Custom e2ee labels.
*/
_e2eeLabels: any;
/**
* Whether the switch is currently enabled or not.
*/
_enabled: boolean;
/**
* Indicates whether all participants in the conference currently support E2EE.
*/
_everyoneSupportE2EE: boolean;
/**
* Whether E2EE is currently enabled or not.
*/
_toggled: boolean;
/**
* The redux {@code dispatch} function.
*/
dispatch: IStore['dispatch'];
}
const useStyles = makeStyles()(() => {
return {
e2eeSection: {
display: 'flex',
flexDirection: 'column'
},
description: {
fontSize: '0.875rem',
margin: '15px 0'
},
controlRow: {
display: 'flex',
justifyContent: 'space-between',
marginTop: '15px',
'& label': {
fontSize: '0.875rem',
fontWeight: 'bold'
}
}
};
});
/**
* Implements a React {@code Component} for displaying a security dialog section with a field
* for setting the E2EE key.
*
* @param {IProps} props - Component's props.
* @returns {JSX}
*/
const E2EESection = ({
_descriptionResource,
_enabled,
_e2eeLabels,
_everyoneSupportE2EE,
_toggled,
dispatch
}: IProps) => {
const { classes } = useStyles();
const { t } = useTranslation();
const [ toggled, setToggled ] = useState(_toggled ?? false);
useEffect(() => {
setToggled(_toggled);
}, [ _toggled ]);
/**
* Callback to be invoked when the user toggles E2EE on or off.
*
* @private
* @returns {void}
*/
const _onToggle = useCallback(() => {
const newValue = !toggled;
setToggled(newValue);
sendAnalytics(createE2EEEvent(`enabled.${String(newValue)}`));
dispatch(toggleE2EE(newValue));
}, [ toggled ]);
const description = _e2eeLabels?.description || t(_descriptionResource ?? '');
const label = _e2eeLabels?.label || t('dialog.e2eeLabel');
const warning = _e2eeLabels?.warning || t('dialog.e2eeWarning');
return (
<div
className = { classes.e2eeSection }
id = 'e2ee-section'>
<p
aria-live = 'polite'
className = { classes.description }
id = 'e2ee-section-description'>
{description}
{!_everyoneSupportE2EE && <br />}
{!_everyoneSupportE2EE && warning}
</p>
<div className = { classes.controlRow }>
<label htmlFor = 'e2ee-section-switch'>
{label}
</label>
<Switch
checked = { toggled }
disabled = { !_enabled }
id = 'e2ee-section-switch'
onChange = { _onToggle } />
</div>
</div>
);
};
/**
* Maps (parts of) the Redux state to the associated props for this component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {IProps}
*/
function mapStateToProps(state: IReduxState) {
const { enabled: e2eeEnabled, maxMode } = state['features/e2ee'];
const { e2ee = {} } = state['features/base/config'];
let descriptionResource: string | undefined = '';
if (e2ee.labels) {
// When e2eeLabels are present, the description resource is ignored.
descriptionResource = undefined;
} else if (maxMode === MAX_MODE.THRESHOLD_EXCEEDED) {
descriptionResource = 'dialog.e2eeDisabledDueToMaxModeDescription';
} else if (maxMode === MAX_MODE.ENABLED) {
descriptionResource = e2eeEnabled
? 'dialog.e2eeWillDisableDueToMaxModeDescription' : 'dialog.e2eeDisabledDueToMaxModeDescription';
} else {
descriptionResource = 'dialog.e2eeDescription';
}
return {
_descriptionResource: descriptionResource,
_e2eeLabels: e2ee.labels,
_enabled: maxMode === MAX_MODE.DISABLED || e2eeEnabled,
_toggled: e2eeEnabled,
_everyoneSupportE2EE: Boolean(doesEveryoneSupportE2EE(state))
};
}
export default connect(mapStateToProps)(E2EESection);

View File

@@ -0,0 +1,123 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState, IStore } from '../../app/types';
import { getParticipantById } from '../../base/participants/functions';
import Dialog from '../../base/ui/components/web/Dialog';
import { participantVerified } from '../actions';
import { ISas } from '../reducer';
interface IProps {
decimal: string;
dispatch: IStore['dispatch'];
emoji: string;
pId: string;
participantName?: string;
sas: ISas;
}
const useStyles = makeStyles()(() => {
return {
container: {
display: 'flex',
flexDirection: 'column',
margin: '16px'
},
row: {
alignSelf: 'center',
display: 'flex'
},
item: {
textAlign: 'center',
margin: '16px'
},
emoji: {
fontSize: '2.5rem',
margin: '12px'
}
};
});
const ParticipantVerificationDialog = ({
dispatch,
participantName,
pId,
sas
}: IProps) => {
const { classes } = useStyles();
const { t } = useTranslation();
const _onDismissed = useCallback(() => {
dispatch(participantVerified(pId, false));
return true;
}, [ pId ]);
const _onConfirmed = useCallback(() => {
dispatch(participantVerified(pId, true));
return true;
}, [ pId ]);
const { emoji } = sas;
return (
<Dialog
cancel = {{ translationKey: 'dialog.verifyParticipantDismiss' }}
ok = {{ translationKey: 'dialog.verifyParticipantConfirm' }}
onCancel = { _onDismissed }
onSubmit = { _onConfirmed }
titleKey = 'dialog.verifyParticipantTitle'>
<div>
{t('dialog.verifyParticipantQuestion', { participantName })}
</div>
<div className = { classes.container }>
<div className = { classes.row }>
{/* @ts-ignore */}
{emoji.slice(0, 4).map((e: Array<string>) =>
(<div
className = { classes.item }
key = { e.toString() }>
<div className = { classes.emoji }>{e[0]}</div>
<div>{e[1].charAt(0).toUpperCase() + e[1].slice(1)}</div>
</div>))}
</div>
<div className = { classes.row }>
{/* @ts-ignore */}
{emoji.slice(4, 7).map((e: Array<string>) =>
(<div
className = { classes.item }
key = { e.toString() }>
<div className = { classes.emoji }>{e[0]} </div>
<div>{e[1].charAt(0).toUpperCase() + e[1].slice(1)}</div>
</div>))}
</div>
</div>
</Dialog>
);
};
/**
* Maps part of the Redux store to the props of this component.
*
* @param {IReduxState} state - The Redux state.
* @param {IProps} ownProps - The own props of the component.
* @returns {IProps}
*/
export function _mapStateToProps(state: IReduxState, ownProps: IProps) {
const participant = getParticipantById(state, ownProps.pId);
return {
sas: ownProps.sas,
pId: ownProps.pId,
participantName: participant?.name
};
}
export default connect(_mapStateToProps)(ParticipantVerificationDialog);

View File

@@ -0,0 +1,53 @@
/**
* The identifier of the sound to be played when e2ee is disabled.
*
* @type {string}
*/
export const E2EE_OFF_SOUND_ID = 'E2EE_OFF_SOUND';
/**
* The identifier of the sound to be played when e2ee is enabled.
*
* @type {string}
*/
export const E2EE_ON_SOUND_ID = 'E2EE_ON_SOUND';
/**
* The number of participants after which e2ee maxMode is set to MAX_MODE.ENABLED.
*
* @type {integer}
*/
export const MAX_MODE_LIMIT = 20;
/**
* If the number of participants is greater then MAX_MODE_LIMIT + MAX_MODE_THRESHOLD
* e2ee maxMode is set to MAX_MODE.THRESHOLD_EXCEEDED.
*
* @type {integer}
*/
export const MAX_MODE_THRESHOLD = 5;
export const MAX_MODE = {
/**
* Mode for which the e2ee can be enabled or disabled.
* If e2ee is enabled, e2ee section is enabled with a warning text.
* If e2ee is disabled, e2ee section is disabled with a warning text.
*
* @type {string}
*/
ENABLED: 'max-mode-enabled',
/**
* Mode for which the e2ee and the e2ee section are automatically disabled.
*
* @type {string}
*/
THRESHOLD_EXCEEDED: 'max-mode-threshold-exceeded',
/**
* The default e2ee maxMode, e2ee can be enabled/disabled, e2ee section is enabled.
*
* @type {string}
*/
DISABLED: 'max-mode-disabled'
};

View File

@@ -0,0 +1,120 @@
import i18next from 'i18next';
import { IReduxState, IStore } from '../app/types';
import { IStateful } from '../base/app/types';
import { getSoundFileSrc } from '../base/media/functions';
import { getParticipantById, getParticipantCount, getParticipantCountWithFake } from '../base/participants/functions';
import { toState } from '../base/redux/functions';
import { registerSound, unregisterSound } from '../base/sounds/actions';
import {
E2EE_OFF_SOUND_ID,
E2EE_ON_SOUND_ID,
MAX_MODE_LIMIT,
MAX_MODE_THRESHOLD
} from './constants';
import {
E2EE_OFF_SOUND_FILE,
E2EE_ON_SOUND_FILE
} from './sounds';
/**
* Gets the value of a specific React {@code Component} prop of the currently
* mounted {@link App}.
*
* @param {IStateful} stateful - The redux store or {@code getState}
* function.
* @param {string} propName - The name of the React {@code Component} prop of
* the currently mounted {@code App} to get.
* @returns {*} The value of the specified React {@code Component} prop of the
* currently mounted {@code App}.
*/
export function doesEveryoneSupportE2EE(stateful: IStateful) {
const state = toState(stateful);
const { numberOfParticipantsNotSupportingE2EE } = state['features/base/participants'];
const { e2eeSupported } = state['features/base/conference'];
const participantCount = getParticipantCountWithFake(state);
if (participantCount === 1) {
// This will happen if we are alone.
return e2eeSupported;
}
return numberOfParticipantsNotSupportingE2EE === 0;
}
/**
* Returns true is the number of participants is larger than {@code MAX_MODE_LIMIT}.
*
* @param {Function|Object} stateful - The redux store or {@code getState}
* function.
* @returns {boolean}
*/
export function isMaxModeReached(stateful: IStateful) {
const participantCount = getParticipantCount(toState(stateful));
return participantCount >= MAX_MODE_LIMIT;
}
/**
* Returns true is the number of participants is larger than {@code MAX_MODE_LIMIT + MAX_MODE_THREHOLD}.
*
* @param {Function|Object} stateful - The redux store or {@code getState}
* function.
* @returns {boolean}
*/
export function isMaxModeThresholdReached(stateful: IStateful) {
const participantCount = getParticipantCount(toState(stateful));
return participantCount >= MAX_MODE_LIMIT + MAX_MODE_THRESHOLD;
}
/**
* Returns whether e2ee is enabled by the backend.
*
* @param {Object} state - The redux state.
* @param {string} pId - The participant id.
* @returns {boolean}
*/
export function displayVerification(state: IReduxState, pId: string) {
const { conference } = state['features/base/conference'];
const participant = getParticipantById(state, pId);
return Boolean(conference?.isE2EEEnabled()
&& participant?.e2eeVerificationAvailable
&& participant?.e2eeVerified === undefined);
}
/**
* Unregisters the audio files based on locale.
*
* @param {Dispatch<any>} dispatch - The redux dispatch function.
* @returns {void}
*/
export function unregisterE2eeAudioFiles(dispatch: IStore['dispatch']) {
dispatch(unregisterSound(E2EE_OFF_SOUND_ID));
dispatch(unregisterSound(E2EE_ON_SOUND_ID));
}
/**
* Registers the audio files based on locale.
*
* @param {Dispatch<any>} dispatch - The redux dispatch function.
* @param {boolean|undefined} shouldUnregister - Whether the sounds should be unregistered.
* @returns {void}
*/
export function registerE2eeAudioFiles(dispatch: IStore['dispatch'], shouldUnregister?: boolean) {
const language = i18next.language;
shouldUnregister && unregisterE2eeAudioFiles(dispatch);
dispatch(registerSound(
E2EE_OFF_SOUND_ID,
getSoundFileSrc(E2EE_OFF_SOUND_FILE, language)));
dispatch(registerSound(
E2EE_ON_SOUND_ID,
getSoundFileSrc(E2EE_ON_SOUND_FILE, language)));
}

View File

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

View File

@@ -0,0 +1,215 @@
import { IStore } from '../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
import { getCurrentConference } from '../base/conference/functions';
import { openDialog } from '../base/dialog/actions';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants/actionTypes';
import { participantUpdated } from '../base/participants/actions';
import {
getLocalParticipant,
getParticipantById,
isScreenShareParticipant
} from '../base/participants/functions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { playSound } from '../base/sounds/actions';
import { PARTICIPANT_VERIFIED, SET_MEDIA_ENCRYPTION_KEY, START_VERIFICATION, TOGGLE_E2EE } from './actionTypes';
import { setE2EEMaxMode, toggleE2EE } from './actions';
import ParticipantVerificationDialog from './components/ParticipantVerificationDialog';
import { E2EE_OFF_SOUND_ID, E2EE_ON_SOUND_ID, MAX_MODE } from './constants';
import {
isMaxModeReached,
isMaxModeThresholdReached,
registerE2eeAudioFiles,
unregisterE2eeAudioFiles
} from './functions';
import logger from './logger';
/**
* Middleware that captures actions related to E2EE.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
const conference = getCurrentConference(getState);
switch (action.type) {
case APP_WILL_MOUNT:
registerE2eeAudioFiles(dispatch);
break;
case APP_WILL_UNMOUNT:
unregisterE2eeAudioFiles(dispatch);
break;
case CONFERENCE_JOINED:
_updateMaxMode(dispatch, getState);
break;
case PARTICIPANT_JOINED: {
const result = next(action);
if (!isScreenShareParticipant(action.participant) && !action.participant.local) {
_updateMaxMode(dispatch, getState);
}
return result;
}
case PARTICIPANT_LEFT: {
const participant = getParticipantById(getState(), action.participant?.id);
const result = next(action);
if (!isScreenShareParticipant(participant)) {
_updateMaxMode(dispatch, getState);
}
return result;
}
case TOGGLE_E2EE: {
if (conference?.isE2EESupported() && conference.isE2EEEnabled() !== action.enabled) {
logger.debug(`E2EE will be ${action.enabled ? 'enabled' : 'disabled'}`);
conference.toggleE2EE(action.enabled);
// Broadcast that we enabled / disabled E2EE.
const participant = getLocalParticipant(getState);
dispatch(participantUpdated({
e2eeEnabled: action.enabled,
id: participant?.id ?? '',
local: true
}));
const soundID = action.enabled ? E2EE_ON_SOUND_ID : E2EE_OFF_SOUND_ID;
dispatch(playSound(soundID));
}
break;
}
case SET_MEDIA_ENCRYPTION_KEY: {
if (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;
}
case PARTICIPANT_VERIFIED: {
const { isVerified, pId } = action;
conference?.markParticipantVerified(pId, isVerified);
break;
}
case START_VERIFICATION: {
conference?.startVerification(action.pId);
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(toggleE2EE(false));
}
if (conference) {
conference.on(JitsiConferenceEvents.E2EE_VERIFICATION_AVAILABLE, (pId: string) => {
dispatch(participantUpdated({
e2eeVerificationAvailable: true,
id: pId
}));
});
conference.on(JitsiConferenceEvents.E2EE_VERIFICATION_READY, (pId: string, sas: object) => {
dispatch(openDialog(ParticipantVerificationDialog, { pId,
sas }));
});
conference.on(JitsiConferenceEvents.E2EE_VERIFICATION_COMPLETED,
(pId: string, success: boolean, message: string) => {
if (message) {
logger.warn('E2EE_VERIFICATION_COMPLETED warning', message);
}
dispatch(participantUpdated({
e2eeVerified: success,
id: pId
}));
});
}
});
/**
* Sets the maxMode based on the number of participants in the conference.
*
* @param { Dispatch<any>} dispatch - The redux dispatch function.
* @param {Function|Object} getState - The {@code getState} function.
* @private
* @returns {void}
*/
function _updateMaxMode(dispatch: IStore['dispatch'], getState: IStore['getState']) {
const state = getState();
const { e2ee = {} } = state['features/base/config'];
if (e2ee.externallyManagedKey) {
return;
}
const { maxMode, enabled } = state['features/e2ee'];
const isMaxModeThresholdReachedValue = isMaxModeThresholdReached(state);
let newMaxMode: string;
if (isMaxModeThresholdReachedValue) {
newMaxMode = MAX_MODE.THRESHOLD_EXCEEDED;
} else if (isMaxModeReached(state)) {
newMaxMode = MAX_MODE.ENABLED;
} else {
newMaxMode = MAX_MODE.DISABLED;
}
if (maxMode !== newMaxMode) {
dispatch(setE2EEMaxMode(newMaxMode));
}
if (isMaxModeThresholdReachedValue && !enabled) {
dispatch(toggleE2EE(false));
}
}

View File

@@ -0,0 +1,44 @@
import ReducerRegistry from '../base/redux/ReducerRegistry';
import {
SET_MAX_MODE,
TOGGLE_E2EE
} from './actionTypes';
import { MAX_MODE } from './constants';
const DEFAULT_STATE = {
enabled: false,
maxMode: MAX_MODE.DISABLED
};
export interface IE2EEState {
enabled: boolean;
maxMode: string;
}
export interface ISas {
emoji: Array<string>;
}
/**
* Reduces the Redux actions of the feature features/e2ee.
*/
ReducerRegistry.register<IE2EEState>('features/e2ee', (state = DEFAULT_STATE, action): IE2EEState => {
switch (action.type) {
case TOGGLE_E2EE:
return {
...state,
enabled: action.enabled
};
case SET_MAX_MODE: {
return {
...state,
maxMode: action.maxMode
};
}
default:
return state;
}
});

View File

@@ -0,0 +1,13 @@
/**
* The name of the bundled audio file which will be played when e2ee is disabled.
*
* @type {string}
*/
export const E2EE_OFF_SOUND_FILE = 'e2eeOff.mp3';
/**
* The name of the bundled audio file which will be played when e2ee is enabled.
*
* @type {string}
*/
export const E2EE_ON_SOUND_FILE = 'e2eeOn.mp3';