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 Aug 31, 2024
1 parent 3e57a76 commit 9a5de12
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(),

Check warning on line 199 in src/analytics/PosthogAnalytics.ts

View check run for this annotation

Codecov / codecov/patch

src/analytics/PosthogAnalytics.ts#L197-L199

Added lines #L197 - L199 were not covered by tests
};
}

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";

Check warning on line 35 in src/analytics/PosthogEvents.ts

View check run for this annotation

Codecov / codecov/patch

src/analytics/PosthogEvents.ts#L35

Added line #L35 was not covered by tests
case E2eeType.PER_PARTICIPANT:
return "per_sender";

Check warning on line 37 in src/analytics/PosthogEvents.ts

View check run for this annotation

Codecov / codecov/patch

src/analytics/PosthogEvents.ts#L37

Added line #L37 was not covered by tests
}
}
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,

Check warning on line 76 in src/analytics/PosthogEvents.ts

View check run for this annotation

Codecov / codecov/patch

src/analytics/PosthogEvents.ts#L75-L76

Added lines #L75 - L76 were not covered by tests
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,

Check failure on line 88 in src/analytics/PosthogEvents.ts

View workflow job for this annotation

GitHub Actions / Lint, format & type check

Property 'statistics' does not exist on type 'MatrixRTCSession'.
toDeviceEncryptionKeysReceived:
rtcSession.statistics.counters.toDeviceEncryptionKeysReceived,

Check failure on line 90 in src/analytics/PosthogEvents.ts

View workflow job for this annotation

GitHub Actions / Lint, format & type check

Property 'statistics' does not exist on type 'MatrixRTCSession'.
toDeviceEncryptionKeysReceivedAverageAge:
rtcSession.statistics.counters.toDeviceEncryptionKeysReceived > 0

Check failure on line 92 in src/analytics/PosthogEvents.ts

View workflow job for this annotation

GitHub Actions / Lint, format & type check

Property 'statistics' does not exist on type 'MatrixRTCSession'.
? rtcSession.statistics.totals

Check failure on line 93 in src/analytics/PosthogEvents.ts

View workflow job for this annotation

GitHub Actions / Lint, format & type check

Property 'statistics' does not exist on type 'MatrixRTCSession'.
.toDeviceEncryptionKeysReceivedTotalAge /
rtcSession.statistics.counters.toDeviceEncryptionKeysReceived

Check failure on line 95 in src/analytics/PosthogEvents.ts

View workflow job for this annotation

GitHub Actions / Lint, format & type check

Property 'statistics' does not exist on type 'MatrixRTCSession'.
: 0,
roomEventEncryptionKeysSent:
rtcSession.statistics.counters.roomEventEncryptionKeysSent,

Check failure on line 98 in src/analytics/PosthogEvents.ts

View workflow job for this annotation

GitHub Actions / Lint, format & type check

Property 'statistics' does not exist on type 'MatrixRTCSession'.
roomEventEncryptionKeysReceived:
rtcSession.statistics.counters.roomEventEncryptionKeysReceived,

Check failure on line 100 in src/analytics/PosthogEvents.ts

View workflow job for this annotation

GitHub Actions / Lint, format & type check

Property 'statistics' does not exist on type 'MatrixRTCSession'.
roomEventEncryptionKeysReceivedAverageAge:
rtcSession.statistics.counters.roomEventEncryptionKeysReceived > 0

Check failure on line 102 in src/analytics/PosthogEvents.ts

View workflow job for this annotation

GitHub Actions / Lint, format & type check

Property 'statistics' does not exist on type 'MatrixRTCSession'.
? rtcSession.statistics.totals

Check failure on line 103 in src/analytics/PosthogEvents.ts

View workflow job for this annotation

GitHub Actions / Lint, format & type check

Property 'statistics' does not exist on type 'MatrixRTCSession'.
.roomEventEncryptionKeysReceivedTotalAge /
rtcSession.statistics.counters.roomEventEncryptionKeysReceived

Check failure on line 105 in src/analytics/PosthogEvents.ts

View workflow job for this annotation

GitHub Actions / Lint, format & type check

Property 'statistics' does not exist on type 'MatrixRTCSession'.
: 0,

Check warning on line 106 in src/analytics/PosthogEvents.ts

View check run for this annotation

Codecov / codecov/patch

src/analytics/PosthogEvents.ts#L86-L106

Added lines #L86 - L106 were not covered by tests
},
{ 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";

Check warning on line 21 in src/home/RegisteredView.tsx

View check run for this annotation

Codecov / codecov/patch

src/home/RegisteredView.tsx#L21

Added line #L21 was not covered by tests
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 },
};

Check warning on line 58 in src/home/RegisteredView.tsx

View check run for this annotation

Codecov / codecov/patch

src/home/RegisteredView.tsx#L48-L58

Added lines #L48 - L58 were not covered by tests

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");

Check warning on line 74 in src/home/RegisteredView.tsx

View check run for this annotation

Codecov / codecov/patch

src/home/RegisteredView.tsx#L73-L74

Added lines #L73 - L74 were not covered by tests

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(

Check warning on line 90 in src/home/RegisteredView.tsx

View check run for this annotation

Codecov / codecov/patch

src/home/RegisteredView.tsx#L90

Added line #L90 was not covered by tests
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,

Check warning on line 93 in src/home/RegisteredView.tsx

View check run for this annotation

Codecov / codecov/patch

src/home/RegisteredView.tsx#L93

Added line #L93 was not covered by tests
);

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

Check warning on line 96 in src/home/RegisteredView.tsx

View check run for this annotation

Codecov / codecov/patch

src/home/RegisteredView.tsx#L96

Added line #L96 was not covered by tests
}

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

Check warning on line 112 in src/home/RegisteredView.tsx

View check run for this annotation

Codecov / codecov/patch

src/home/RegisteredView.tsx#L112

Added line #L112 was not covered by tests
);

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)

Check warning on line 155 in src/home/RegisteredView.tsx

View check run for this annotation

Codecov / codecov/patch

src/home/RegisteredView.tsx#L151-L155

Added lines #L151 - L155 were not covered by tests
}
values={Object.keys(encryptionOptions).map((value) => [
value,
encryptionOptions[value as keyof typeof encryptionOptions]
.label,
])}
placeholder=""
/>

Check warning on line 163 in src/home/RegisteredView.tsx

View check run for this annotation

Codecov / codecov/patch

src/home/RegisteredView.tsx#L157-L163

Added lines #L157 - L163 were not covered by tests
<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";

Check warning on line 21 in src/home/UnauthenticatedView.tsx

View check run for this annotation

Codecov / codecov/patch

src/home/UnauthenticatedView.tsx#L21

Added line #L21 was not covered by tests
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 },
};

Check warning on line 57 in src/home/UnauthenticatedView.tsx

View check run for this annotation

Codecov / codecov/patch

src/home/UnauthenticatedView.tsx#L47-L57

Added lines #L47 - L57 were not covered by tests

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");

Check warning on line 68 in src/home/UnauthenticatedView.tsx

View check run for this annotation

Codecov / codecov/patch

src/home/UnauthenticatedView.tsx#L67-L68

Added lines #L67 - L68 were not covered by tests

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;

Check warning on line 101 in src/home/UnauthenticatedView.tsx

View check run for this annotation

Codecov / codecov/patch

src/home/UnauthenticatedView.tsx#L100-L101

Added lines #L100 - L101 were not covered by tests
try {
createRoomResult = await createRoom(
const res = await createRoom(

Check warning on line 103 in src/home/UnauthenticatedView.tsx

View check run for this annotation

Codecov / codecov/patch

src/home/UnauthenticatedView.tsx#L103

Added line #L103 was not covered by tests
client,
roomName,
E2eeType.SHARED_KEY,
encryptionOptions[encryption].e2eeType,

Check warning on line 106 in src/home/UnauthenticatedView.tsx

View check run for this annotation

Codecov / codecov/patch

src/home/UnauthenticatedView.tsx#L106

Added line #L106 was not covered by tests
);
roomId = res.roomId;
encryptionSystem = res.encryptionSystem;

Check warning on line 109 in src/home/UnauthenticatedView.tsx

View check run for this annotation

Codecov / codecov/patch

src/home/UnauthenticatedView.tsx#L108-L109

Added lines #L108 - L109 were not covered by tests
} 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));

Check warning on line 140 in src/home/UnauthenticatedView.tsx

View check run for this annotation

Codecov / codecov/patch

src/home/UnauthenticatedView.tsx#L140

Added line #L140 was not covered by tests
}

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

Check warning on line 157 in src/home/UnauthenticatedView.tsx

View check run for this annotation

Codecov / codecov/patch

src/home/UnauthenticatedView.tsx#L157

Added line #L157 was not covered by tests
],
);

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)

Check warning on line 224 in src/home/UnauthenticatedView.tsx

View check run for this annotation

Codecov / codecov/patch

src/home/UnauthenticatedView.tsx#L220-L224

Added lines #L220 - L224 were not covered by tests
}
values={Object.keys(encryptionOptions).map((value) => [
value,
encryptionOptions[value as keyof typeof encryptionOptions]
.label,
])}
placeholder=""
/>

Check warning on line 232 in src/home/UnauthenticatedView.tsx

View check run for this annotation

Codecov / codecov/patch

src/home/UnauthenticatedView.tsx#L226-L232

Added lines #L226 - L232 were not covered by tests

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

Check warning on line 100 in src/room/GroupCallView.tsx

View check run for this annotation

Codecov / codecov/patch

src/room/GroupCallView.tsx#L100

Added line #L100 was not covered by tests
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);

Check warning on line 194 in src/room/GroupCallView.tsx

View check run for this annotation

Codecov / codecov/patch

src/room/GroupCallView.tsx#L194

Added line #L194 was not covered by tests
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);

Check warning on line 204 in src/room/GroupCallView.tsx

View check run for this annotation

Codecov / codecov/patch

src/room/GroupCallView.tsx#L204

Added line #L204 was not covered by tests
};
// No lobby and no preload: we enter the RTC Session right away.
join();
}
}, [rtcSession, preload, skipLobby, perParticipantE2EE]);
}, [rtcSession, preload, skipLobby, e2eeSystem]);

Check warning on line 209 in src/room/GroupCallView.tsx

View check run for this annotation

Codecov / codecov/patch

src/room/GroupCallView.tsx#L209

Added line #L209 was not covered by tests

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,

Check warning on line 227 in src/room/GroupCallView.tsx

View check run for this annotation

Codecov / codecov/patch

src/room/GroupCallView.tsx#L226-L227

Added lines #L226 - L227 were not covered by tests
sendInstantly,
);

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

Check warning on line 242 in src/room/GroupCallView.tsx

View check run for this annotation

Codecov / codecov/patch

src/room/GroupCallView.tsx#L242

Added line #L242 was not covered by tests
);

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]);

Check warning on line 268 in src/room/GroupCallView.tsx

View check run for this annotation

Codecov / codecov/patch

src/room/GroupCallView.tsx#L267-L268

Added lines #L267 - L268 were not covered by tests

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)}

Check warning on line 321 in src/room/GroupCallView.tsx

View check run for this annotation

Codecov / codecov/patch

src/room/GroupCallView.tsx#L321

Added line #L321 was not covered by tests
confineToRoom={confineToRoom}
hideHeader={hideHeader}
participantCount={participantCount}
Expand Down
Loading

0 comments on commit 9a5de12

Please sign in to comment.