Handle embedding

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner
2023-08-23 11:01:49 +02:00
parent fa5b014abe
commit b45c8924a6
13 changed files with 124 additions and 58 deletions

View File

@@ -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 (

View File

@@ -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"),
};
};

View File

@@ -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<KeyProviderOptions> = {}
) {
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

View File

@@ -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;
};

View File

@@ -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<HTMLFormElement> = useCallback(
(e: FormEvent) => {

View File

@@ -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<HTMLFormElement> = useCallback(
(e) => {

View File

@@ -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>(muteStates);
const devices = useMediaDevices();

View File

@@ -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<Error | undefined>(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 (
<ErrorView
error={
@@ -319,7 +328,7 @@ export function GroupCallView({
<LobbyView
matrixInfo={matrixInfo}
muteStates={muteStates}
onEnter={() => enterRTCSession(rtcSession)}
onEnter={() => enterRTCSession(rtcSession, embeddedE2EEEnabled)}
isEmbedded={isEmbedded}
hideHeader={hideHeader}
/>

View File

@@ -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<GroupCallStatus>({ kind: "loading" });
const [e2eeEnabled] = useEnableE2EE();
const [e2eeEnabled] = useEnableSPAE2EE();
useEffect(() => {
const fetchOrCreateRoom = async (): Promise<Room> => {

View File

@@ -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) {

View File

@@ -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();

View File

@@ -89,14 +89,15 @@ export const useOptInAnalytics = (): DisableableSetting<boolean | null> => {
return [false, null];
};
export const useEnableE2EE = (): DisableableSetting<boolean | null> => {
export const useEnableSPAE2EE = (): DisableableSetting<boolean | null> => {
const settingVal = useSetting<boolean | null>(
"enable-end-to-end-encryption",
false
);
if (isE2EESupported()) return settingVal;
return [false, null];
if (!isE2EESupported()) return [false, null];
return settingVal;
};
export const useDeveloperSettingsTab = () =>

View File

@@ -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