diff --git a/src/e2ee/sharedKeyManagement.ts b/src/e2ee/sharedKeyManagement.ts index 2f46c532..967ab014 100644 --- a/src/e2ee/sharedKeyManagement.ts +++ b/src/e2ee/sharedKeyManagement.ts @@ -15,12 +15,11 @@ limitations under the License. */ import { useEffect, useMemo } from "react"; -import { Room } from "matrix-js-sdk"; import { setLocalStorageItem, useLocalStorage } from "../useLocalStorage"; -import { useClient } from "../ClientContext"; import { UrlParams, getUrlParams, useUrlParams } from "../UrlParams"; -import { widget } from "../widget"; +import { E2eeType } from "./e2eeType"; +import { useClient } from "../ClientContext"; export function saveKeyForRoom(roomId: string, password: string): void { setLocalStorageItem(getRoomSharedKeyLocalStorageKey(roomId), password); @@ -68,30 +67,37 @@ const useKeyFromUrl = (): [string, string] | [undefined, undefined] => { : [undefined, undefined]; }; -export const useRoomSharedKey = (roomId: string): string | undefined => { +export type Unencrypted = { kind: E2eeType.NONE }; +export type SharedSecret = { kind: E2eeType.SHARED_KEY; secret: string }; +export type PerParticipantE2EE = { kind: E2eeType.PER_PARTICIPANT }; +export type EncryptionSystem = Unencrypted | SharedSecret | PerParticipantE2EE; + +export function useRoomEncryptionSystem(roomId: string): EncryptionSystem { + const { client } = useClient(); + // make sure we've extracted the key from the URL first // (and we still need to take the value it returns because // the effect won't run in time for it to save to localstorage in // time for us to read it out again). const [urlRoomId, passwordFormUrl] = useKeyFromUrl(); - const storedPassword = useInternalRoomSharedKey(roomId); - - if (storedPassword) return storedPassword; - if (urlRoomId === roomId) return passwordFormUrl; - return undefined; -}; - -export const useIsRoomE2EE = (roomId: string): boolean | null => { - const { client } = useClient(); - const room = useMemo(() => client?.getRoom(roomId), [roomId, client]); - - return useMemo(() => !room || isRoomE2EE(room), [room]); -}; - -export function isRoomE2EE(room: Room): boolean { - // For now, rooms in widget mode are never considered encrypted. - // In the future, when widget mode gains encryption support, then perhaps we - // should inspect the e2eEnabled URL parameter here? - return widget === null && !room.getCanonicalAlias(); + const room = client?.getRoom(roomId); + const e2eeSystem = useMemo(() => { + if (!room) return { kind: E2eeType.NONE } as Unencrypted; + if (storedPassword) + return { + kind: E2eeType.SHARED_KEY, + secret: storedPassword, + } as SharedSecret; + if (urlRoomId === roomId) + return { + kind: E2eeType.SHARED_KEY, + secret: passwordFormUrl, + } as SharedSecret; + if (room.hasEncryptionStateEvent()) { + return { kind: E2eeType.PER_PARTICIPANT } as PerParticipantE2EE; + } + return { kind: E2eeType.NONE } as EncryptionSystem; + }, [passwordFormUrl, room, roomId, storedPassword, urlRoomId]); + return e2eeSystem; } diff --git a/src/home/RegisteredView.tsx b/src/home/RegisteredView.tsx index 8024467f..35e958ab 100644 --- a/src/home/RegisteredView.tsx +++ b/src/home/RegisteredView.tsx @@ -78,12 +78,14 @@ export const RegisteredView: FC = ({ client }) => { roomName, E2eeType.SHARED_KEY, ); + if (!createRoomResult.password) + throw new Error("Failed to create room with shared secret"); history.push( getRelativeRoomUrl( createRoomResult.roomId, + { kind: E2eeType.SHARED_KEY, secret: createRoomResult.password }, roomName, - createRoomResult.password, ), ); } diff --git a/src/home/UnauthenticatedView.tsx b/src/home/UnauthenticatedView.tsx index c580bcd8..d5f00fea 100644 --- a/src/home/UnauthenticatedView.tsx +++ b/src/home/UnauthenticatedView.tsx @@ -116,13 +116,15 @@ export const UnauthenticatedView: FC = () => { if (!setClient) { throw new Error("setClient is undefined"); } + if (!createRoomResult.password) + throw new Error("Failed to create room with shared secret"); setClient({ client, session }); history.push( getRelativeRoomUrl( createRoomResult.roomId, + { kind: E2eeType.SHARED_KEY, secret: createRoomResult.password }, roomName, - createRoomResult.password, ), ); } diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index f6858693..988dc0f8 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -41,11 +41,7 @@ import { } from "./useECConnectionState"; import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider"; import { E2eeType } from "../e2ee/e2eeType"; - -export type E2EEConfig = { - mode: E2eeType; - sharedKey?: string; -}; +import { EncryptionSystem } from "../e2ee/sharedKeyManagement"; interface UseLivekitResult { livekitRoom?: Room; @@ -56,41 +52,35 @@ export function useLiveKit( rtcSession: MatrixRTCSession, muteStates: MuteStates, sfuConfig: SFUConfig | undefined, - e2eeConfig: E2EEConfig, + e2eeSystem: EncryptionSystem, ): UseLivekitResult { const e2eeOptions = useMemo((): E2EEOptions | undefined => { - if (e2eeConfig.mode === E2eeType.NONE) return undefined; + if (e2eeSystem.kind === E2eeType.NONE) return undefined; - if (e2eeConfig.mode === E2eeType.PER_PARTICIPANT) { + if (e2eeSystem.kind === E2eeType.PER_PARTICIPANT) { return { keyProvider: new MatrixKeyProvider(), worker: new E2EEWorker(), }; - } else if ( - e2eeConfig.mode === E2eeType.SHARED_KEY && - e2eeConfig.sharedKey - ) { + } else if (e2eeSystem.kind === E2eeType.SHARED_KEY && e2eeSystem.secret) { return { keyProvider: new ExternalE2EEKeyProvider(), worker: new E2EEWorker(), }; } - }, [e2eeConfig]); + }, [e2eeSystem]); useEffect(() => { - if (e2eeConfig.mode === E2eeType.NONE || !e2eeOptions) return; + if (e2eeSystem.kind === E2eeType.NONE || !e2eeOptions) return; - if (e2eeConfig.mode === E2eeType.PER_PARTICIPANT) { + if (e2eeSystem.kind === E2eeType.PER_PARTICIPANT) { (e2eeOptions.keyProvider as MatrixKeyProvider).setRTCSession(rtcSession); - } else if ( - e2eeConfig.mode === E2eeType.SHARED_KEY && - e2eeConfig.sharedKey - ) { + } else if (e2eeSystem.kind === E2eeType.SHARED_KEY && e2eeSystem.secret) { (e2eeOptions.keyProvider as ExternalE2EEKeyProvider).setKey( - e2eeConfig.sharedKey, + e2eeSystem.secret, ); } - }, [e2eeOptions, e2eeConfig, rtcSession]); + }, [e2eeOptions, e2eeSystem, rtcSession]); const initialMuteStates = useRef(muteStates); const devices = useMediaDevices(); @@ -131,9 +121,9 @@ export function useLiveKit( // useEffect() with an argument that references itself, if E2EE is enabled const room = useMemo(() => { const r = new Room(roomOptions); - r.setE2EEEnabled(e2eeConfig.mode !== E2eeType.NONE); + r.setE2EEEnabled(e2eeSystem.kind !== E2eeType.NONE); return r; - }, [roomOptions, e2eeConfig]); + }, [roomOptions, e2eeSystem]); const connectionState = useECConnectionState( { diff --git a/src/room/AppSelectionModal.tsx b/src/room/AppSelectionModal.tsx index 1a9009f5..ffb9932d 100644 --- a/src/room/AppSelectionModal.tsx +++ b/src/room/AppSelectionModal.tsx @@ -21,13 +21,14 @@ import PopOutIcon from "@vector-im/compound-design-tokens/icons/pop-out.svg?reac import { logger } from "matrix-js-sdk/src/logger"; import { Modal } from "../Modal"; -import { useIsRoomE2EE, useRoomSharedKey } from "../e2ee/sharedKeyManagement"; +import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement"; import { getAbsoluteRoomUrl } from "../matrix-utils"; import styles from "./AppSelectionModal.module.css"; import { editFragmentQuery } from "../UrlParams"; +import { E2eeType } from "../e2ee/e2eeType"; interface Props { - roomId: string | null; + roomId: string; } export const AppSelectionModal: FC = ({ roomId }) => { @@ -42,10 +43,9 @@ export const AppSelectionModal: FC = ({ roomId }) => { }, [setOpen], ); + const e2eeSystem = useRoomEncryptionSystem(roomId); - const roomSharedKey = useRoomSharedKey(roomId ?? ""); - const roomIsEncrypted = useIsRoomE2EE(roomId ?? ""); - if (roomIsEncrypted && roomSharedKey === undefined) { + if (e2eeSystem.kind === E2eeType.NONE) { logger.error( "Generating app redirect URL for encrypted room but don't have key available!", ); @@ -60,7 +60,7 @@ export const AppSelectionModal: FC = ({ roomId }) => { const url = new URL( roomId === null ? window.location.href - : getAbsoluteRoomUrl(roomId, undefined, roomSharedKey ?? undefined), + : getAbsoluteRoomUrl(roomId, e2eeSystem), ); // Edit the URL to prevent the app selection prompt from appearing a second // time within the app, and to keep the user confined to the current room @@ -73,7 +73,7 @@ export const AppSelectionModal: FC = ({ roomId }) => { const result = new URL("io.element.call:/"); result.searchParams.set("url", url.toString()); return result.toString(); - }, [roomId, roomSharedKey]); + }, [e2eeSystem, roomId]); return ( = ({ const { displayName, avatarUrl } = useProfile(client); const roomName = useRoomName(rtcSession.room); const roomAvatar = useRoomAvatar(rtcSession.room); - const e2eeSharedKey = useRoomSharedKey(rtcSession.room.roomId); const { perParticipantE2EE, returnToLobby } = useUrlParams(); - const roomEncrypted = - useIsRoomE2EE(rtcSession.room.roomId) || perParticipantE2EE; + const e2eeSystem = useRoomEncryptionSystem(rtcSession.room.roomId); const matrixInfo = useMemo((): MatrixInfo => { return { @@ -100,16 +100,16 @@ export const GroupCallView: FC = ({ roomName, roomAlias: rtcSession.room.getCanonicalAlias(), roomAvatar, - roomEncrypted, + e2eeSystem, }; }, [ + client, displayName, avatarUrl, - rtcSession, + rtcSession.room, roomName, roomAvatar, - roomEncrypted, - client, + e2eeSystem, ]); // Count each member only once, regardless of how many devices they use @@ -126,16 +126,6 @@ export const GroupCallView: FC = ({ const latestMuteStates = useRef(); latestMuteStates.current = muteStates; - const e2eeConfig = useMemo((): E2EEConfig => { - if (perParticipantE2EE) { - return { mode: E2eeType.PER_PARTICIPANT }; - } else if (e2eeSharedKey) { - return { mode: E2eeType.SHARED_KEY, sharedKey: e2eeSharedKey }; - } else { - return { mode: E2eeType.NONE }; - } - }, [perParticipantE2EE, e2eeSharedKey]); - useEffect(() => { const defaultDeviceSetup = async ( requestedDeviceData: JoinCallData, @@ -288,17 +278,18 @@ export const GroupCallView: FC = ({ const { t } = useTranslation(); - if (roomEncrypted && !perParticipantE2EE && !e2eeSharedKey) { + if (e2eeSystem.kind === E2eeType.NONE) { return ( ); - } else if (!isE2EESupported() && roomEncrypted) { + } else if (!isE2EESupportedBrowser()) { + // and we have a encryption system. return ( {t("browser_media_e2ee_unsupported_heading")} @@ -345,7 +336,7 @@ export const GroupCallView: FC = ({ onLeave={onLeave} hideHeader={hideHeader} muteStates={muteStates} - e2eeConfig={e2eeConfig} + e2eeSystem={e2eeSystem} //otelGroupCallMembership={otelGroupCallMembership} onShareClick={onShareClick} /> diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 5f6ff4a2..56cdba49 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -63,7 +63,7 @@ import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal"; import { useRageshakeRequestModal } from "../settings/submit-rageshake"; import { RageshakeRequestModal } from "./RageshakeRequestModal"; -import { E2EEConfig, useLiveKit } from "../livekit/useLiveKit"; +import { useLiveKit } from "../livekit/useLiveKit"; import { useFullscreen } from "./useFullscreen"; import { useLayoutStates } from "../video-grid/Layout"; import { useWakeLock } from "../useWakeLock"; @@ -76,13 +76,15 @@ import { ECConnectionState } from "../livekit/useECConnectionState"; import { useOpenIDSFU } from "../livekit/openIDSFU"; import { useCallViewModel } from "../state/CallViewModel"; import { subscribe } from "../state/subscribe"; +import { EncryptionSystem } from "../e2ee/sharedKeyManagement"; +import { E2eeType } from "../e2ee/e2eeType"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); export interface ActiveCallProps extends Omit { - e2eeConfig: E2EEConfig; + e2eeSystem: EncryptionSystem; } export const ActiveCall: FC = (props) => { @@ -91,7 +93,7 @@ export const ActiveCall: FC = (props) => { props.rtcSession, props.muteStates, sfuConfig, - props.e2eeConfig, + props.e2eeSystem, ); useEffect(() => { @@ -238,7 +240,7 @@ export const InCallView: FC = subscribe( const vm = useCallViewModel( rtcSession.room, livekitRoom, - matrixInfo.roomEncrypted, + matrixInfo.e2eeSystem.kind !== E2eeType.NONE, connState, ); const items = useStateObservable(vm.tiles); @@ -432,7 +434,7 @@ export const InCallView: FC = subscribe( id={matrixInfo.roomId} name={matrixInfo.roomName} avatarUrl={matrixInfo.roomAvatar} - encrypted={matrixInfo.roomEncrypted} + encrypted={matrixInfo.e2eeSystem.kind !== E2eeType.NONE} participantCount={participantCount} /> diff --git a/src/room/InviteModal.tsx b/src/room/InviteModal.tsx index caecab44..3a66ebea 100644 --- a/src/room/InviteModal.tsx +++ b/src/room/InviteModal.tsx @@ -25,8 +25,8 @@ import useClipboard from "react-use-clipboard"; import { Modal } from "../Modal"; import { getAbsoluteRoomUrl } from "../matrix-utils"; import styles from "./InviteModal.module.css"; -import { useRoomSharedKey } from "../e2ee/sharedKeyManagement"; import { Toast } from "../Toast"; +import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement"; interface Props { room: Room; @@ -36,11 +36,11 @@ interface Props { export const InviteModal: FC = ({ room, open, onDismiss }) => { const { t } = useTranslation(); - const roomSharedKey = useRoomSharedKey(room.roomId); + const e2eeSystem = useRoomEncryptionSystem(room.roomId); + const url = useMemo( - () => - getAbsoluteRoomUrl(room.roomId, room.name, roomSharedKey ?? undefined), - [room, roomSharedKey], + () => getAbsoluteRoomUrl(room.roomId, e2eeSystem, room.name), + [e2eeSystem, room.name, room.roomId], ); const [, setCopied] = useClipboard(url); const [toastOpen, setToastOpen] = useState(false); diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index 08cc0f05..df3496f1 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -36,6 +36,7 @@ import { } from "../button/Button"; import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal"; import { useMediaQuery } from "../useMediaQuery"; +import { E2eeType } from "../e2ee/e2eeType"; interface Props { client: MatrixClient; @@ -104,7 +105,7 @@ export const LobbyView: FC = ({ id={matrixInfo.roomId} name={matrixInfo.roomName} avatarUrl={matrixInfo.roomAvatar} - encrypted={matrixInfo.roomEncrypted} + encrypted={matrixInfo.e2eeSystem.kind !== E2eeType.NONE} participantCount={participantCount} /> diff --git a/src/room/VideoPreview.tsx b/src/room/VideoPreview.tsx index 4d1f040c..602ca5e5 100644 --- a/src/room/VideoPreview.tsx +++ b/src/room/VideoPreview.tsx @@ -32,6 +32,7 @@ import styles from "./VideoPreview.module.css"; import { useMediaDevices } from "../livekit/MediaDevicesContext"; import { MuteStates } from "./MuteStates"; import { useMediaQuery } from "../useMediaQuery"; +import { EncryptionSystem } from "../e2ee/sharedKeyManagement"; export type MatrixInfo = { userId: string; @@ -41,7 +42,7 @@ export type MatrixInfo = { roomName: string; roomAlias: string | null; roomAvatar: string | null; - roomEncrypted: boolean; + e2eeSystem: EncryptionSystem; }; interface Props { diff --git a/src/room/WaitForInviteView.tsx b/src/room/WaitForInviteView.tsx index c98483d8..20401d20 100644 --- a/src/room/WaitForInviteView.tsx +++ b/src/room/WaitForInviteView.tsx @@ -53,8 +53,6 @@ export const WaitForInviteView: FC = ({ [setSettingsModalOpen], ); - // TODO: Unify this component with InCallView, so we can get slick joining - // animations and don't have to feel bad about reusing its CSS return ( <>
diff --git a/src/widget.ts b/src/widget.ts index 32ab780e..ad7156e3 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -122,6 +122,7 @@ export const widget = ((): WidgetHelpers | null => { ]; const receiveState = [ { eventType: EventType.RoomMember }, + { eventType: EventType.RoomEncryption }, { eventType: EventType.GroupCallPrefix }, { eventType: EventType.GroupCallMemberPrefix }, ];