diff --git a/package.json b/package.json index 3d3d938b..6a5a47a1 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "i18next-http-backend": "^1.4.4", "livekit-client": "1.12.0", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#d79d9ae69c3220c02406706d4a1ec52c22c44fbd", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#b698217445318f453e0b1086364a33113eaa85d9", "matrix-widget-api": "^1.3.1", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index 5fe1ffa8..41604e6d 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -38,6 +38,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", @@ -72,6 +73,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/options.ts b/src/livekit/options.ts index e808149b..5115f213 100644 --- a/src/livekit/options.ts +++ b/src/livekit/options.ts @@ -6,9 +6,7 @@ import { TrackPublishDefaults, VideoPreset, VideoPresets, - ExternalE2EEKeyProvider, } from "livekit-client"; -import E2EEWorker from "livekit-client/e2ee-worker?worker"; const defaultLiveKitPublishOptions: TrackPublishDefaults = { audioPreset: AudioPresets.music, @@ -24,16 +22,7 @@ const defaultLiveKitPublishOptions: TrackPublishDefaults = { backupCodec: { codec: "vp8", encoding: VideoPresets.h720.encoding }, } as const; -const e2eeWorker = new E2EEWorker(); -const e2eeKeyProvider = new ExternalE2EEKeyProvider(); -e2eeKeyProvider.setKey("not secret password"); - export const defaultLiveKitOptions: RoomOptions = { - e2ee: { - keyProvider: e2eeKeyProvider, - worker: e2eeWorker, - }, - // automatically manage subscribed video quality adaptiveStream: true, diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index 6b862a66..aa8d1e42 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -1,6 +1,12 @@ -import { Room, RoomOptions } from "livekit-client"; +import { + E2EEOptions, + ExternalE2EEKeyProvider, + Room, + RoomOptions, +} 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,10 +21,32 @@ export type DeviceChoices = { enabled: boolean; }; +export type E2EEConfig = { + sharedKey: string; +}; + 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 = { @@ -29,8 +57,11 @@ export function useLiveKit( ...options.audioCaptureDefaults, deviceId: userChoices.audio?.selectedId, }; + + options.e2ee = e2eeOptions; + return options; - }, [userChoices.video, userChoices.audio]); + }, [userChoices.video, userChoices.audio, e2eeOptions]); const roomWithoutProps = useMemo(() => new Room(roomOptions), [roomOptions]); diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 9afc2ec3..1ec959bc 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -32,7 +32,7 @@ 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"; @@ -218,10 +218,12 @@ export function GroupCallView({ const [userChoices, setUserChoices] = useState( undefined ); + const [e2eeConfig, setE2EEConfig] = useState( + undefined + ); const livekitServiceURL = - groupCall.foci[0]?.livekitServiceUrl ?? - Config.get().livekit?.livekit_service_url; + groupCall.livekitServiceURL ?? Config.get().livekit?.livekit_service_url; if (!livekitServiceURL) { return ; } @@ -243,6 +245,7 @@ export function GroupCallView({ unencryptedEventsFromUsers={unencryptedEventsFromUsers} hideHeader={hideHeader} userChoices={userChoices} + e2eeConfig={e2eeConfig} otelGroupCallMembership={otelGroupCallMembership} /> @@ -283,8 +286,9 @@ export function GroupCallView({ return ( { + onEnter={(choices: UserChoices, e2eeConfig?: E2EEConfig) => { setUserChoices(choices); + setE2EEConfig(e2eeConfig); enter(); }} isEmbedded={isEmbedded} diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 0140061d..0889a883 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -77,7 +77,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"; @@ -92,11 +92,16 @@ 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; 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 0f07ae7d..99c4f21a 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; } @@ -39,6 +41,8 @@ export function LobbyView(props: Props) { const { t } = useTranslation(); useLocationNavigation(); + const [enableE2EE] = useEnableE2EE(); + const joinCallButtonRef = useRef(null); useEffect(() => { if (joinCallButtonRef.current) { @@ -49,6 +53,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 (
@@ -68,12 +83,26 @@ export function LobbyView(props: Props) { matrixInfo={props.matrixInfo} 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: "", diff --git a/src/widget.ts b/src/widget.ts index a7ea1b0f..93107fcd 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -164,7 +164,7 @@ export const widget: WidgetHelpers | null = (() => { // message so that we can use the widget API in less racy mode, but we need to change // element-web to use waitForIFrameLoad=false. Once that change has rolled out, // we can just start the client after we've fetched the config. - foci: [], + livekitServiceURL: undefined, } ); @@ -178,9 +178,7 @@ export const widget: WidgetHelpers | null = (() => { // Now we've fetched the config, be evil and use the getter to inject the focus // into the client (see above XXX). if (focus) { - client.getFoci().push({ - livekitServiceUrl: livekit.livekit_service_url, - }); + client.setLivekitServiceURL(livekit.livekit_service_url); } await client.startClient(); resolve(client); diff --git a/yarn.lock b/yarn.lock index 7073bdf9..a9f1980d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11012,9 +11012,9 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#d79d9ae69c3220c02406706d4a1ec52c22c44fbd": +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#b698217445318f453e0b1086364a33113eaa85d9": version "26.2.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d79d9ae69c3220c02406706d4a1ec52c22c44fbd" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/b698217445318f453e0b1086364a33113eaa85d9" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-js" "^0.1.1"