From 15d3e7574d0582803db1922a4ffa4a5c8fc19ae1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 16 Oct 2023 17:45:06 +0100 Subject: [PATCH] Re-apply Simon's emebdded e2ee work on latest livekit branch Replaces https://github.com/vector-im/element-call/pull/1350 --- src/UrlParams.ts | 5 +++ src/e2ee/matrixKeyProvider.ts | 70 +++++++++++++++++++++++++++++++++++ src/livekit/useLiveKit.ts | 44 ++++++++++++++++------ src/room/GroupCallView.tsx | 24 +++++++----- src/room/InCallView.tsx | 1 + src/rtcSessionHelpers.ts | 7 +++- src/widget.ts | 2 + 7 files changed, 130 insertions(+), 23 deletions(-) create mode 100644 src/e2ee/matrixKeyProvider.ts diff --git a/src/UrlParams.ts b/src/UrlParams.ts index 498d7cc5..fb203375 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -111,6 +111,10 @@ interface UrlParams { * E2EE password */ password: string | null; + /** + * Whether we the app should use per participant keys for E2EE. + */ + perParticipantE2EE: boolean; } // This is here as a stopgap, but what would be far nicer is a function that @@ -206,6 +210,7 @@ export const getUrlParams = ( fontScale: Number.isNaN(fontScale) ? null : fontScale, analyticsID: parser.getParam("analyticsID"), allowIceFallback: parser.getFlagParam("allowIceFallback"), + perParticipantE2EE: true /*parser.getFlagParam("perParticipantE2EE")*/, }; }; diff --git a/src/e2ee/matrixKeyProvider.ts b/src/e2ee/matrixKeyProvider.ts new file mode 100644 index 00000000..7053054a --- /dev/null +++ b/src/e2ee/matrixKeyProvider.ts @@ -0,0 +1,70 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { BaseKeyProvider, createKeyMaterialFromString } from "livekit-client"; +import { logger } from "matrix-js-sdk/src/logger"; +import { + MatrixRTCSession, + MatrixRTCSessionEvent, +} from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; + +export class MatrixKeyProvider extends BaseKeyProvider { + private rtcSession?: MatrixRTCSession; + + public setRTCSession(rtcSession: MatrixRTCSession): void { + if (this.rtcSession) { + this.rtcSession.off( + MatrixRTCSessionEvent.EncryptionKeyChanged, + this.onEncryptionKeyChanged, + ); + } + + this.rtcSession = rtcSession; + + this.rtcSession.on( + MatrixRTCSessionEvent.EncryptionKeyChanged, + this.onEncryptionKeyChanged, + ); + + // The new session could be aware of keys of which the old session wasn't, + // so emit a key changed event. + for (const [ + participant, + encryptionKeys, + ] of this.rtcSession.getEncryptionKeys()) { + for (const [index, encryptionKey] of encryptionKeys.entries()) { + this.onEncryptionKeyChanged(encryptionKey, index, participant); + } + } + } + + private onEncryptionKeyChanged = async ( + encryptionKey: string, + encryptionKeyIndex: number, + participantId: string, + ): Promise => { + this.onSetEncryptionKey( + await createKeyMaterialFromString(encryptionKey), + participantId, + encryptionKeyIndex, + ); + + logger.debug( + `Embedded-E2EE-LOG onEncryptionKeyChanged participantId=${participantId} encryptionKeyIndex=${encryptionKeyIndex} encryptionKey=${encryptionKey}`, + this.getKeys(), + ); + }; +} diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index 814eb096..b6741058 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -26,6 +26,7 @@ import { useLiveKitRoom } from "@livekit/components-react"; import { useEffect, useMemo, useRef, useState } from "react"; import E2EEWorker from "livekit-client/e2ee-worker?worker"; import { logger } from "matrix-js-sdk/src/logger"; +import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; import { defaultLiveKitOptions } from "./options"; import { SFUConfig } from "./openIDSFU"; @@ -39,9 +40,16 @@ import { ECConnectionState, useECConnectionState, } 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; }; interface UseLivekitResult { @@ -50,26 +58,38 @@ interface UseLivekitResult { } export function useLiveKit( + rtcSession: MatrixRTCSession, muteStates: MuteStates, sfuConfig?: SFUConfig, e2eeConfig?: E2EEConfig, ): UseLivekitResult { - const e2eeOptions = useMemo(() => { - if (!e2eeConfig?.sharedKey) return undefined; + const e2eeOptions = useMemo((): E2EEOptions | 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 (!e2eeConfig || !e2eeOptions) 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 4b2f4cfa..9103b56b 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -44,6 +44,8 @@ import { useRoomAvatar } from "./useRoomAvatar"; import { useRoomName } from "./useRoomName"; import { useJoinRule } from "./useJoinRule"; import { InviteModal } from "./InviteModal"; +import { E2EEConfig, E2EEMode } from "../livekit/useLiveKit"; +import { useUrlParams } from "../UrlParams"; declare global { interface Window { @@ -85,6 +87,7 @@ export const GroupCallView: FC = ({ const roomName = useRoomName(rtcSession.room); const roomAvatar = useRoomAvatar(rtcSession.room); const roomEncrypted = useIsRoomE2EE(rtcSession.room.roomId)!; + const { perParticipantE2EE } = useUrlParams(); const matrixInfo = useMemo((): MatrixInfo => { return { @@ -176,7 +179,7 @@ export const GroupCallView: FC = ({ } } - enterRTCSession(rtcSession); + enterRTCSession(rtcSession, perParticipantE2EE); 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 @@ -195,7 +198,7 @@ export const GroupCallView: FC = ({ widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin); }; } - }, [rtcSession, preload]); + }, [rtcSession, preload, perParticipantE2EE]); const [left, setLeft] = useState(false); const [leaveError, setLeaveError] = useState(undefined); @@ -255,16 +258,19 @@ export const GroupCallView: FC = ({ } }, [isJoined, rtcSession]); - 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, perParticipantE2EE); + }, [rtcSession, perParticipantE2EE]); const joinRule = useJoinRule(rtcSession.room); @@ -380,7 +386,7 @@ export const GroupCallView: FC = ({ client={client} matrixInfo={matrixInfo} muteStates={muteStates} - onEnter={(): void => enterRTCSession(rtcSession)} + onEnter={(): void => enterRTCSession(rtcSession, perParticipantE2EE)} confineToRoom={confineToRoom} hideHeader={hideHeader} participantCount={participantCount} diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index bd0fd1a7..373c8b14 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -100,6 +100,7 @@ export interface ActiveCallProps export const ActiveCall: FC = (props) => { const sfuConfig = useOpenIDSFU(props.client, props.rtcSession); const { livekitRoom, connState } = useLiveKit( + props.rtcSession, props.muteStates, sfuConfig, props.e2eeConfig, diff --git a/src/rtcSessionHelpers.ts b/src/rtcSessionHelpers.ts index 1ea00160..bcde93e5 100644 --- a/src/rtcSessionHelpers.ts +++ b/src/rtcSessionHelpers.ts @@ -33,7 +33,10 @@ function makeFocus(livekitAlias: string): LivekitFocus { }; } -export function enterRTCSession(rtcSession: MatrixRTCSession): void { +export function enterRTCSession( + rtcSession: MatrixRTCSession, + encryptMedia: boolean, +): void { PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId); @@ -44,7 +47,7 @@ export function enterRTCSession(rtcSession: MatrixRTCSession): void { // right now we assume everything is a room-scoped call const livekitAlias = rtcSession.room.roomId; - rtcSession.joinRoomSession([makeFocus(livekitAlias)]); + rtcSession.joinRoomSession([makeFocus(livekitAlias)], encryptMedia); } export async function leaveRTCSession( diff --git a/src/widget.ts b/src/widget.ts index e0a0d340..3a7549f8 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -77,6 +77,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