Skip to content

Commit

Permalink
Prototyping for to-device key distribution
Browse files Browse the repository at this point in the history
Show participant ID and some encryption status message
Allow encryption system to be chosen at point of room creation
Send cryptoVersion platform data to Posthog
Send key distribution stats to posthog
Send encryption type for CallStarted and CallEnded events
Update js-sdk
  • Loading branch information
hughns committed Sep 2, 2024
1 parent 0402887 commit 497b38b
Show file tree
Hide file tree
Showing 14 changed files with 339 additions and 58 deletions.
4 changes: 4 additions & 0 deletions src/analytics/PosthogAnalytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ interface PlatformProperties {
appVersion: string;
matrixBackend: "embedded" | "jssdk";
callBackend: "livekit" | "full-mesh";
cryptoVersion?: string;
}

interface PosthogSettings {
Expand Down Expand Up @@ -193,6 +194,9 @@ export class PosthogAnalytics {
appVersion,
matrixBackend: widget ? "embedded" : "jssdk",
callBackend: "livekit",
cryptoVersion: widget
? undefined
: window.matrixclient.getCrypto()?.getVersion(),
};
}

Expand Down
50 changes: 48 additions & 2 deletions src/analytics/PosthogEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,40 @@ limitations under the License.

import { DisconnectReason } from "livekit-client";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";

import {
IPosthogEvent,
PosthogAnalytics,
RegistrationType,
} from "./PosthogAnalytics";

import { E2eeType } from "../e2ee/e2eeType";

type EncryptionScheme = "none" | "shared" | "per_sender";

function mapE2eeType(type: E2eeType): EncryptionScheme {
switch (type) {
case E2eeType.NONE:
return "none";
case E2eeType.SHARED_KEY:
return "shared";
case E2eeType.PER_PARTICIPANT:
return "per_sender";
}
}
interface CallEnded extends IPosthogEvent {
eventName: "CallEnded";
callId: string;
callParticipantsOnLeave: number;
callParticipantsMax: number;
callDuration: number;
encryption: EncryptionScheme;
toDeviceEncryptionKeysSent: number;
toDeviceEncryptionKeysReceived: number;
toDeviceEncryptionKeysReceivedAverageAge: number;
roomEventEncryptionKeysSent: number;
roomEventEncryptionKeysReceived: number;
roomEventEncryptionKeysReceivedAverageAge: number;
}

export class CallEndedTracker {
Expand All @@ -51,6 +72,8 @@ export class CallEndedTracker {
public track(
callId: string,
callParticipantsNow: number,
e2eeType: E2eeType,
rtcSession: MatrixRTCSession,
sendInstantly: boolean,
): void {
PosthogAnalytics.instance.trackEvent<CallEnded>(
Expand All @@ -60,6 +83,27 @@ export class CallEndedTracker {
callParticipantsMax: this.cache.maxParticipantsCount,
callParticipantsOnLeave: callParticipantsNow,
callDuration: (Date.now() - this.cache.startTime.getTime()) / 1000,
encryption: mapE2eeType(e2eeType),
toDeviceEncryptionKeysSent:
rtcSession.statistics.counters.toDeviceEncryptionKeysSent,
toDeviceEncryptionKeysReceived:
rtcSession.statistics.counters.toDeviceEncryptionKeysReceived,
toDeviceEncryptionKeysReceivedAverageAge:
rtcSession.statistics.counters.toDeviceEncryptionKeysReceived > 0
? rtcSession.statistics.totals
.toDeviceEncryptionKeysReceivedTotalAge /
rtcSession.statistics.counters.toDeviceEncryptionKeysReceived
: 0,
roomEventEncryptionKeysSent:
rtcSession.statistics.counters.roomEventEncryptionKeysSent,
roomEventEncryptionKeysReceived:
rtcSession.statistics.counters.roomEventEncryptionKeysReceived,
roomEventEncryptionKeysReceivedAverageAge:
rtcSession.statistics.counters.roomEventEncryptionKeysReceived > 0
? rtcSession.statistics.totals
.roomEventEncryptionKeysReceivedTotalAge /
rtcSession.statistics.counters.roomEventEncryptionKeysReceived
: 0,
},
{ send_instantly: sendInstantly },
);
Expand All @@ -69,13 +113,15 @@ export class CallEndedTracker {
interface CallStarted extends IPosthogEvent {
eventName: "CallStarted";
callId: string;
encryption: EncryptionScheme;
}

export class CallStartedTracker {
public track(callId: string): void {
public track(callId: string, e2eeType: E2eeType): void {
PosthogAnalytics.instance.trackEvent<CallStarted>({
eventName: "CallStarted",
callId: callId,
encryption: mapE2eeType(e2eeType),
});
}
}
Expand Down
47 changes: 33 additions & 14 deletions src/home/RegisteredView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { useState, useCallback, FormEvent, FormEventHandler, FC } from "react";
import { useHistory } from "react-router-dom";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { useTranslation } from "react-i18next";
import { Heading } from "@vector-im/compound-web";
import { Dropdown, Heading } from "@vector-im/compound-web";
import { logger } from "matrix-js-sdk/src/logger";
import { Button } from "@vector-im/compound-web";

Expand All @@ -45,6 +45,17 @@ import { useOptInAnalytics } from "../settings/settings";
interface Props {
client: MatrixClient;
}
const encryptionOptions = {
shared: {
label: "Shared key",
e2eeType: E2eeType.SHARED_KEY,
},
sender: {
label: "Per-participant key",
e2eeType: E2eeType.PER_PARTICIPANT,
},
none: { label: "None", e2eeType: E2eeType.NONE },
};

export const RegisteredView: FC<Props> = ({ client }) => {
const [loading, setLoading] = useState(false);
Expand All @@ -59,6 +70,9 @@ export const RegisteredView: FC<Props> = ({ client }) => {
[setJoinExistingCallModalOpen],
);

const [encryption, setEncryption] =
useState<keyof typeof encryptionOptions>("shared");

const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
(e: FormEvent) => {
e.preventDefault();
Expand All @@ -73,21 +87,13 @@ export const RegisteredView: FC<Props> = ({ client }) => {
setError(undefined);
setLoading(true);

const createRoomResult = await createRoom(
const { roomId, encryptionSystem } = await createRoom(
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,
),
encryptionOptions[encryption].e2eeType,
);

history.push(getRelativeRoomUrl(roomId, encryptionSystem, roomName));
}

submit().catch((error) => {
Expand All @@ -103,7 +109,7 @@ export const RegisteredView: FC<Props> = ({ client }) => {
}
});
},
[client, history, setJoinExistingCallModalOpen],
[client, history, setJoinExistingCallModalOpen, encryption],
);

const recentRooms = useGroupCallRooms(client);
Expand Down Expand Up @@ -142,6 +148,19 @@ export const RegisteredView: FC<Props> = ({ client }) => {
data-testid="home_callName"
/>

<Dropdown
label="Encryption"
defaultValue={encryption}
onValueChange={(x) =>
setEncryption(x as keyof typeof encryptionOptions)
}
values={Object.keys(encryptionOptions).map((value) => [
value,
encryptionOptions[value as keyof typeof encryptionOptions]
.label,
])}
placeholder=""
/>
<Button
type="submit"
size="lg"
Expand Down
53 changes: 40 additions & 13 deletions src/home/UnauthenticatedView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { FC, useCallback, useState, FormEventHandler } from "react";
import { useHistory } from "react-router-dom";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { Trans, useTranslation } from "react-i18next";
import { Button, Heading } from "@vector-im/compound-web";
import { Button, Dropdown, Heading } from "@vector-im/compound-web";
import { logger } from "matrix-js-sdk/src/logger";

import { useClient } from "../ClientContext";
Expand All @@ -44,6 +44,18 @@ import { Config } from "../config/Config";
import { E2eeType } from "../e2ee/e2eeType";
import { useOptInAnalytics } from "../settings/settings";

const encryptionOptions = {
shared: {
label: "Shared key",
e2eeType: E2eeType.SHARED_KEY,
},
sender: {
label: "Per-participant key",
e2eeType: E2eeType.PER_PARTICIPANT,
},
none: { label: "None", e2eeType: E2eeType.NONE },
};

export const UnauthenticatedView: FC = () => {
const { setClient } = useClient();
const [loading, setLoading] = useState(false);
Expand All @@ -52,6 +64,9 @@ export const UnauthenticatedView: FC = () => {
const { recaptchaKey, register } = useInteractiveRegistration();
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);

const [encryption, setEncryption] =
useState<keyof typeof encryptionOptions>("shared");

const [joinExistingCallModalOpen, setJoinExistingCallModalOpen] =
useState(false);
const onDismissJoinExistingCallModal = useCallback(
Expand Down Expand Up @@ -82,13 +97,16 @@ export const UnauthenticatedView: FC = () => {
true,
);

let createRoomResult;
let roomId;
let encryptionSystem;
try {
createRoomResult = await createRoom(
const res = await createRoom(
client,
roomName,
E2eeType.SHARED_KEY,
encryptionOptions[encryption].e2eeType,
);
roomId = res.roomId;
encryptionSystem = res.encryptionSystem;
} catch (error) {
if (!setClient) {
throw error;
Expand All @@ -115,17 +133,11 @@ 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");
// 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,
),
);
history.push(getRelativeRoomUrl(roomId, encryptionSystem, roomName));
}

submit().catch((error) => {
Expand All @@ -142,6 +154,7 @@ export const UnauthenticatedView: FC = () => {
history,
setJoinExistingCallModalOpen,
setClient,
encryption,
],
);

Expand Down Expand Up @@ -204,6 +217,20 @@ export const UnauthenticatedView: FC = () => {
<ErrorMessage error={error} />
</FieldRow>
)}
<Dropdown
label="Encryption"
defaultValue={encryption}
onValueChange={(x) =>
setEncryption(x as keyof typeof encryptionOptions)
}
values={Object.keys(encryptionOptions).map((value) => [
value,
encryptionOptions[value as keyof typeof encryptionOptions]
.label,
])}
placeholder=""
/>

<Button
type="submit"
size="lg"
Expand Down
18 changes: 10 additions & 8 deletions src/room/GroupCallView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export const GroupCallView: FC<Props> = ({
const { displayName, avatarUrl } = useProfile(client);
const roomName = useRoomName(rtcSession.room);
const roomAvatar = useRoomAvatar(rtcSession.room);
const { perParticipantE2EE, returnToLobby } = useUrlParams();
const { returnToLobby } = useUrlParams();
const e2eeSystem = useRoomEncryptionSystem(rtcSession.room.roomId);

const matrixInfo = useMemo((): MatrixInfo => {
Expand Down Expand Up @@ -191,7 +191,7 @@ export const GroupCallView: FC<Props> = ({
ev: CustomEvent<IWidgetApiRequest>,
): Promise<void> => {
await defaultDeviceSetup(ev.detail.data as unknown as JoinCallData);
await enterRTCSession(rtcSession, perParticipantE2EE);
await enterRTCSession(rtcSession, e2eeSystem.kind);
await widget!.api.transport.reply(ev.detail, {});
};
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
Expand All @@ -201,12 +201,12 @@ export const GroupCallView: FC<Props> = ({
} else if (widget && !preload && skipLobby) {
const join = async (): Promise<void> => {
await defaultDeviceSetup({ audioInput: null, videoInput: null });
await enterRTCSession(rtcSession, perParticipantE2EE);
await enterRTCSession(rtcSession, e2eeSystem.kind);
};
// No lobby and no preload: we enter the RTC Session right away.
join();
}
}, [rtcSession, preload, skipLobby, perParticipantE2EE]);
}, [rtcSession, preload, skipLobby, e2eeSystem]);

const [left, setLeft] = useState(false);
const [leaveError, setLeaveError] = useState<Error | undefined>(undefined);
Expand All @@ -223,6 +223,8 @@ export const GroupCallView: FC<Props> = ({
PosthogAnalytics.instance.eventCallEnded.track(
rtcSession.room.roomId,
rtcSession.memberships.length,
matrixInfo.e2eeSystem.kind,
rtcSession,
sendInstantly,
);

Expand All @@ -237,7 +239,7 @@ export const GroupCallView: FC<Props> = ({
history.push("/");
}
},
[rtcSession, isPasswordlessUser, confineToRoom, history],
[rtcSession, isPasswordlessUser, confineToRoom, history, matrixInfo],
);

useEffect(() => {
Expand All @@ -262,8 +264,8 @@ export const GroupCallView: FC<Props> = ({
const onReconnect = useCallback(() => {
setLeft(false);
setLeaveError(undefined);
enterRTCSession(rtcSession, perParticipantE2EE);
}, [rtcSession, perParticipantE2EE]);
enterRTCSession(rtcSession, e2eeSystem.kind);
}, [rtcSession, e2eeSystem]);

const joinRule = useJoinRule(rtcSession.room);

Expand Down Expand Up @@ -316,7 +318,7 @@ export const GroupCallView: FC<Props> = ({
client={client}
matrixInfo={matrixInfo}
muteStates={muteStates}
onEnter={() => void enterRTCSession(rtcSession, perParticipantE2EE)}
onEnter={() => void enterRTCSession(rtcSession, e2eeSystem.kind)}
confineToRoom={confineToRoom}
hideHeader={hideHeader}
participantCount={participantCount}
Expand Down
Loading

0 comments on commit 497b38b

Please sign in to comment.