Add E2EE password prompt
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
@@ -6,9 +6,7 @@ import {
|
|||||||
TrackPublishDefaults,
|
TrackPublishDefaults,
|
||||||
VideoPreset,
|
VideoPreset,
|
||||||
VideoPresets,
|
VideoPresets,
|
||||||
ExternalE2EEKeyProvider,
|
|
||||||
} from "livekit-client";
|
} from "livekit-client";
|
||||||
import E2EEWorker from "livekit-client/e2ee-worker?worker";
|
|
||||||
|
|
||||||
const defaultLiveKitPublishOptions: TrackPublishDefaults = {
|
const defaultLiveKitPublishOptions: TrackPublishDefaults = {
|
||||||
audioPreset: AudioPresets.music,
|
audioPreset: AudioPresets.music,
|
||||||
@@ -24,16 +22,7 @@ const defaultLiveKitPublishOptions: TrackPublishDefaults = {
|
|||||||
backupCodec: { codec: "vp8", encoding: VideoPresets.h720.encoding },
|
backupCodec: { codec: "vp8", encoding: VideoPresets.h720.encoding },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const e2eeWorker = new E2EEWorker();
|
|
||||||
const e2eeKeyProvider = new ExternalE2EEKeyProvider();
|
|
||||||
e2eeKeyProvider.setKey("not secret password");
|
|
||||||
|
|
||||||
export const defaultLiveKitOptions: RoomOptions = {
|
export const defaultLiveKitOptions: RoomOptions = {
|
||||||
e2ee: {
|
|
||||||
keyProvider: e2eeKeyProvider,
|
|
||||||
worker: e2eeWorker,
|
|
||||||
},
|
|
||||||
|
|
||||||
// automatically manage subscribed video quality
|
// automatically manage subscribed video quality
|
||||||
adaptiveStream: true,
|
adaptiveStream: true,
|
||||||
|
|
||||||
|
|||||||
@@ -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 { 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 { defaultLiveKitOptions } from "./options";
|
||||||
import { SFUConfig } from "./openIDSFU";
|
import { SFUConfig } from "./openIDSFU";
|
||||||
@@ -15,10 +21,32 @@ export type DeviceChoices = {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type E2EEConfig = {
|
||||||
|
sharedKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
export function useLiveKit(
|
export function useLiveKit(
|
||||||
userChoices: UserChoices,
|
userChoices: UserChoices,
|
||||||
sfuConfig?: SFUConfig
|
sfuConfig?: SFUConfig,
|
||||||
|
e2eeConfig?: E2EEConfig
|
||||||
): Room | undefined {
|
): 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 roomOptions = useMemo((): RoomOptions => {
|
||||||
const options = defaultLiveKitOptions;
|
const options = defaultLiveKitOptions;
|
||||||
options.videoCaptureDefaults = {
|
options.videoCaptureDefaults = {
|
||||||
@@ -29,8 +57,11 @@ export function useLiveKit(
|
|||||||
...options.audioCaptureDefaults,
|
...options.audioCaptureDefaults,
|
||||||
deviceId: userChoices.audio?.selectedId,
|
deviceId: userChoices.audio?.selectedId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
options.e2ee = e2eeOptions;
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}, [userChoices.video, userChoices.audio]);
|
}, [userChoices.video, userChoices.audio, e2eeOptions]);
|
||||||
|
|
||||||
const roomWithoutProps = useMemo(() => new Room(roomOptions), [roomOptions]);
|
const roomWithoutProps = useMemo(() => new Room(roomOptions), [roomOptions]);
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import { CallEndedView } from "./CallEndedView";
|
|||||||
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
||||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||||
import { useProfile } from "../profile/useProfile";
|
import { useProfile } from "../profile/useProfile";
|
||||||
import { UserChoices } from "../livekit/useLiveKit";
|
import { E2EEConfig, UserChoices } from "../livekit/useLiveKit";
|
||||||
import { findDeviceByName } from "../media-utils";
|
import { findDeviceByName } from "../media-utils";
|
||||||
import { OpenIDLoader } from "../livekit/OpenIDLoader";
|
import { OpenIDLoader } from "../livekit/OpenIDLoader";
|
||||||
import { ActiveCall } from "./InCallView";
|
import { ActiveCall } from "./InCallView";
|
||||||
@@ -218,10 +218,12 @@ export function GroupCallView({
|
|||||||
const [userChoices, setUserChoices] = useState<UserChoices | undefined>(
|
const [userChoices, setUserChoices] = useState<UserChoices | undefined>(
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
const [e2eeConfig, setE2EEConfig] = useState<E2EEConfig | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
const livekitServiceURL =
|
const livekitServiceURL =
|
||||||
groupCall.foci[0]?.livekitServiceUrl ??
|
groupCall.livekitServiceURL ?? Config.get().livekit?.livekit_service_url;
|
||||||
Config.get().livekit?.livekit_service_url;
|
|
||||||
if (!livekitServiceURL) {
|
if (!livekitServiceURL) {
|
||||||
return <ErrorView error={new Error("No livekit_service_url defined")} />;
|
return <ErrorView error={new Error("No livekit_service_url defined")} />;
|
||||||
}
|
}
|
||||||
@@ -243,6 +245,7 @@ export function GroupCallView({
|
|||||||
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
|
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
|
||||||
hideHeader={hideHeader}
|
hideHeader={hideHeader}
|
||||||
userChoices={userChoices}
|
userChoices={userChoices}
|
||||||
|
e2eeConfig={e2eeConfig}
|
||||||
otelGroupCallMembership={otelGroupCallMembership}
|
otelGroupCallMembership={otelGroupCallMembership}
|
||||||
/>
|
/>
|
||||||
</OpenIDLoader>
|
</OpenIDLoader>
|
||||||
@@ -283,8 +286,9 @@ export function GroupCallView({
|
|||||||
return (
|
return (
|
||||||
<LobbyView
|
<LobbyView
|
||||||
matrixInfo={matrixInfo}
|
matrixInfo={matrixInfo}
|
||||||
onEnter={(choices: UserChoices) => {
|
onEnter={(choices: UserChoices, e2eeConfig?: E2EEConfig) => {
|
||||||
setUserChoices(choices);
|
setUserChoices(choices);
|
||||||
|
setE2EEConfig(e2eeConfig);
|
||||||
enter();
|
enter();
|
||||||
}}
|
}}
|
||||||
isEmbedded={isEmbedded}
|
isEmbedded={isEmbedded}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ import { SettingsModal } from "../settings/SettingsModal";
|
|||||||
import { InviteModal } from "./InviteModal";
|
import { InviteModal } from "./InviteModal";
|
||||||
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
|
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
|
||||||
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
||||||
import { UserChoices, useLiveKit } from "../livekit/useLiveKit";
|
import { E2EEConfig, UserChoices, useLiveKit } from "../livekit/useLiveKit";
|
||||||
import { useMediaDevicesSwitcher } from "../livekit/useMediaDevicesSwitcher";
|
import { useMediaDevicesSwitcher } from "../livekit/useMediaDevicesSwitcher";
|
||||||
import { useFullscreen } from "./useFullscreen";
|
import { useFullscreen } from "./useFullscreen";
|
||||||
import { useLayoutStates } from "../video-grid/Layout";
|
import { useLayoutStates } from "../video-grid/Layout";
|
||||||
@@ -92,11 +92,16 @@ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|||||||
|
|
||||||
export interface ActiveCallProps extends Omit<InCallViewProps, "livekitRoom"> {
|
export interface ActiveCallProps extends Omit<InCallViewProps, "livekitRoom"> {
|
||||||
userChoices: UserChoices;
|
userChoices: UserChoices;
|
||||||
|
e2eeConfig?: E2EEConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActiveCall(props: ActiveCallProps) {
|
export function ActiveCall(props: ActiveCallProps) {
|
||||||
const sfuConfig = useSFUConfig();
|
const sfuConfig = useSFUConfig();
|
||||||
const livekitRoom = useLiveKit(props.userChoices, sfuConfig);
|
const livekitRoom = useLiveKit(
|
||||||
|
props.userChoices,
|
||||||
|
sfuConfig,
|
||||||
|
props.e2eeConfig
|
||||||
|
);
|
||||||
|
|
||||||
if (!livekitRoom) {
|
if (!livekitRoom) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -66,3 +66,9 @@ limitations under the License.
|
|||||||
.copyButton:last-child {
|
.copyButton:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.passwordField {
|
||||||
|
width: 320px !important;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
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 { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import styles from "./LobbyView.module.css";
|
import styles from "./LobbyView.module.css";
|
||||||
@@ -25,12 +25,13 @@ import { UserMenuContainer } from "../UserMenuContainer";
|
|||||||
import { Body, Link } from "../typography/Typography";
|
import { Body, Link } from "../typography/Typography";
|
||||||
import { useLocationNavigation } from "../useLocationNavigation";
|
import { useLocationNavigation } from "../useLocationNavigation";
|
||||||
import { MatrixInfo, VideoPreview } from "./VideoPreview";
|
import { MatrixInfo, VideoPreview } from "./VideoPreview";
|
||||||
import { UserChoices } from "../livekit/useLiveKit";
|
import { E2EEConfig, UserChoices } from "../livekit/useLiveKit";
|
||||||
|
import { InputField } from "../input/Input";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
matrixInfo: MatrixInfo;
|
matrixInfo: MatrixInfo;
|
||||||
|
|
||||||
onEnter: (userChoices: UserChoices) => void;
|
onEnter: (userChoices: UserChoices, e2eeConfig?: E2EEConfig) => void;
|
||||||
isEmbedded: boolean;
|
isEmbedded: boolean;
|
||||||
hideHeader: boolean;
|
hideHeader: boolean;
|
||||||
}
|
}
|
||||||
@@ -49,6 +50,17 @@ export function LobbyView(props: Props) {
|
|||||||
const [userChoices, setUserChoices] = useState<UserChoices | undefined>(
|
const [userChoices, setUserChoices] = useState<UserChoices | undefined>(
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
const [e2eeSharedKey, setE2EESharedKey] = useState<string | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const onE2EESharedKeyChanged = useCallback(
|
||||||
|
(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
setE2EESharedKey(value === "" ? undefined : value);
|
||||||
|
},
|
||||||
|
[setE2EESharedKey]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.room}>
|
<div className={styles.room}>
|
||||||
@@ -68,12 +80,24 @@ export function LobbyView(props: Props) {
|
|||||||
matrixInfo={props.matrixInfo}
|
matrixInfo={props.matrixInfo}
|
||||||
onUserChoicesChanged={setUserChoices}
|
onUserChoicesChanged={setUserChoices}
|
||||||
/>
|
/>
|
||||||
|
<InputField
|
||||||
|
className={styles.passwordField}
|
||||||
|
label={t("Password (if none E2EE, is disabled)")}
|
||||||
|
type="text"
|
||||||
|
onChange={onE2EESharedKeyChanged}
|
||||||
|
value={e2eeSharedKey}
|
||||||
|
/>
|
||||||
<Trans>
|
<Trans>
|
||||||
<Button
|
<Button
|
||||||
ref={joinCallButtonRef}
|
ref={joinCallButtonRef}
|
||||||
className={styles.copyButton}
|
className={styles.copyButton}
|
||||||
size="lg"
|
size="lg"
|
||||||
onPress={() => props.onEnter(userChoices!)}
|
onPress={() =>
|
||||||
|
props.onEnter(
|
||||||
|
userChoices!,
|
||||||
|
e2eeSharedKey ? { sharedKey: e2eeSharedKey } : undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
data-testid="lobby_joinCall"
|
data-testid="lobby_joinCall"
|
||||||
>
|
>
|
||||||
Join call now
|
Join call now
|
||||||
|
|||||||
Reference in New Issue
Block a user