From 8946af8f4e5e86b94198f39a1fe7f4270dace997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 12 Jul 2023 17:50:07 +0200 Subject: [PATCH 01/10] Hack e2ee in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/livekit/options.ts | 11 +++++++++++ src/livekit/useLiveKit.ts | 4 +++- src/room/InCallView.tsx | 2 ++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/livekit/options.ts b/src/livekit/options.ts index 5115f213..e808149b 100644 --- a/src/livekit/options.ts +++ b/src/livekit/options.ts @@ -6,7 +6,9 @@ import { TrackPublishDefaults, VideoPreset, VideoPresets, + ExternalE2EEKeyProvider, } from "livekit-client"; +import E2EEWorker from "livekit-client/e2ee-worker?worker"; const defaultLiveKitPublishOptions: TrackPublishDefaults = { audioPreset: AudioPresets.music, @@ -22,7 +24,16 @@ 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 7a0a5330..6b862a66 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -32,12 +32,14 @@ export function useLiveKit( return options; }, [userChoices.video, userChoices.audio]); + 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/InCallView.tsx b/src/room/InCallView.tsx index b8284975..0140061d 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -102,6 +102,8 @@ export function ActiveCall(props: ActiveCallProps) { return null; } + livekitRoom.setE2EEEnabled(true); + return ( From 4193629c2ce0ed4eb84ffb09995af504db95dd91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 17 Jul 2023 16:53:58 +0200 Subject: [PATCH 02/10] Add E2EE password prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/livekit/options.ts | 11 ---------- src/livekit/useLiveKit.ts | 39 +++++++++++++++++++++++++++++++---- src/room/GroupCallView.tsx | 12 +++++++---- src/room/InCallView.tsx | 9 ++++++-- src/room/LobbyView.module.css | 6 ++++++ src/room/LobbyView.tsx | 32 ++++++++++++++++++++++++---- 6 files changed, 84 insertions(+), 25 deletions(-) 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..e52835ae 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,13 @@ 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"; interface Props { matrixInfo: MatrixInfo; - onEnter: (userChoices: UserChoices) => void; + onEnter: (userChoices: UserChoices, e2eeConfig?: E2EEConfig) => void; isEmbedded: boolean; hideHeader: boolean; } @@ -49,6 +50,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 +80,24 @@ export function LobbyView(props: Props) { matrixInfo={props.matrixInfo} onUserChoicesChanged={setUserChoices} /> + 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: "", From 9abd409a9affc1ebc8983cdfa4140eeee6e7ff44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 25 Jul 2023 11:07:30 +0200 Subject: [PATCH 06/10] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- public/locales/en-GB/app.json | 1 + 1 file changed, 1 insertion(+) diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index 5425fe69..4fcdc1b5 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", From 7ddede4feeb9bb3f2b737bc092d8c583b5c852d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 25 Jul 2023 13:03:31 +0200 Subject: [PATCH 07/10] Update string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- public/locales/en-GB/app.json | 2 +- src/room/LobbyView.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index 4fcdc1b5..41604e6d 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -73,7 +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)", + "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/room/LobbyView.tsx b/src/room/LobbyView.tsx index 258b474f..99c4f21a 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -86,7 +86,7 @@ export function LobbyView(props: Props) { {enableE2EE && ( Date: Tue, 25 Jul 2023 15:49:59 +0200 Subject: [PATCH 08/10] Add a comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/livekit/useLiveKit.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index aa8d1e42..37fcffbb 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -63,8 +63,10 @@ export function useLiveKit( 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, From a1e18322df30073d05d8dca7ac607096477d4992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 25 Jul 2023 16:13:45 +0200 Subject: [PATCH 09/10] Missing import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/room/GroupCallView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index a75ed451..63c73474 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -36,6 +36,7 @@ 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 From 926eb8adbff8867957874c71d59d3d2864fd416d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 25 Jul 2023 16:40:12 +0200 Subject: [PATCH 10/10] Fix e2ee bugginess MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/room/InCallView.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 60f3e0f1..ee3e893a 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -110,7 +110,9 @@ export function ActiveCall(props: ActiveCallProps) { return null; } - livekitRoom.setE2EEEnabled(true); + if (props.e2eeConfig && !livekitRoom.isE2EEEnabled) { + livekitRoom.setE2EEEnabled(!!props.e2eeConfig); + } return (