diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index f63e9816..34d84534 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -39,6 +39,7 @@ "Download debug logs": "Download debug logs", "Element Call Home": "Element Call Home", "Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call is temporarily not end-to-end encrypted while we test scalability.", + "Enable end-to-end encryption (password protected calls)": "Enable end-to-end encryption (password protected calls)", "Exit full screen": "Exit full screen", "Expose developer settings in the settings window.": "Expose developer settings in the settings window.", "Feedback": "Feedback", @@ -73,6 +74,7 @@ "Not registered yet? <2>Create an account": "Not registered yet? <2>Create an account", "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}": "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}", "Password": "Password", + "Password (if none, E2EE is disabled)": "Password (if none, E2EE is disabled)", "Passwords must match": "Passwords must match", "Profile": "Profile", "Recaptcha dismissed": "Recaptcha dismissed", diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index adb5c72c..0421301d 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -1,6 +1,13 @@ -import { Room, RoomOptions, setLogLevel } from "livekit-client"; +import { + E2EEOptions, + ExternalE2EEKeyProvider, + Room, + RoomOptions, + setLogLevel, +} from "livekit-client"; import { useLiveKitRoom } from "@livekit/components-react"; -import { useMemo } from "react"; +import { useEffect, useMemo } from "react"; +import E2EEWorker from "livekit-client/e2ee-worker?worker"; import { defaultLiveKitOptions } from "./options"; import { SFUConfig } from "./openIDSFU"; @@ -15,12 +22,34 @@ export type DeviceChoices = { enabled: boolean; }; +export type E2EEConfig = { + sharedKey: string; +}; + setLogLevel("debug"); export function useLiveKit( userChoices: UserChoices, - sfuConfig?: SFUConfig + sfuConfig?: SFUConfig, + e2eeConfig?: E2EEConfig ): Room | undefined { + const e2eeOptions = useMemo(() => { + if (!e2eeConfig?.sharedKey) return undefined; + + return { + keyProvider: new ExternalE2EEKeyProvider(), + worker: new E2EEWorker(), + } as E2EEOptions; + }, [e2eeConfig]); + + useEffect(() => { + if (!e2eeConfig?.sharedKey || !e2eeOptions) return; + + (e2eeOptions.keyProvider as ExternalE2EEKeyProvider).setKey( + e2eeConfig?.sharedKey + ); + }, [e2eeOptions, e2eeConfig?.sharedKey]); + const roomOptions = useMemo((): RoomOptions => { const options = defaultLiveKitOptions; options.videoCaptureDefaults = { @@ -31,15 +60,22 @@ export function useLiveKit( ...options.audioCaptureDefaults, deviceId: userChoices.audio?.selectedId, }; - return options; - }, [userChoices.video, userChoices.audio]); + options.e2ee = e2eeOptions; + + return options; + }, [userChoices.video, userChoices.audio, e2eeOptions]); + + // We have to create the room manually here due to a bug inside + // @livekit/components-react. JSON.stringify() is used in deps of a + // useEffect() with an argument that references itself, if E2EE is enabled + const roomWithoutProps = useMemo(() => new Room(roomOptions), [roomOptions]); const { room } = useLiveKitRoom({ token: sfuConfig?.jwt, serverUrl: sfuConfig?.url, audio: userChoices.audio?.enabled ?? false, video: userChoices.video?.enabled ?? false, - options: roomOptions, + room: roomWithoutProps, }); return room; diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index d47cbb31..63c73474 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -32,10 +32,11 @@ import { CallEndedView } from "./CallEndedView"; import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { useProfile } from "../profile/useProfile"; -import { UserChoices } from "../livekit/useLiveKit"; +import { E2EEConfig, UserChoices } from "../livekit/useLiveKit"; import { findDeviceByName } from "../media-utils"; import { OpenIDLoader } from "../livekit/OpenIDLoader"; import { ActiveCall } from "./InCallView"; +import { Config } from "../config/Config"; /** * If there already is this many participants in the call, we automatically mute @@ -228,6 +229,9 @@ export function GroupCallView({ const [userChoices, setUserChoices] = useState( undefined ); + const [e2eeConfig, setE2EEConfig] = useState( + undefined + ); const onReconnect = useCallback(() => { setLeft(false); @@ -235,7 +239,11 @@ export function GroupCallView({ groupCall.enter(); }, [groupCall]); - console.log("LOG participant size", participants.size); + const livekitServiceURL = + groupCall.livekitServiceURL ?? Config.get().livekit?.livekit_service_url; + if (!livekitServiceURL) { + return ; + } if (error) { return ; @@ -254,6 +262,7 @@ export function GroupCallView({ unencryptedEventsFromUsers={unencryptedEventsFromUsers} hideHeader={hideHeader} userChoices={userChoices} + e2eeConfig={e2eeConfig} otelGroupCallMembership={otelGroupCallMembership} /> @@ -297,8 +306,9 @@ export function GroupCallView({ return ( { + onEnter={(choices: UserChoices, e2eeConfig?: E2EEConfig) => { setUserChoices(choices); + setE2EEConfig(e2eeConfig); enter(); }} initWithMutedAudio={participants.size > MUTE_PARTICIPANT_COUNT} diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 863ea6a8..ee3e893a 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -78,7 +78,7 @@ import { SettingsModal } from "../settings/SettingsModal"; import { InviteModal } from "./InviteModal"; import { useRageshakeRequestModal } from "../settings/submit-rageshake"; import { RageshakeRequestModal } from "./RageshakeRequestModal"; -import { UserChoices, useLiveKit } from "../livekit/useLiveKit"; +import { E2EEConfig, UserChoices, useLiveKit } from "../livekit/useLiveKit"; import { useMediaDevicesSwitcher } from "../livekit/useMediaDevicesSwitcher"; import { useFullscreen } from "./useFullscreen"; import { useLayoutStates } from "../video-grid/Layout"; @@ -95,16 +95,25 @@ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); export interface ActiveCallProps extends Omit { userChoices: UserChoices; + e2eeConfig?: E2EEConfig; } export function ActiveCall(props: ActiveCallProps) { const sfuConfig = useSFUConfig(); - const livekitRoom = useLiveKit(props.userChoices, sfuConfig); + const livekitRoom = useLiveKit( + props.userChoices, + sfuConfig, + props.e2eeConfig + ); if (!livekitRoom) { return null; } + if (props.e2eeConfig && !livekitRoom.isE2EEEnabled) { + livekitRoom.setE2EEEnabled(!!props.e2eeConfig); + } + return ( diff --git a/src/room/LobbyView.module.css b/src/room/LobbyView.module.css index 55b7b5b0..3bb0162e 100644 --- a/src/room/LobbyView.module.css +++ b/src/room/LobbyView.module.css @@ -66,3 +66,9 @@ limitations under the License. .copyButton:last-child { margin-bottom: 0; } + +.passwordField { + width: 320px !important; + margin-bottom: 20px; + flex: 0; +} diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index c97cc3ca..bfd437e8 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useRef, useEffect, useState } from "react"; +import { useRef, useEffect, useState, useCallback, ChangeEvent } from "react"; import { Trans, useTranslation } from "react-i18next"; import styles from "./LobbyView.module.css"; @@ -25,12 +25,14 @@ import { UserMenuContainer } from "../UserMenuContainer"; import { Body, Link } from "../typography/Typography"; import { useLocationNavigation } from "../useLocationNavigation"; import { MatrixInfo, VideoPreview } from "./VideoPreview"; -import { UserChoices } from "../livekit/useLiveKit"; +import { E2EEConfig, UserChoices } from "../livekit/useLiveKit"; +import { InputField } from "../input/Input"; +import { useEnableE2EE } from "../settings/useSetting"; interface Props { matrixInfo: MatrixInfo; - onEnter: (userChoices: UserChoices) => void; + onEnter: (userChoices: UserChoices, e2eeConfig?: E2EEConfig) => void; isEmbedded: boolean; hideHeader: boolean; initWithMutedAudio: boolean; @@ -40,6 +42,8 @@ export function LobbyView(props: Props) { const { t } = useTranslation(); useLocationNavigation(); + const [enableE2EE] = useEnableE2EE(); + const joinCallButtonRef = useRef(null); useEffect(() => { if (joinCallButtonRef.current) { @@ -50,6 +54,17 @@ export function LobbyView(props: Props) { const [userChoices, setUserChoices] = useState( undefined ); + const [e2eeSharedKey, setE2EESharedKey] = useState( + undefined + ); + + const onE2EESharedKeyChanged = useCallback( + (event: ChangeEvent) => { + const value = event.target.value; + setE2EESharedKey(value === "" ? undefined : value); + }, + [setE2EESharedKey] + ); return (
@@ -70,12 +85,26 @@ export function LobbyView(props: Props) { initWithMutedAudio={props.initWithMutedAudio} onUserChoicesChanged={setUserChoices} /> + {enableE2EE && ( + + )} diff --git a/src/settings/useSetting.ts b/src/settings/useSetting.ts index 63f1e23a..8d3f074f 100644 --- a/src/settings/useSetting.ts +++ b/src/settings/useSetting.ts @@ -104,6 +104,9 @@ export const useDeveloperSettingsTab = () => export const useShowConnectionStats = () => useSetting("show-connection-stats", false); +export const useEnableE2EE = () => + useSetting("enable-end-to-end-encryption", false); + export const useDefaultDevices = () => useSetting("defaultDevices", { audioinput: "",