diff --git a/src/E2EEBanner.tsx b/src/E2EEBanner.tsx index 774f3582..28b9e349 100644 --- a/src/E2EEBanner.tsx +++ b/src/E2EEBanner.tsx @@ -19,10 +19,10 @@ import { Trans } from "react-i18next"; import { Banner } from "./Banner"; import styles from "./E2EEBanner.module.css"; import { ReactComponent as LockOffIcon } from "./icons/LockOff.svg"; -import { useEnableE2EE } from "./settings/useSetting"; +import { useEnableE2EE } from "./e2ee/sharedKeyManagement"; export const E2EEBanner = () => { - const [e2eeEnabled] = useEnableE2EE(); + const e2eeEnabled = useEnableE2EE(); if (e2eeEnabled) return null; return ( diff --git a/src/UrlParams.ts b/src/UrlParams.ts index e6f0cd05..0ba9e049 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -92,6 +92,10 @@ interface UrlParams { * E2EE password */ password: string | null; + /** + * Whether we the app should use per participant keys for E2EE. + */ + perParticipantE2EE: boolean; } /** @@ -190,6 +194,7 @@ export const getUrlParams = ( fontScale: Number.isNaN(fontScale) ? null : fontScale, analyticsID: getParam("analyticsID"), allowIceFallback: hasParam("allowIceFallback"), + perParticipantE2EE: hasParam("perParticipantE2EE"), }; }; diff --git a/src/e2ee/matrixKeyProvider.ts b/src/e2ee/matrixKeyProvider.ts index 2d0bc1a8..532cb0e4 100644 --- a/src/e2ee/matrixKeyProvider.ts +++ b/src/e2ee/matrixKeyProvider.ts @@ -15,11 +15,7 @@ limitations under the License. */ import { logger } from "@sentry/utils"; -import { - BaseKeyProvider, - KeyProviderOptions, - createKeyMaterialFromString, -} from "livekit-client"; +import { BaseKeyProvider, createKeyMaterialFromString } from "livekit-client"; import { CallMembership } from "matrix-js-sdk/src/matrixrtc/CallMembership"; import { MatrixRTCSession, @@ -27,19 +23,29 @@ import { } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; export class MatrixKeyProvider extends BaseKeyProvider { - constructor( - private rtcSession: MatrixRTCSession, - keyProviderOptions: Partial = {} - ) { - super(keyProviderOptions); + private rtcSession?: MatrixRTCSession; - const encryptionKey = this.rtcSession.activeEncryptionKey; + public setRTCSession(rtcSession: MatrixRTCSession) { + const encryptionKey = rtcSession.activeEncryptionKey; if (!encryptionKey) { throw new Error( "MatrixKeyProvider requires the given MatrixRTCSession to have an activeEncryptionKey" ); } + if (this.rtcSession) { + this.rtcSession.off( + MatrixRTCSessionEvent.MembershipsChanged, + this.onMemberShipsChanged + ); + this.rtcSession.off( + MatrixRTCSessionEvent.ActiveEncryptionKeyChanged, + this.onEncryptionKeyChanged + ); + } + + this.rtcSession = rtcSession; + this.rtcSession.on( MatrixRTCSessionEvent.MembershipsChanged, this.onMemberShipsChanged diff --git a/src/e2ee/sharedKeyManagement.ts b/src/e2ee/sharedKeyManagement.ts index 83eca2ea..9fb08708 100644 --- a/src/e2ee/sharedKeyManagement.ts +++ b/src/e2ee/sharedKeyManagement.ts @@ -15,8 +15,9 @@ limitations under the License. */ import { useEffect, useMemo } from "react"; +import { isE2EESupported } from "livekit-client"; -import { useEnableE2EE } from "../settings/useSetting"; +import { useEnableSPAE2EE } from "../settings/useSetting"; import { useLocalStorage } from "../useLocalStorage"; import { useClient } from "../ClientContext"; import { PASSWORD_STRING, useUrlParams } from "../UrlParams"; @@ -28,7 +29,7 @@ export const useInternalRoomSharedKey = ( roomId: string ): [string | null, (value: string) => void] => { const key = useMemo(() => getRoomSharedKeyLocalStorageKey(roomId), [roomId]); - const [e2eeEnabled] = useEnableE2EE(); + const [e2eeEnabled] = useEnableSPAE2EE(); const [roomSharedKey, setRoomSharedKey] = useLocalStorage(key); return [e2eeEnabled ? roomSharedKey : null, setRoomSharedKey]; @@ -67,7 +68,7 @@ export const useManageRoomSharedKey = (roomId: string): string | null => { }; export const useIsRoomE2EE = (roomId: string): boolean | null => { - const { isEmbedded } = useUrlParams(); + const { isEmbedded, perParticipantE2EE } = useUrlParams(); const client = useClient(); const room = useMemo( () => client.client?.getRoom(roomId) ?? null, @@ -75,11 +76,32 @@ export const useIsRoomE2EE = (roomId: string): boolean | null => { ); const isE2EE = useMemo(() => { if (isEmbedded) { - return false; + return perParticipantE2EE; } else { return room ? !room?.getCanonicalAlias() : null; } - }, [isEmbedded, room]); + }, [room, isEmbedded, perParticipantE2EE]); return isE2EE; }; + +export const useEnableEmbeddedE2EE = (): boolean => { + const { isEmbedded, perParticipantE2EE } = useUrlParams(); + + if (!isEmbedded) return false; + if (!isE2EESupported()) return false; + + return perParticipantE2EE; +}; + +export const useEnableE2EE = (): boolean => { + const [spaE2EEEnabled] = useEnableSPAE2EE(); + const embeddedE2EEEnabled = useEnableEmbeddedE2EE(); + + const e2eeEnabled = useMemo( + () => spaE2EEEnabled || embeddedE2EEEnabled, + [spaE2EEEnabled, embeddedE2EEEnabled] + ); + + return e2eeEnabled; +}; diff --git a/src/home/RegisteredView.tsx b/src/home/RegisteredView.tsx index 42aba9e8..511b89f7 100644 --- a/src/home/RegisteredView.tsx +++ b/src/home/RegisteredView.tsx @@ -38,7 +38,7 @@ import { JoinExistingCallModal } from "./JoinExistingCallModal"; import { Caption, Title } from "../typography/Typography"; import { Form } from "../form/Form"; import { CallType, CallTypeDropdown } from "./CallTypeDropdown"; -import { useEnableE2EE, useOptInAnalytics } from "../settings/useSetting"; +import { useEnableSPAE2EE, useOptInAnalytics } from "../settings/useSetting"; import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; import { E2EEBanner } from "../E2EEBanner"; import { setLocalStorageItem } from "../useLocalStorage"; @@ -57,7 +57,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) { const history = useHistory(); const { t } = useTranslation(); const { modalState, modalProps } = useModalTriggerState(); - const [e2eeEnabled] = useEnableE2EE(); + const [e2eeEnabled] = useEnableSPAE2EE(); const onSubmit: FormEventHandler = useCallback( (e: FormEvent) => { diff --git a/src/home/UnauthenticatedView.tsx b/src/home/UnauthenticatedView.tsx index 721c56d1..c0099b6c 100644 --- a/src/home/UnauthenticatedView.tsx +++ b/src/home/UnauthenticatedView.tsx @@ -40,7 +40,7 @@ import styles from "./UnauthenticatedView.module.css"; import commonStyles from "./common.module.css"; import { generateRandomName } from "../auth/generateRandomName"; import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; -import { useEnableE2EE, useOptInAnalytics } from "../settings/useSetting"; +import { useEnableSPAE2EE, useOptInAnalytics } from "../settings/useSetting"; import { Config } from "../config/Config"; import { E2EEBanner } from "../E2EEBanner"; import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement"; @@ -60,7 +60,7 @@ export const UnauthenticatedView: FC = () => { const history = useHistory(); const { t } = useTranslation(); - const [e2eeEnabled] = useEnableE2EE(); + const [e2eeEnabled] = useEnableSPAE2EE(); const onSubmit: FormEventHandler = useCallback( (e) => { diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index 5adccee1..337872fd 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -16,7 +16,7 @@ limitations under the License. import { ConnectionState, - E2EEOptions, + ExternalE2EEKeyProvider, Room, RoomOptions, setLogLevel, @@ -42,8 +42,14 @@ import { } from "./useECConnectionState"; import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider"; +export enum E2EEMode { + PerParticipantKey = "per_participant_key", + SharedKey = "shared_key", +} + export type E2EEConfig = { - sharedKey: string; + mode: E2EEMode; + sharedKey?: string; }; setLogLevel("debug"); @@ -60,21 +66,33 @@ export function useLiveKit( e2eeConfig?: E2EEConfig ): UseLivekitResult { const e2eeOptions = useMemo(() => { - if (!e2eeConfig?.sharedKey) return undefined; + if (!e2eeConfig) return undefined; - return { - keyProvider: new ExternalE2EEKeyProvider(), - worker: new E2EEWorker(), - } as E2EEOptions; + if (e2eeConfig.mode === E2EEMode.PerParticipantKey) { + return { + keyProvider: new MatrixKeyProvider(), + worker: new E2EEWorker(), + }; + } else if (e2eeConfig.mode === E2EEMode.SharedKey && e2eeConfig.sharedKey) { + return { + keyProvider: new ExternalE2EEKeyProvider(), + worker: new E2EEWorker(), + }; + } }, [e2eeConfig]); useEffect(() => { - if (!e2eeConfig?.sharedKey || !e2eeOptions) return; + if (!e2eeOptions) return; + if (!e2eeConfig) return; - (e2eeOptions.keyProvider as ExternalE2EEKeyProvider).setKey( - e2eeConfig?.sharedKey - ); - }, [e2eeOptions, e2eeConfig?.sharedKey]); + if (e2eeConfig.mode === E2EEMode.PerParticipantKey) { + (e2eeOptions.keyProvider as MatrixKeyProvider).setRTCSession(rtcSession); + } else if (e2eeConfig.mode === E2EEMode.SharedKey && e2eeConfig.sharedKey) { + (e2eeOptions.keyProvider as ExternalE2EEKeyProvider).setKey( + e2eeConfig.sharedKey + ); + } + }, [e2eeOptions, e2eeConfig, rtcSession]); const initialMuteStates = useRef(muteStates); const devices = useMediaDevices(); diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 57182b04..bef42b01 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -40,8 +40,12 @@ import { useMatrixRTCSessionJoinState } from "../useMatrixRTCSessionJoinState"; import { useManageRoomSharedKey, useIsRoomE2EE, + useEnableEmbeddedE2EE, + useEnableE2EE, } from "../e2ee/sharedKeyManagement"; -import { useEnableE2EE } from "../settings/useSetting"; +import { useEnableSPAE2EE } from "../settings/useSetting"; +import { E2EEConfig, E2EEMode } from "../livekit/useLiveKit"; +import { useUrlParams } from "../UrlParams"; declare global { interface Window { @@ -71,6 +75,10 @@ export function GroupCallView({ const e2eeSharedKey = useManageRoomSharedKey(rtcSession.room.roomId); const isRoomE2EE = useIsRoomE2EE(rtcSession.room.roomId); + const [spaE2EEEnabled] = useEnableSPAE2EE(); + const embeddedE2EEEnabled = useEnableEmbeddedE2EE(); + const e2eeEnabled = useEnableE2EE(); + const { perParticipantE2EE } = useUrlParams(); const { t } = useTranslation(); @@ -153,7 +161,7 @@ export function GroupCallView({ } } - enterRTCSession(rtcSession); + enterRTCSession(rtcSession, embeddedE2EEEnabled); PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); // we only have room sessions right now, so call ID is the emprty string - we use the room ID @@ -172,18 +180,18 @@ export function GroupCallView({ widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin); }; } - }, [rtcSession, preload]); + }, [rtcSession, preload, embeddedE2EEEnabled]); useEffect(() => { if (isEmbedded && !preload) { // In embedded mode, bypass the lobby and just enter the call straight away - enterRTCSession(rtcSession); + enterRTCSession(rtcSession, embeddedE2EEEnabled); PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); // use the room ID as above PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId); } - }, [rtcSession, isEmbedded, preload]); + }, [rtcSession, isEmbedded, preload, embeddedE2EEEnabled]); const [left, setLeft] = useState(false); const [leaveError, setLeaveError] = useState(undefined); @@ -237,20 +245,21 @@ export function GroupCallView({ } }, [isJoined, rtcSession]); - const [e2eeEnabled] = useEnableE2EE(); - - const e2eeConfig = useMemo( - () => (e2eeSharedKey ? { sharedKey: e2eeSharedKey } : undefined), - [e2eeSharedKey] - ); + const e2eeConfig = useMemo((): E2EEConfig | undefined => { + if (perParticipantE2EE) { + return { mode: E2EEMode.PerParticipantKey }; + } else if (e2eeSharedKey) { + return { mode: E2EEMode.SharedKey, sharedKey: e2eeSharedKey }; + } + }, [perParticipantE2EE, e2eeSharedKey]); const onReconnect = useCallback(() => { setLeft(false); setLeaveError(undefined); - enterRTCSession(rtcSession); - }, [rtcSession]); + enterRTCSession(rtcSession, embeddedE2EEEnabled); + }, [rtcSession, embeddedE2EEEnabled]); - if (e2eeEnabled && isRoomE2EE && !e2eeSharedKey) { + if (spaE2EEEnabled && isRoomE2EE && !e2eeSharedKey) { return ( enterRTCSession(rtcSession)} + onEnter={() => enterRTCSession(rtcSession, embeddedE2EEEnabled)} isEmbedded={isEmbedded} hideHeader={hideHeader} /> diff --git a/src/room/useLoadGroupCall.ts b/src/room/useLoadGroupCall.ts index 9218bf32..23f068a4 100644 --- a/src/room/useLoadGroupCall.ts +++ b/src/room/useLoadGroupCall.ts @@ -26,7 +26,7 @@ import type { Room } from "matrix-js-sdk/src/models/room"; import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; import { setLocalStorageItem } from "../useLocalStorage"; import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils"; -import { useEnableE2EE } from "../settings/useSetting"; +import { useEnableSPAE2EE } from "../settings/useSetting"; import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement"; export type GroupCallLoaded = { @@ -62,7 +62,7 @@ export const useLoadGroupCall = ( const { t } = useTranslation(); const [state, setState] = useState({ kind: "loading" }); - const [e2eeEnabled] = useEnableE2EE(); + const [e2eeEnabled] = useEnableSPAE2EE(); useEffect(() => { const fetchOrCreateRoom = async (): Promise => { diff --git a/src/rtcSessionHelpers.ts b/src/rtcSessionHelpers.ts index 3d62f980..9cfcdcd2 100644 --- a/src/rtcSessionHelpers.ts +++ b/src/rtcSessionHelpers.ts @@ -33,7 +33,10 @@ function makeFocus(livekitAlias: string): LivekitFocus { }; } -export function enterRTCSession(rtcSession: MatrixRTCSession) { +export function enterRTCSession( + rtcSession: MatrixRTCSession, + encryptMedia: boolean +) { PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId); @@ -44,7 +47,7 @@ export function enterRTCSession(rtcSession: MatrixRTCSession) { // right now we asume everything is a room-scoped call const livekitAlias = rtcSession.room.roomId; - rtcSession.joinRoomSession([makeFocus(livekitAlias)]); + rtcSession.joinRoomSession([makeFocus(livekitAlias)], encryptMedia); } export function leaveRTCSession(rtcSession: MatrixRTCSession) { diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 0d4983ed..50128370 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -34,7 +34,7 @@ import { useOptInAnalytics, useDeveloperSettingsTab, useShowConnectionStats, - useEnableE2EE, + useEnableSPAE2EE, } from "./useSetting"; import { FieldRow, InputField } from "../input/Input"; import { Button } from "../button"; @@ -69,7 +69,7 @@ export const SettingsModal = (props: Props) => { useDeveloperSettingsTab(); const [showConnectionStats, setShowConnectionStats] = useShowConnectionStats(); - const [enableE2EE, setEnableE2EE] = useEnableE2EE(); + const [enableE2EE, setEnableE2EE] = useEnableSPAE2EE(); const downloadDebugLog = useDownloadDebugLog(); diff --git a/src/settings/useSetting.ts b/src/settings/useSetting.ts index 5d86a4b6..344f0d42 100644 --- a/src/settings/useSetting.ts +++ b/src/settings/useSetting.ts @@ -89,14 +89,15 @@ export const useOptInAnalytics = (): DisableableSetting => { return [false, null]; }; -export const useEnableE2EE = (): DisableableSetting => { +export const useEnableSPAE2EE = (): DisableableSetting => { const settingVal = useSetting( "enable-end-to-end-encryption", false ); - if (isE2EESupported()) return settingVal; - return [false, null]; + if (!isE2EESupported()) return [false, null]; + + return settingVal; }; export const useDeveloperSettingsTab = () => diff --git a/src/widget.ts b/src/widget.ts index 324dbe4a..b948f575 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -79,6 +79,8 @@ export const widget: WidgetHelpers | null = (() => { logger.info("Widget API is available"); const api = new WidgetApi(widgetId, parentOrigin); api.requestCapability(MatrixCapabilities.AlwaysOnScreen); + api.requestCapabilityToSendEvent(EventType.CallEncryptionPrefix); + api.requestCapabilityToReceiveEvent(EventType.CallEncryptionPrefix); // Set up the lazy action emitter, but only for select actions that we // intend for the app to handle