Merge pull request #1270 from vector-im/SimonBrandner/feat/e2ee-pw

This commit is contained in:
Šimon Brandner
2023-07-25 15:42:46 +02:00
committed by GitHub
12 changed files with 113 additions and 32 deletions

View File

@@ -60,7 +60,7 @@
"i18next-http-backend": "^1.4.4",
"livekit-client": "1.12.0",
"lodash": "^4.17.21",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#d79d9ae69c3220c02406706d4a1ec52c22c44fbd",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#b698217445318f453e0b1086364a33113eaa85d9",
"matrix-widget-api": "^1.3.1",
"mermaid": "^8.13.8",
"normalize.css": "^8.0.1",

View File

@@ -38,6 +38,7 @@
"Download debug logs": "Download debug logs",
"Element Call Home": "Element Call Home",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call is temporarily not end-to-end encrypted while we test scalability.",
"Enable end-to-end encryption (password protected calls)": "Enable end-to-end encryption (password protected calls)",
"Exit full screen": "Exit full screen",
"Expose developer settings in the settings window.": "Expose developer settings in the settings window.",
"Feedback": "Feedback",
@@ -72,6 +73,7 @@
"Not registered yet? <2>Create an account</2>": "Not registered yet? <2>Create an account</2>",
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>",
"Password": "Password",
"Password (if none, E2EE is disabled)": "Password (if none, E2EE is disabled)",
"Passwords must match": "Passwords must match",
"Profile": "Profile",
"Recaptcha dismissed": "Recaptcha dismissed",

View File

@@ -6,9 +6,7 @@ import {
TrackPublishDefaults,
VideoPreset,
VideoPresets,
ExternalE2EEKeyProvider,
} from "livekit-client";
import E2EEWorker from "livekit-client/e2ee-worker?worker";
const defaultLiveKitPublishOptions: TrackPublishDefaults = {
audioPreset: AudioPresets.music,
@@ -24,16 +22,7 @@ const defaultLiveKitPublishOptions: TrackPublishDefaults = {
backupCodec: { codec: "vp8", encoding: VideoPresets.h720.encoding },
} as const;
const e2eeWorker = new E2EEWorker();
const e2eeKeyProvider = new ExternalE2EEKeyProvider();
e2eeKeyProvider.setKey("not secret password");
export const defaultLiveKitOptions: RoomOptions = {
e2ee: {
keyProvider: e2eeKeyProvider,
worker: e2eeWorker,
},
// automatically manage subscribed video quality
adaptiveStream: true,

View File

@@ -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 { useMemo } from "react";
import { useEffect, useMemo } from "react";
import E2EEWorker from "livekit-client/e2ee-worker?worker";
import { defaultLiveKitOptions } from "./options";
import { SFUConfig } from "./openIDSFU";
@@ -15,10 +21,32 @@ export type DeviceChoices = {
enabled: boolean;
};
export type E2EEConfig = {
sharedKey: string;
};
export function useLiveKit(
userChoices: UserChoices,
sfuConfig?: SFUConfig
sfuConfig?: SFUConfig,
e2eeConfig?: E2EEConfig
): 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 options = defaultLiveKitOptions;
options.videoCaptureDefaults = {
@@ -29,8 +57,11 @@ export function useLiveKit(
...options.audioCaptureDefaults,
deviceId: userChoices.audio?.selectedId,
};
options.e2ee = e2eeOptions;
return options;
}, [userChoices.video, userChoices.audio]);
}, [userChoices.video, userChoices.audio, e2eeOptions]);
const roomWithoutProps = useMemo(() => new Room(roomOptions), [roomOptions]);

View File

@@ -32,7 +32,7 @@ import { CallEndedView } from "./CallEndedView";
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useProfile } from "../profile/useProfile";
import { UserChoices } from "../livekit/useLiveKit";
import { E2EEConfig, UserChoices } from "../livekit/useLiveKit";
import { findDeviceByName } from "../media-utils";
import { OpenIDLoader } from "../livekit/OpenIDLoader";
import { ActiveCall } from "./InCallView";
@@ -218,10 +218,12 @@ export function GroupCallView({
const [userChoices, setUserChoices] = useState<UserChoices | undefined>(
undefined
);
const [e2eeConfig, setE2EEConfig] = useState<E2EEConfig | undefined>(
undefined
);
const livekitServiceURL =
groupCall.foci[0]?.livekitServiceUrl ??
Config.get().livekit?.livekit_service_url;
groupCall.livekitServiceURL ?? Config.get().livekit?.livekit_service_url;
if (!livekitServiceURL) {
return <ErrorView error={new Error("No livekit_service_url defined")} />;
}
@@ -243,6 +245,7 @@ export function GroupCallView({
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
hideHeader={hideHeader}
userChoices={userChoices}
e2eeConfig={e2eeConfig}
otelGroupCallMembership={otelGroupCallMembership}
/>
</OpenIDLoader>
@@ -283,8 +286,9 @@ export function GroupCallView({
return (
<LobbyView
matrixInfo={matrixInfo}
onEnter={(choices: UserChoices) => {
onEnter={(choices: UserChoices, e2eeConfig?: E2EEConfig) => {
setUserChoices(choices);
setE2EEConfig(e2eeConfig);
enter();
}}
isEmbedded={isEmbedded}

View File

@@ -77,7 +77,7 @@ import { SettingsModal } from "../settings/SettingsModal";
import { InviteModal } from "./InviteModal";
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { UserChoices, useLiveKit } from "../livekit/useLiveKit";
import { E2EEConfig, UserChoices, useLiveKit } from "../livekit/useLiveKit";
import { useMediaDevicesSwitcher } from "../livekit/useMediaDevicesSwitcher";
import { useFullscreen } from "./useFullscreen";
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"> {
userChoices: UserChoices;
e2eeConfig?: E2EEConfig;
}
export function ActiveCall(props: ActiveCallProps) {
const sfuConfig = useSFUConfig();
const livekitRoom = useLiveKit(props.userChoices, sfuConfig);
const livekitRoom = useLiveKit(
props.userChoices,
sfuConfig,
props.e2eeConfig
);
if (!livekitRoom) {
return null;

View File

@@ -66,3 +66,9 @@ limitations under the License.
.copyButton:last-child {
margin-bottom: 0;
}
.passwordField {
width: 320px !important;
margin-bottom: 20px;
flex: 0;
}

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
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 styles from "./LobbyView.module.css";
@@ -25,12 +25,14 @@ import { UserMenuContainer } from "../UserMenuContainer";
import { Body, Link } from "../typography/Typography";
import { useLocationNavigation } from "../useLocationNavigation";
import { MatrixInfo, VideoPreview } from "./VideoPreview";
import { UserChoices } from "../livekit/useLiveKit";
import { E2EEConfig, UserChoices } from "../livekit/useLiveKit";
import { InputField } from "../input/Input";
import { useEnableE2EE } from "../settings/useSetting";
interface Props {
matrixInfo: MatrixInfo;
onEnter: (userChoices: UserChoices) => void;
onEnter: (userChoices: UserChoices, e2eeConfig?: E2EEConfig) => void;
isEmbedded: boolean;
hideHeader: boolean;
}
@@ -39,6 +41,8 @@ export function LobbyView(props: Props) {
const { t } = useTranslation();
useLocationNavigation();
const [enableE2EE] = useEnableE2EE();
const joinCallButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (joinCallButtonRef.current) {
@@ -49,6 +53,17 @@ export function LobbyView(props: Props) {
const [userChoices, setUserChoices] = useState<UserChoices | 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 (
<div className={styles.room}>
@@ -68,12 +83,26 @@ export function LobbyView(props: Props) {
matrixInfo={props.matrixInfo}
onUserChoicesChanged={setUserChoices}
/>
{enableE2EE && (
<InputField
className={styles.passwordField}
label={t("Password (if none, E2EE is disabled)")}
type="text"
onChange={onE2EESharedKeyChanged}
value={e2eeSharedKey}
/>
)}
<Trans>
<Button
ref={joinCallButtonRef}
className={styles.copyButton}
size="lg"
onPress={() => props.onEnter(userChoices!)}
onPress={() =>
props.onEnter(
userChoices!,
e2eeSharedKey ? { sharedKey: e2eeSharedKey } : undefined
)
}
data-testid="lobby_joinCall"
>
Join call now

View File

@@ -34,6 +34,7 @@ import {
useOptInAnalytics,
useDeveloperSettingsTab,
useShowConnectionStats,
useEnableE2EE,
} from "./useSetting";
import { FieldRow, InputField } from "../input/Input";
import { Button } from "../button";
@@ -68,6 +69,7 @@ export const SettingsModal = (props: Props) => {
useDeveloperSettingsTab();
const [showConnectionStats, setShowConnectionStats] =
useShowConnectionStats();
const [enableE2EE, setEnableE2EE] = useEnableE2EE();
const downloadDebugLog = useDownloadDebugLog();
@@ -249,6 +251,18 @@ export const SettingsModal = (props: Props) => {
}
/>
</FieldRow>
<FieldRow>
<InputField
id="enableE2EE"
name="end-to-end-encryption"
label={t("Enable end-to-end encryption (password protected calls)")}
type="checkbox"
checked={enableE2EE}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setEnableE2EE(e.target.checked)
}
/>
</FieldRow>
<FieldRow>
<Button onPress={downloadDebugLog}>{t("Download debug logs")}</Button>
</FieldRow>

View File

@@ -104,6 +104,9 @@ export const useDeveloperSettingsTab = () =>
export const useShowConnectionStats = () =>
useSetting("show-connection-stats", false);
export const useEnableE2EE = () =>
useSetting("enable-end-to-end-encryption", false);
export const useDefaultDevices = () =>
useSetting("defaultDevices", {
audioinput: "",

View File

@@ -164,7 +164,7 @@ export const widget: WidgetHelpers | null = (() => {
// message so that we can use the widget API in less racy mode, but we need to change
// element-web to use waitForIFrameLoad=false. Once that change has rolled out,
// we can just start the client after we've fetched the config.
foci: [],
livekitServiceURL: undefined,
}
);
@@ -178,9 +178,7 @@ export const widget: WidgetHelpers | null = (() => {
// Now we've fetched the config, be evil and use the getter to inject the focus
// into the client (see above XXX).
if (focus) {
client.getFoci().push({
livekitServiceUrl: livekit.livekit_service_url,
});
client.setLivekitServiceURL(livekit.livekit_service_url);
}
await client.startClient();
resolve(client);

View File

@@ -11012,9 +11012,9 @@ matrix-events-sdk@0.0.1:
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd"
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#d79d9ae69c3220c02406706d4a1ec52c22c44fbd":
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#b698217445318f453e0b1086364a33113eaa85d9":
version "26.2.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d79d9ae69c3220c02406706d4a1ec52c22c44fbd"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/b698217445318f453e0b1086364a33113eaa85d9"
dependencies:
"@babel/runtime" "^7.12.5"
"@matrix-org/matrix-sdk-crypto-js" "^0.1.1"