Replace uses of checkDeviceTrust with getDeviceVerificationStatus (#10663)

matrix-org/matrix-js-sdk#3287 and matrix-org/matrix-js-sdk#3303 added a new API called getDeviceVerificationStatus. Let's use it.
This commit is contained in:
Richard van der Hoff 2023-04-24 14:19:46 +01:00 committed by GitHub
parent aa8c0f5cc7
commit d7bb8043ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 286 additions and 161 deletions

View file

@ -310,7 +310,11 @@ export default class DeviceListener {
const newUnverifiedDeviceIds = new Set<string>(); const newUnverifiedDeviceIds = new Set<string>();
const isCurrentDeviceTrusted = const isCurrentDeviceTrusted =
crossSigningReady && (await cli.checkDeviceTrust(cli.getUserId()!, cli.deviceId!).isCrossSigningVerified()); crossSigningReady &&
Boolean(
(await cli.getCrypto()?.getDeviceVerificationStatus(cli.getUserId()!, cli.deviceId!))
?.crossSigningVerified,
);
// as long as cross-signing isn't ready, // as long as cross-signing isn't ready,
// you can't see or dismiss any device toasts // you can't see or dismiss any device toasts
@ -319,8 +323,10 @@ export default class DeviceListener {
for (const device of devices) { for (const device of devices) {
if (device.deviceId === cli.deviceId) continue; if (device.deviceId === cli.deviceId) continue;
const deviceTrust = await cli.checkDeviceTrust(cli.getUserId()!, device.deviceId!); const deviceTrust = await cli
if (!deviceTrust.isCrossSigningVerified() && !this.dismissed.has(device.deviceId)) { .getCrypto()!
.getDeviceVerificationStatus(cli.getUserId()!, device.deviceId!);
if (!deviceTrust?.crossSigningVerified && !this.dismissed.has(device.deviceId)) {
if (this.ourDeviceIdsAtStart?.has(device.deviceId)) { if (this.ourDeviceIdsAtStart?.has(device.deviceId)) {
oldUnverifiedDeviceIds.add(device.deviceId); oldUnverifiedDeviceIds.add(device.deviceId);
} else { } else {

View file

@ -1073,9 +1073,9 @@ export const Commands = [
}, },
); );
} }
const deviceTrust = await cli.checkDeviceTrust(userId, deviceId); const deviceTrust = await cli.getCrypto()?.getDeviceVerificationStatus(userId, deviceId);
if (deviceTrust.isVerified()) { if (deviceTrust?.isVerified()) {
if (device.getFingerprint() === fingerprint) { if (device.getFingerprint() === fingerprint) {
throw new UserFriendlyError("Session already verified!"); throw new UserFriendlyError("Session already verified!");
} else { } else {

View file

@ -79,6 +79,7 @@ import PosthogTrackers from "../../../PosthogTrackers";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { DirectoryMember, startDmOnFirstMessage } from "../../../utils/direct-messages"; import { DirectoryMember, startDmOnFirstMessage } from "../../../utils/direct-messages";
import { SdkContextClass } from "../../../contexts/SDKContext"; import { SdkContextClass } from "../../../contexts/SDKContext";
import { asyncSome } from "../../../utils/arrays";
export interface IDevice extends DeviceInfo { export interface IDevice extends DeviceInfo {
ambiguous?: boolean; ambiguous?: boolean;
@ -101,22 +102,22 @@ export const disambiguateDevices = (devices: IDevice[]): void => {
} }
}; };
export const getE2EStatus = (cli: MatrixClient, userId: string, devices: IDevice[]): E2EStatus => { export const getE2EStatus = async (cli: MatrixClient, userId: string, devices: IDevice[]): Promise<E2EStatus> => {
const isMe = userId === cli.getUserId(); const isMe = userId === cli.getUserId();
const userTrust = cli.checkUserTrust(userId); const userTrust = cli.checkUserTrust(userId);
if (!userTrust.isCrossSigningVerified()) { if (!userTrust.isCrossSigningVerified()) {
return userTrust.wasCrossSigningVerified() ? E2EStatus.Warning : E2EStatus.Normal; return userTrust.wasCrossSigningVerified() ? E2EStatus.Warning : E2EStatus.Normal;
} }
const anyDeviceUnverified = devices.some((device) => { const anyDeviceUnverified = await asyncSome(devices, async (device) => {
const { deviceId } = device; const { deviceId } = device;
// For your own devices, we use the stricter check of cross-signing // For your own devices, we use the stricter check of cross-signing
// verification to encourage everyone to trust their own devices via // verification to encourage everyone to trust their own devices via
// cross-signing so that other users can then safely trust you. // cross-signing so that other users can then safely trust you.
// For other people's devices, the more general verified check that // For other people's devices, the more general verified check that
// includes locally verified devices can be used. // includes locally verified devices can be used.
const deviceTrust = cli.checkDeviceTrust(userId, deviceId); const deviceTrust = await cli.getCrypto()?.getDeviceVerificationStatus(userId, deviceId);
return isMe ? !deviceTrust.isCrossSigningVerified() : !deviceTrust.isVerified(); return isMe ? !deviceTrust?.crossSigningVerified : !deviceTrust?.isVerified();
}); });
return anyDeviceUnverified ? E2EStatus.Warning : E2EStatus.Verified; return anyDeviceUnverified ? E2EStatus.Warning : E2EStatus.Verified;
}; };
@ -161,14 +162,20 @@ function useHasCrossSigningKeys(
export function DeviceItem({ userId, device }: { userId: string; device: IDevice }): JSX.Element { export function DeviceItem({ userId, device }: { userId: string; device: IDevice }): JSX.Element {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const isMe = userId === cli.getUserId(); const isMe = userId === cli.getUserId();
const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId);
const userTrust = cli.checkUserTrust(userId); const userTrust = cli.checkUserTrust(userId);
// For your own devices, we use the stricter check of cross-signing
// verification to encourage everyone to trust their own devices via /** is the device verified? */
// cross-signing so that other users can then safely trust you. const isVerified = useAsyncMemo(async () => {
// For other people's devices, the more general verified check that const deviceTrust = await cli.getCrypto()?.getDeviceVerificationStatus(userId, device.deviceId);
// includes locally verified devices can be used. if (!deviceTrust) return false;
const isVerified = isMe ? deviceTrust.isCrossSigningVerified() : deviceTrust.isVerified();
// For your own devices, we use the stricter check of cross-signing
// verification to encourage everyone to trust their own devices via
// cross-signing so that other users can then safely trust you.
// For other people's devices, the more general verified check that
// includes locally verified devices can be used.
return isMe ? deviceTrust.crossSigningVerified : deviceTrust.isVerified();
}, [cli, userId, device]);
const classes = classNames("mx_UserInfo_device", { const classes = classNames("mx_UserInfo_device", {
mx_UserInfo_device_verified: isVerified, mx_UserInfo_device_verified: isVerified,
@ -199,7 +206,10 @@ export function DeviceItem({ userId, device }: { userId: string; device: IDevice
let trustedLabel: string | undefined; let trustedLabel: string | undefined;
if (userTrust.isVerified()) trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted"); if (userTrust.isVerified()) trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted");
if (isVerified) { if (isVerified === undefined) {
// we're still deciding if the device is verified
return <div className={classes} title={device.deviceId} />;
} else if (isVerified) {
return ( return (
<div className={classes} title={device.deviceId}> <div className={classes} title={device.deviceId}>
<div className={iconClasses} /> <div className={iconClasses} />
@ -232,15 +242,17 @@ function DevicesSection({
const [isExpanded, setExpanded] = useState(false); const [isExpanded, setExpanded] = useState(false);
if (loading) { const deviceTrusts = useAsyncMemo(() => {
const cryptoApi = cli.getCrypto();
if (!cryptoApi) return Promise.resolve(undefined);
return Promise.all(devices.map((d) => cryptoApi.getDeviceVerificationStatus(userId, d.deviceId)));
}, [cli, userId, devices]);
if (loading || deviceTrusts === undefined) {
// still loading // still loading
return <Spinner />; return <Spinner />;
} }
if (devices === null) {
return <p>{_t("Unable to load session list")}</p>;
}
const isMe = userId === cli.getUserId(); const isMe = userId === cli.getUserId();
const deviceTrusts = devices.map((d) => cli.checkDeviceTrust(userId, d.deviceId));
let expandSectionDevices: IDevice[] = []; let expandSectionDevices: IDevice[] = [];
const unverifiedDevices: IDevice[] = []; const unverifiedDevices: IDevice[] = [];
@ -258,7 +270,7 @@ function DevicesSection({
// cross-signing so that other users can then safely trust you. // cross-signing so that other users can then safely trust you.
// For other people's devices, the more general verified check that // For other people's devices, the more general verified check that
// includes locally verified devices can be used. // includes locally verified devices can be used.
const isVerified = isMe ? deviceTrust.isCrossSigningVerified() : deviceTrust.isVerified(); const isVerified = deviceTrust && (isMe ? deviceTrust.crossSigningVerified : deviceTrust.isVerified());
if (isVerified) { if (isVerified) {
expandSectionDevices.push(device); expandSectionDevices.push(device);
@ -1611,10 +1623,12 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
const isRoomEncrypted = useIsEncrypted(cli, room); const isRoomEncrypted = useIsEncrypted(cli, room);
const devices = useDevices(user.userId) ?? []; const devices = useDevices(user.userId) ?? [];
let e2eStatus: E2EStatus | undefined; const e2eStatus = useAsyncMemo(async () => {
if (isRoomEncrypted && devices) { if (!isRoomEncrypted || !devices) {
e2eStatus = getE2EStatus(cli, user.userId, devices); return undefined;
} }
return await getE2EStatus(cli, user.userId, devices);
}, [cli, isRoomEncrypted, user.userId, devices]);
const classes = ["mx_UserInfo"]; const classes = ["mx_UserInfo"];

View file

@ -265,6 +265,8 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
public static contextType = RoomContext; public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>; public context!: React.ContextType<typeof RoomContext>;
private unmounted = false;
public constructor(props: EventTileProps, context: React.ContextType<typeof MatrixClientContext>) { public constructor(props: EventTileProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context); super(props, context);
@ -420,6 +422,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
this.props.mxEvent.removeListener(MatrixEventEvent.RelationsCreated, this.onReactionsCreated); this.props.mxEvent.removeListener(MatrixEventEvent.RelationsCreated, this.onReactionsCreated);
} }
this.props.mxEvent.off(ThreadEvent.Update, this.updateThread); this.props.mxEvent.off(ThreadEvent.Update, this.updateThread);
this.unmounted = false;
} }
public componentDidUpdate(prevProps: Readonly<EventTileProps>, prevState: Readonly<IState>): void { public componentDidUpdate(prevProps: Readonly<EventTileProps>, prevState: Readonly<IState>): void {
@ -561,7 +564,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
this.verifyEvent(); this.verifyEvent();
}; };
private verifyEvent(): void { private async verifyEvent(): Promise<void> {
// if the event was edited, show the verification info for the edit, not // if the event was edited, show the verification info for the edit, not
// the original // the original
const mxEvent = this.props.mxEvent.replacingEvent() ?? this.props.mxEvent; const mxEvent = this.props.mxEvent.replacingEvent() ?? this.props.mxEvent;
@ -590,7 +593,14 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
} }
const eventSenderTrust = const eventSenderTrust =
encryptionInfo.sender && MatrixClientPeg.get().checkDeviceTrust(senderId, encryptionInfo.sender.deviceId); senderId &&
encryptionInfo.sender &&
(await MatrixClientPeg.get()
.getCrypto()
?.getDeviceVerificationStatus(senderId, encryptionInfo.sender.deviceId));
if (this.unmounted) return;
if (!eventSenderTrust) { if (!eventSenderTrust) {
this.setState({ verified: E2EState.Unknown }); this.setState({ verified: E2EState.Unknown });
return; return;

View file

@ -33,6 +33,7 @@ import MemberAvatar from "./../avatars/MemberAvatar";
import DisambiguatedProfile from "../messages/DisambiguatedProfile"; import DisambiguatedProfile from "../messages/DisambiguatedProfile";
import UserIdentifierCustomisations from "../../../customisations/UserIdentifier"; import UserIdentifierCustomisations from "../../../customisations/UserIdentifier";
import { E2EState } from "./E2EIcon"; import { E2EState } from "./E2EIcon";
import { asyncSome } from "../../../utils/arrays";
interface IProps { interface IProps {
member: RoomMember; member: RoomMember;
@ -127,15 +128,15 @@ export default class MemberTile extends React.Component<IProps, IState> {
} }
const devices = cli.getStoredDevicesForUser(userId); const devices = cli.getStoredDevicesForUser(userId);
const anyDeviceUnverified = devices.some((device) => { const anyDeviceUnverified = await asyncSome(devices, async (device) => {
const { deviceId } = device; const { deviceId } = device;
// For your own devices, we use the stricter check of cross-signing // For your own devices, we use the stricter check of cross-signing
// verification to encourage everyone to trust their own devices via // verification to encourage everyone to trust their own devices via
// cross-signing so that other users can then safely trust you. // cross-signing so that other users can then safely trust you.
// For other people's devices, the more general verified check that // For other people's devices, the more general verified check that
// includes locally verified devices can be used. // includes locally verified devices can be used.
const deviceTrust = cli.checkDeviceTrust(userId, deviceId); const deviceTrust = await cli.getCrypto()?.getDeviceVerificationStatus(userId, deviceId);
return isMe ? !deviceTrust.isCrossSigningVerified() : !deviceTrust.isVerified(); return !deviceTrust || (isMe ? !deviceTrust.crossSigningVerified : !deviceTrust.isVerified());
}); });
this.setState({ this.setState({
e2eStatus: anyDeviceUnverified ? E2EState.Warning : E2EState.Verified, e2eStatus: anyDeviceUnverified ? E2EState.Warning : E2EState.Verified,

View file

@ -26,14 +26,15 @@ import Spinner from "../elements/Spinner";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import { deleteDevicesWithInteractiveAuth } from "./devices/deleteDevices"; import { deleteDevicesWithInteractiveAuth } from "./devices/deleteDevices";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { isDeviceVerified } from "../../../utils/device/isDeviceVerified"; import { fetchExtendedDeviceInformation } from "./devices/useOwnDevices";
import { DevicesDictionary, ExtendedDevice } from "./devices/types";
interface IProps { interface IProps {
className?: string; className?: string;
} }
interface IState { interface IState {
devices: IMyDevice[]; devices?: DevicesDictionary;
deviceLoadError?: string; deviceLoadError?: string;
selectedDevices: string[]; selectedDevices: string[];
deleting?: boolean; deleting?: boolean;
@ -47,7 +48,6 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
public constructor(props: IProps) { public constructor(props: IProps) {
super(props); super(props);
this.state = { this.state = {
devices: [],
selectedDevices: [], selectedDevices: [],
}; };
this.loadDevices = this.loadDevices.bind(this); this.loadDevices = this.loadDevices.bind(this);
@ -70,18 +70,16 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
private loadDevices(): void { private loadDevices(): void {
const cli = this.context; const cli = this.context;
cli.getDevices().then( fetchExtendedDeviceInformation(cli).then(
(resp) => { (devices) => {
if (this.unmounted) { if (this.unmounted) {
return; return;
} }
this.setState((state, props) => { this.setState((state, props) => {
const deviceIds = resp.devices.map((device) => device.device_id);
const selectedDevices = state.selectedDevices.filter((deviceId) => deviceIds.includes(deviceId));
return { return {
devices: resp.devices || [], devices: devices,
selectedDevices, selectedDevices: state.selectedDevices.filter((deviceId) => devices.hasOwnProperty(deviceId)),
}; };
}); });
}, },
@ -119,10 +117,6 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
return idA < idB ? -1 : idA > idB ? 1 : 0; return idA < idB ? -1 : idA > idB ? 1 : 0;
} }
private isDeviceVerified(device: IMyDevice): boolean | null {
return isDeviceVerified(this.context, device.device_id);
}
private onDeviceSelectionToggled = (device: IMyDevice): void => { private onDeviceSelectionToggled = (device: IMyDevice): void => {
if (this.unmounted) { if (this.unmounted) {
return; return;
@ -205,15 +199,15 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
} }
}; };
private renderDevice = (device: IMyDevice): JSX.Element => { private renderDevice = (device: ExtendedDevice): JSX.Element => {
const myDeviceId = this.context.getDeviceId(); const myDeviceId = this.context.getDeviceId()!;
const myDevice = this.state.devices.find((device) => device.device_id === myDeviceId); const myDevice = this.state.devices?.[myDeviceId];
const isOwnDevice = device.device_id === myDeviceId; const isOwnDevice = device.device_id === myDeviceId;
// If our own device is unverified, it can't verify other // If our own device is unverified, it can't verify other
// devices, it can only request verification for itself // devices, it can only request verification for itself
const canBeVerified = (myDevice && this.isDeviceVerified(myDevice)) || isOwnDevice; const canBeVerified = (myDevice && myDevice.isVerified) || isOwnDevice;
return ( return (
<DevicesPanelEntry <DevicesPanelEntry
@ -221,7 +215,7 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
device={device} device={device}
selected={this.state.selectedDevices.includes(device.device_id)} selected={this.state.selectedDevices.includes(device.device_id)}
isOwnDevice={isOwnDevice} isOwnDevice={isOwnDevice}
verified={this.isDeviceVerified(device)} verified={device.isVerified}
canBeVerified={canBeVerified} canBeVerified={canBeVerified}
onDeviceChange={this.loadDevices} onDeviceChange={this.loadDevices}
onDeviceToggled={this.onDeviceSelectionToggled} onDeviceToggled={this.onDeviceSelectionToggled}
@ -242,21 +236,21 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
return <Spinner />; return <Spinner />;
} }
const myDeviceId = this.context.getDeviceId(); const myDeviceId = this.context.getDeviceId()!;
const myDevice = devices.find((device) => device.device_id === myDeviceId); const myDevice = devices[myDeviceId];
if (!myDevice) { if (!myDevice) {
return loadError; return loadError;
} }
const otherDevices = devices.filter((device) => device.device_id !== myDeviceId); const otherDevices = Object.values(devices).filter((device) => device.device_id !== myDeviceId);
otherDevices.sort(this.deviceCompare); otherDevices.sort(this.deviceCompare);
const verifiedDevices: IMyDevice[] = []; const verifiedDevices: ExtendedDevice[] = [];
const unverifiedDevices: IMyDevice[] = []; const unverifiedDevices: ExtendedDevice[] = [];
const nonCryptoDevices: IMyDevice[] = []; const nonCryptoDevices: ExtendedDevice[] = [];
for (const device of otherDevices) { for (const device of otherDevices) {
const verified = this.isDeviceVerified(device); const verified = device.isVerified;
if (verified === true) { if (verified === true) {
verifiedDevices.push(device); verifiedDevices.push(device);
} else if (verified === false) { } else if (verified === false) {
@ -266,7 +260,7 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
} }
} }
const section = (trustIcon: JSX.Element, title: string, deviceList: IMyDevice[]): JSX.Element => { const section = (trustIcon: JSX.Element, title: string, deviceList: ExtendedDevice[]): JSX.Element => {
if (deviceList.length === 0) { if (deviceList.length === 0) {
return <React.Fragment />; return <React.Fragment />;
} }

View file

@ -50,24 +50,26 @@ const parseDeviceExtendedInformation = (matrixClient: MatrixClient, device: IMyD
}; };
}; };
const fetchDevicesWithVerification = async (matrixClient: MatrixClient): Promise<DevicesState["devices"]> => { /**
* Fetch extended details of the user's own devices
*
* @param matrixClient - Matrix Client
* @returns A dictionary mapping from device ID to ExtendedDevice
*/
export async function fetchExtendedDeviceInformation(matrixClient: MatrixClient): Promise<DevicesDictionary> {
const { devices } = await matrixClient.getDevices(); const { devices } = await matrixClient.getDevices();
const devicesDict = devices.reduce( const devicesDict: DevicesDictionary = {};
(acc, device: IMyDevice) => ({ for (const device of devices) {
...acc, devicesDict[device.device_id] = {
[device.device_id]: { ...device,
...device, isVerified: await isDeviceVerified(matrixClient, device.device_id),
isVerified: isDeviceVerified(matrixClient, device.device_id), ...parseDeviceExtendedInformation(matrixClient, device),
...parseDeviceExtendedInformation(matrixClient, device), ...parseUserAgent(device[UNSTABLE_MSC3852_LAST_SEEN_UA.name]),
...parseUserAgent(device[UNSTABLE_MSC3852_LAST_SEEN_UA.name]), };
}, }
}),
{},
);
return devicesDict; return devicesDict;
}; }
export enum OwnDevicesError { export enum OwnDevicesError {
Unsupported = "Unsupported", Unsupported = "Unsupported",
@ -112,7 +114,7 @@ export const useOwnDevices = (): DevicesState => {
const refreshDevices = useCallback(async (): Promise<void> => { const refreshDevices = useCallback(async (): Promise<void> => {
setIsLoadingDeviceList(true); setIsLoadingDeviceList(true);
try { try {
const devices = await fetchDevicesWithVerification(matrixClient); const devices = await fetchExtendedDeviceInformation(matrixClient);
setDevices(devices); setDevices(devices);
const { pushers } = await matrixClient.getPushers(); const { pushers } = await matrixClient.getPushers();

View file

@ -2256,7 +2256,6 @@
"Room settings": "Room settings", "Room settings": "Room settings",
"Trusted": "Trusted", "Trusted": "Trusted",
"Not trusted": "Not trusted", "Not trusted": "Not trusted",
"Unable to load session list": "Unable to load session list",
"%(count)s verified sessions|other": "%(count)s verified sessions", "%(count)s verified sessions|other": "%(count)s verified sessions",
"%(count)s verified sessions|one": "1 verified session", "%(count)s verified sessions|one": "1 verified session",
"Hide verified sessions": "Hide verified sessions", "Hide verified sessions": "Hide verified sessions",

View file

@ -109,14 +109,20 @@ export class SetupEncryptionStore extends EventEmitter {
const dehydratedDevice = await cli.getDehydratedDevice(); const dehydratedDevice = await cli.getDehydratedDevice();
const ownUserId = cli.getUserId()!; const ownUserId = cli.getUserId()!;
const crossSigningInfo = cli.getStoredCrossSigningForUser(ownUserId); const crossSigningInfo = cli.getStoredCrossSigningForUser(ownUserId);
this.hasDevicesToVerifyAgainst = cli this.hasDevicesToVerifyAgainst = cli.getStoredDevicesForUser(ownUserId).some((device) => {
.getStoredDevicesForUser(ownUserId) if (!device.getIdentityKey() || (dehydratedDevice && device.deviceId == dehydratedDevice?.device_id)) {
.some( return false;
(device) => }
device.getIdentityKey() && // check if the device is signed by the cross-signing key stored for our user. Note that this is
(!dehydratedDevice || device.deviceId != dehydratedDevice.device_id) && // *different* to calling `cryptoApi.getDeviceVerificationStatus`, because even if we have stored
crossSigningInfo?.checkDeviceTrust(crossSigningInfo, device, false, true).isCrossSigningVerified(), // a cross-signing key for our user, we don't necessarily trust it yet (In legacy Crypto, we have not
); // yet imported it into `Crypto.crossSigningInfo`, which for maximal confusion is a different object to
// `Crypto.getStoredCrossSigningForUser(ownUserId)`).
//
// TODO: figure out wtf to to here for rust-crypto
const verificationStatus = crossSigningInfo?.checkDeviceTrust(crossSigningInfo, device, false, true);
return !!verificationStatus?.isCrossSigningVerified();
});
this.phase = Phase.Intro; this.phase = Phase.Intro;
this.emit("update"); this.emit("update");

View file

@ -48,7 +48,7 @@ export const showToast = async (deviceId: string): Promise<void> => {
const device = await cli.getDevice(deviceId); const device = await cli.getDevice(deviceId);
const extendedDevice = { const extendedDevice = {
...device, ...device,
isVerified: isDeviceVerified(cli, deviceId), isVerified: await isDeviceVerified(cli, deviceId),
deviceType: DeviceType.Unknown, deviceType: DeviceType.Unknown,
}; };

View file

@ -18,6 +18,7 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import DMRoomMap from "./DMRoomMap"; import DMRoomMap from "./DMRoomMap";
import { asyncSome } from "./arrays";
export enum E2EStatus { export enum E2EStatus {
Warning = "warning", Warning = "warning",
@ -54,8 +55,9 @@ export async function shieldStatusForRoom(client: MatrixClient, room: Room): Pro
const targets = includeUser ? [...verified, client.getUserId()!] : verified; const targets = includeUser ? [...verified, client.getUserId()!] : verified;
for (const userId of targets) { for (const userId of targets) {
const devices = client.getStoredDevicesForUser(userId); const devices = client.getStoredDevicesForUser(userId);
const anyDeviceNotVerified = devices.some(({ deviceId }) => { const anyDeviceNotVerified = await asyncSome(devices, async ({ deviceId }) => {
return !client.checkDeviceTrust(userId, deviceId).isVerified(); const verificationStatus = await client.getCrypto()?.getDeviceVerificationStatus(userId, deviceId);
return !verificationStatus?.isVerified();
}); });
if (anyDeviceNotVerified) { if (anyDeviceNotVerified) {
return E2EStatus.Warning; return E2EStatus.Warning;

View file

@ -324,6 +324,16 @@ export async function asyncEvery<T>(values: T[], predicate: (value: T) => Promis
return true; return true;
} }
/**
* Async version of Array.some.
*/
export async function asyncSome<T>(values: T[], predicate: (value: T) => Promise<boolean>): Promise<boolean> {
for (const value of values) {
if (await predicate(value)) return true;
}
return false;
}
export function filterBoolean<T>(values: Array<T | null | undefined>): T[] { export function filterBoolean<T>(values: Array<T | null | undefined>): T[] {
return values.filter(Boolean) as T[]; return values.filter(Boolean) as T[];
} }

View file

@ -25,10 +25,10 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix";
* @returns `true` if the device has been correctly cross-signed. `false` if the device is unknown or not correctly * @returns `true` if the device has been correctly cross-signed. `false` if the device is unknown or not correctly
* cross-signed. `null` if there was an error fetching the device info. * cross-signed. `null` if there was an error fetching the device info.
*/ */
export const isDeviceVerified = (client: MatrixClient, deviceId: string): boolean | null => { export const isDeviceVerified = async (client: MatrixClient, deviceId: string): Promise<boolean | null> => {
try { try {
const trustLevel = client.checkDeviceTrust(client.getSafeUserId(), deviceId); const trustLevel = await client.getCrypto()?.getDeviceVerificationStatus(client.getSafeUserId(), deviceId);
return trustLevel.isCrossSigningVerified(); return trustLevel?.crossSigningVerified ?? false;
} catch (e) { } catch (e) {
console.error("Error getting device cross-signing info", e); console.error("Error getting device cross-signing info", e);
return null; return null;

View file

@ -15,10 +15,10 @@ limitations under the License.
*/ */
import { Mocked, mocked } from "jest-mock"; import { Mocked, mocked } from "jest-mock";
import { MatrixEvent, Room, MatrixClient } from "matrix-js-sdk/src/matrix"; import { MatrixEvent, Room, MatrixClient, DeviceVerificationStatus, CryptoApi } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
import { CrossSigningInfo, DeviceTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning";
import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
@ -60,6 +60,7 @@ const flushPromises = async () => await new Promise(process.nextTick);
describe("DeviceListener", () => { describe("DeviceListener", () => {
let mockClient: Mocked<MatrixClient> | undefined; let mockClient: Mocked<MatrixClient> | undefined;
let mockCrypto: Mocked<CryptoApi> | undefined;
// spy on various toasts' hide and show functions // spy on various toasts' hide and show functions
// easier than mocking // easier than mocking
@ -75,6 +76,11 @@ describe("DeviceListener", () => {
mockPlatformPeg({ mockPlatformPeg({
getAppVersion: jest.fn().mockResolvedValue("1.2.3"), getAppVersion: jest.fn().mockResolvedValue("1.2.3"),
}); });
mockCrypto = {
getDeviceVerificationStatus: jest.fn().mockResolvedValue({
crossSigningVerified: false,
}),
} as unknown as Mocked<CryptoApi>;
mockClient = getMockClientWithEventEmitter({ mockClient = getMockClientWithEventEmitter({
isGuest: jest.fn(), isGuest: jest.fn(),
getUserId: jest.fn().mockReturnValue(userId), getUserId: jest.fn().mockReturnValue(userId),
@ -97,7 +103,7 @@ describe("DeviceListener", () => {
setAccountData: jest.fn(), setAccountData: jest.fn(),
getAccountData: jest.fn(), getAccountData: jest.fn(),
deleteAccountData: jest.fn(), deleteAccountData: jest.fn(),
checkDeviceTrust: jest.fn().mockReturnValue(new DeviceTrustLevel(false, false, false, false)), getCrypto: jest.fn().mockReturnValue(mockCrypto),
}); });
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
@ -391,14 +397,14 @@ describe("DeviceListener", () => {
const device2 = new DeviceInfo("d2"); const device2 = new DeviceInfo("d2");
const device3 = new DeviceInfo("d3"); const device3 = new DeviceInfo("d3");
const deviceTrustVerified = new DeviceTrustLevel(true, false, false, false); const deviceTrustVerified = new DeviceVerificationStatus({ crossSigningVerified: true });
const deviceTrustUnverified = new DeviceTrustLevel(false, false, false, false); const deviceTrustUnverified = new DeviceVerificationStatus({});
beforeEach(() => { beforeEach(() => {
mockClient!.isCrossSigningReady.mockResolvedValue(true); mockClient!.isCrossSigningReady.mockResolvedValue(true);
mockClient!.getStoredDevicesForUser.mockReturnValue([currentDevice, device2, device3]); mockClient!.getStoredDevicesForUser.mockReturnValue([currentDevice, device2, device3]);
// all devices verified by default // all devices verified by default
mockClient!.checkDeviceTrust.mockReturnValue(deviceTrustVerified); mockCrypto!.getDeviceVerificationStatus.mockResolvedValue(deviceTrustVerified);
mockClient!.deviceId = currentDevice.deviceId; mockClient!.deviceId = currentDevice.deviceId;
jest.spyOn(SettingsStore, "getValue").mockImplementation( jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === UIFeature.BulkUnverifiedSessionsReminder, (settingName) => settingName === UIFeature.BulkUnverifiedSessionsReminder,
@ -423,7 +429,7 @@ describe("DeviceListener", () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
// currentDevice, device2 are verified, device3 is unverified // currentDevice, device2 are verified, device3 is unverified
// ie if reminder was enabled it should be shown // ie if reminder was enabled it should be shown
mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => { mockCrypto!.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => {
switch (deviceId) { switch (deviceId) {
case currentDevice.deviceId: case currentDevice.deviceId:
case device2.deviceId: case device2.deviceId:
@ -438,7 +444,7 @@ describe("DeviceListener", () => {
it("hides toast when current device is unverified", async () => { it("hides toast when current device is unverified", async () => {
// device2 verified, current and device3 unverified // device2 verified, current and device3 unverified
mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => { mockCrypto!.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => {
switch (deviceId) { switch (deviceId) {
case device2.deviceId: case device2.deviceId:
return deviceTrustVerified; return deviceTrustVerified;
@ -454,7 +460,7 @@ describe("DeviceListener", () => {
it("hides toast when reminder is snoozed", async () => { it("hides toast when reminder is snoozed", async () => {
mocked(isBulkUnverifiedDeviceReminderSnoozed).mockReturnValue(true); mocked(isBulkUnverifiedDeviceReminderSnoozed).mockReturnValue(true);
// currentDevice, device2 are verified, device3 is unverified // currentDevice, device2 are verified, device3 is unverified
mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => { mockCrypto!.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => {
switch (deviceId) { switch (deviceId) {
case currentDevice.deviceId: case currentDevice.deviceId:
case device2.deviceId: case device2.deviceId:
@ -470,7 +476,7 @@ describe("DeviceListener", () => {
it("shows toast with unverified devices at app start", async () => { it("shows toast with unverified devices at app start", async () => {
// currentDevice, device2 are verified, device3 is unverified // currentDevice, device2 are verified, device3 is unverified
mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => { mockCrypto!.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => {
switch (deviceId) { switch (deviceId) {
case currentDevice.deviceId: case currentDevice.deviceId:
case device2.deviceId: case device2.deviceId:
@ -488,7 +494,7 @@ describe("DeviceListener", () => {
it("hides toast when unverified sessions at app start have been dismissed", async () => { it("hides toast when unverified sessions at app start have been dismissed", async () => {
// currentDevice, device2 are verified, device3 is unverified // currentDevice, device2 are verified, device3 is unverified
mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => { mockCrypto!.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => {
switch (deviceId) { switch (deviceId) {
case currentDevice.deviceId: case currentDevice.deviceId:
case device2.deviceId: case device2.deviceId:
@ -510,7 +516,7 @@ describe("DeviceListener", () => {
it("hides toast when unverified sessions are added after app start", async () => { it("hides toast when unverified sessions are added after app start", async () => {
// currentDevice, device2 are verified, device3 is unverified // currentDevice, device2 are verified, device3 is unverified
mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => { mockCrypto!.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => {
switch (deviceId) { switch (deviceId) {
case currentDevice.deviceId: case currentDevice.deviceId:
case device2.deviceId: case device2.deviceId:

View file

@ -18,9 +18,18 @@ import React from "react";
import { fireEvent, render, screen, waitFor, cleanup, act, within } from "@testing-library/react"; import { fireEvent, render, screen, waitFor, cleanup, act, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { Mocked, mocked } from "jest-mock"; import { Mocked, mocked } from "jest-mock";
import { Room, User, MatrixClient, RoomMember, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; import {
Room,
User,
MatrixClient,
RoomMember,
MatrixEvent,
EventType,
CryptoApi,
DeviceVerificationStatus,
} from "matrix-js-sdk/src/matrix";
import { Phase, VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { Phase, VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import { DeviceTrustLevel, UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
import { defer } from "matrix-js-sdk/src/utils"; import { defer } from "matrix-js-sdk/src/utils";
@ -79,6 +88,7 @@ const defaultUser = new User(defaultUserId);
let mockRoom: Mocked<Room>; let mockRoom: Mocked<Room>;
let mockSpace: Mocked<Room>; let mockSpace: Mocked<Room>;
let mockClient: Mocked<MatrixClient>; let mockClient: Mocked<MatrixClient>;
let mockCrypto: Mocked<CryptoApi>;
beforeEach(() => { beforeEach(() => {
mockRoom = mocked({ mockRoom = mocked({
@ -115,6 +125,10 @@ beforeEach(() => {
getEventReadUpTo: jest.fn(), getEventReadUpTo: jest.fn(),
} as unknown as Room); } as unknown as Room);
mockCrypto = mocked({
getDeviceVerificationStatus: jest.fn(),
} as unknown as CryptoApi);
mockClient = mocked({ mockClient = mocked({
getUser: jest.fn(), getUser: jest.fn(),
isGuest: jest.fn().mockReturnValue(false), isGuest: jest.fn().mockReturnValue(false),
@ -134,13 +148,13 @@ beforeEach(() => {
currentState: { currentState: {
on: jest.fn(), on: jest.fn(),
}, },
checkDeviceTrust: jest.fn(),
checkUserTrust: jest.fn(), checkUserTrust: jest.fn(),
getRoom: jest.fn(), getRoom: jest.fn(),
credentials: {}, credentials: {},
setPowerLevel: jest.fn(), setPowerLevel: jest.fn(),
downloadKeys: jest.fn(), downloadKeys: jest.fn(),
getStoredDevicesForUser: jest.fn(), getStoredDevicesForUser: jest.fn(),
getCrypto: jest.fn().mockReturnValue(mockCrypto),
} as unknown as MatrixClient); } as unknown as MatrixClient);
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
@ -251,7 +265,6 @@ describe("<UserInfo />", () => {
beforeEach(() => { beforeEach(() => {
mockClient.isCryptoEnabled.mockReturnValue(true); mockClient.isCryptoEnabled.mockReturnValue(true);
mockClient.checkUserTrust.mockReturnValue(new UserTrustLevel(false, false, false)); mockClient.checkUserTrust.mockReturnValue(new UserTrustLevel(false, false, false));
mockClient.checkDeviceTrust.mockReturnValue(new DeviceTrustLevel(false, false, false, false));
const device1 = DeviceInfo.fromStorage( const device1 = DeviceInfo.fromStorage(
{ {
@ -370,10 +383,10 @@ describe("<DeviceItem />", () => {
mockClient.checkUserTrust.mockReturnValue({ isVerified: () => isVerified } as UserTrustLevel); mockClient.checkUserTrust.mockReturnValue({ isVerified: () => isVerified } as UserTrustLevel);
}; };
const setMockDeviceTrust = (isVerified = false, isCrossSigningVerified = false) => { const setMockDeviceTrust = (isVerified = false, isCrossSigningVerified = false) => {
mockClient.checkDeviceTrust.mockReturnValue({ mockCrypto.getDeviceVerificationStatus.mockResolvedValue({
isVerified: () => isVerified, isVerified: () => isVerified,
isCrossSigningVerified: () => isCrossSigningVerified, crossSigningVerified: isCrossSigningVerified,
} as DeviceTrustLevel); } as DeviceVerificationStatus);
}; };
const mockVerifyDevice = jest.spyOn(mockVerification, "verifyDevice"); const mockVerifyDevice = jest.spyOn(mockVerification, "verifyDevice");
@ -384,7 +397,7 @@ describe("<DeviceItem />", () => {
}); });
afterEach(() => { afterEach(() => {
mockClient.checkDeviceTrust.mockReset(); mockCrypto.getDeviceVerificationStatus.mockReset();
mockClient.checkUserTrust.mockReset(); mockClient.checkUserTrust.mockReset();
mockVerifyDevice.mockClear(); mockVerifyDevice.mockClear();
}); });
@ -393,32 +406,36 @@ describe("<DeviceItem />", () => {
mockVerifyDevice.mockRestore(); mockVerifyDevice.mockRestore();
}); });
it("with unverified user and device, displays button without a label", () => { it("with unverified user and device, displays button without a label", async () => {
renderComponent(); renderComponent();
await act(flushPromises);
expect(screen.getByRole("button", { name: device.getDisplayName()! })).toBeInTheDocument; expect(screen.getByRole("button", { name: device.getDisplayName()! })).toBeInTheDocument;
expect(screen.queryByText(/trusted/i)).not.toBeInTheDocument(); expect(screen.queryByText(/trusted/i)).not.toBeInTheDocument();
}); });
it("with verified user only, displays button with a 'Not trusted' label", () => { it("with verified user only, displays button with a 'Not trusted' label", async () => {
setMockUserTrust(true); setMockUserTrust(true);
renderComponent(); renderComponent();
await act(flushPromises);
expect(screen.getByRole("button", { name: `${device.getDisplayName()} Not trusted` })).toBeInTheDocument; expect(screen.getByRole("button", { name: `${device.getDisplayName()} Not trusted` })).toBeInTheDocument;
}); });
it("with verified device only, displays no button without a label", () => { it("with verified device only, displays no button without a label", async () => {
setMockDeviceTrust(true); setMockDeviceTrust(true);
renderComponent(); renderComponent();
await act(flushPromises);
expect(screen.getByText(device.getDisplayName()!)).toBeInTheDocument(); expect(screen.getByText(device.getDisplayName()!)).toBeInTheDocument();
expect(screen.queryByText(/trusted/)).not.toBeInTheDocument(); expect(screen.queryByText(/trusted/)).not.toBeInTheDocument();
}); });
it("when userId is the same as userId from client, uses isCrossSigningVerified to determine if button is shown", () => { it("when userId is the same as userId from client, uses isCrossSigningVerified to determine if button is shown", async () => {
mockClient.getSafeUserId.mockReturnValueOnce(defaultUserId); mockClient.getSafeUserId.mockReturnValueOnce(defaultUserId);
mockClient.getUserId.mockReturnValueOnce(defaultUserId); mockClient.getUserId.mockReturnValueOnce(defaultUserId);
renderComponent(); renderComponent();
await act(flushPromises);
// set trust to be false for isVerified, true for isCrossSigningVerified // set trust to be false for isVerified, true for isCrossSigningVerified
setMockDeviceTrust(false, true); setMockDeviceTrust(false, true);
@ -428,10 +445,11 @@ describe("<DeviceItem />", () => {
expect(screen.getByText(device.getDisplayName()!)).toBeInTheDocument(); expect(screen.getByText(device.getDisplayName()!)).toBeInTheDocument();
}); });
it("with verified user and device, displays no button and a 'Trusted' label", () => { it("with verified user and device, displays no button and a 'Trusted' label", async () => {
setMockUserTrust(true); setMockUserTrust(true);
setMockDeviceTrust(true); setMockDeviceTrust(true);
renderComponent(); renderComponent();
await act(flushPromises);
expect(screen.queryByRole("button")).not.toBeInTheDocument; expect(screen.queryByRole("button")).not.toBeInTheDocument;
expect(screen.getByText(device.getDisplayName()!)).toBeInTheDocument(); expect(screen.getByText(device.getDisplayName()!)).toBeInTheDocument();
@ -441,6 +459,7 @@ describe("<DeviceItem />", () => {
it("does not call verifyDevice if client.getUser returns null", async () => { it("does not call verifyDevice if client.getUser returns null", async () => {
mockClient.getUser.mockReturnValueOnce(null); mockClient.getUser.mockReturnValueOnce(null);
renderComponent(); renderComponent();
await act(flushPromises);
const button = screen.getByRole("button", { name: device.getDisplayName()! }); const button = screen.getByRole("button", { name: device.getDisplayName()! });
expect(button).toBeInTheDocument; expect(button).toBeInTheDocument;
@ -455,6 +474,7 @@ describe("<DeviceItem />", () => {
// even more mocking // even more mocking
mockClient.isGuest.mockReturnValueOnce(true); mockClient.isGuest.mockReturnValueOnce(true);
renderComponent(); renderComponent();
await act(flushPromises);
const button = screen.getByRole("button", { name: device.getDisplayName()! }); const button = screen.getByRole("button", { name: device.getDisplayName()! });
expect(button).toBeInTheDocument; expect(button).toBeInTheDocument;

View file

@ -19,7 +19,7 @@ import { render, waitFor, screen, act, fireEvent } from "@testing-library/react"
import { mocked } from "jest-mock"; import { mocked } from "jest-mock";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client"; import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
import { TweakName } from "matrix-js-sdk/src/matrix"; import { CryptoApi, TweakName } from "matrix-js-sdk/src/matrix";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room"; import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
import { DeviceTrustLevel, UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; import { DeviceTrustLevel, UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
@ -30,7 +30,7 @@ import EventTile, { EventTileProps } from "../../../../src/components/views/room
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext"; import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { getRoomContext, mkEncryptedEvent, mkEvent, mkMessage, stubClient } from "../../../test-utils"; import { flushPromises, getRoomContext, mkEncryptedEvent, mkEvent, mkMessage, stubClient } from "../../../test-utils";
import { mkThread } from "../../../test-utils/threads"; import { mkThread } from "../../../test-utils/threads";
import DMRoomMap from "../../../../src/utils/DMRoomMap"; import DMRoomMap from "../../../../src/utils/DMRoomMap";
import dis from "../../../../src/dispatcher/dispatcher"; import dis from "../../../../src/dispatcher/dispatcher";
@ -221,13 +221,16 @@ describe("EventTile", () => {
// a version of checkDeviceTrust which says that TRUSTED_DEVICE is trusted, and others are not. // a version of checkDeviceTrust which says that TRUSTED_DEVICE is trusted, and others are not.
const trustedDeviceTrustLevel = DeviceTrustLevel.fromUserTrustLevel(trustedUserTrustLevel, true, false); const trustedDeviceTrustLevel = DeviceTrustLevel.fromUserTrustLevel(trustedUserTrustLevel, true, false);
const untrustedDeviceTrustLevel = DeviceTrustLevel.fromUserTrustLevel(trustedUserTrustLevel, false, false); const untrustedDeviceTrustLevel = DeviceTrustLevel.fromUserTrustLevel(trustedUserTrustLevel, false, false);
client.checkDeviceTrust = (userId, deviceId) => { const mockCrypto = {
if (deviceId === TRUSTED_DEVICE.deviceId) { getDeviceVerificationStatus: async (userId: string, deviceId: string) => {
return trustedDeviceTrustLevel; if (deviceId === TRUSTED_DEVICE.deviceId) {
} else { return trustedDeviceTrustLevel;
return untrustedDeviceTrustLevel; } else {
} return untrustedDeviceTrustLevel;
}; }
},
} as unknown as CryptoApi;
client.getCrypto = () => mockCrypto;
}); });
it("shows a warning for an event from an unverified device", async () => { it("shows a warning for an event from an unverified device", async () => {
@ -243,6 +246,7 @@ describe("EventTile", () => {
} as IEncryptedEventInfo); } as IEncryptedEventInfo);
const { container } = getComponent(); const { container } = getComponent();
await act(flushPromises);
const eventTiles = container.getElementsByClassName("mx_EventTile"); const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1); expect(eventTiles).toHaveLength(1);
@ -270,6 +274,7 @@ describe("EventTile", () => {
} as IEncryptedEventInfo); } as IEncryptedEventInfo);
const { container } = getComponent(); const { container } = getComponent();
await act(flushPromises);
const eventTiles = container.getElementsByClassName("mx_EventTile"); const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1); expect(eventTiles).toHaveLength(1);
@ -295,6 +300,7 @@ describe("EventTile", () => {
} as IEncryptedEventInfo); } as IEncryptedEventInfo);
const { container } = getComponent(); const { container } = getComponent();
await act(flushPromises);
const eventTiles = container.getElementsByClassName("mx_EventTile"); const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1); expect(eventTiles).toHaveLength(1);
@ -317,8 +323,9 @@ describe("EventTile", () => {
sender: UNTRUSTED_DEVICE, sender: UNTRUSTED_DEVICE,
} as IEncryptedEventInfo); } as IEncryptedEventInfo);
act(() => { await act(async () => {
mxEvent.makeReplaced(replacementEvent); mxEvent.makeReplaced(replacementEvent);
flushPromises();
}); });
// check it was updated // check it was updated
@ -345,6 +352,7 @@ describe("EventTile", () => {
} as IEncryptedEventInfo); } as IEncryptedEventInfo);
const { container } = getComponent(); const { container } = getComponent();
await act(flushPromises);
const eventTiles = container.getElementsByClassName("mx_EventTile"); const eventTiles = container.getElementsByClassName("mx_EventTile");
expect(eventTiles).toHaveLength(1); expect(eventTiles).toHaveLength(1);
@ -363,8 +371,9 @@ describe("EventTile", () => {
event: true, event: true,
}); });
act(() => { await act(async () => {
mxEvent.makeReplaced(replacementEvent); mxEvent.makeReplaced(replacementEvent);
await flushPromises();
}); });
// check it was updated // check it was updated

View file

@ -18,7 +18,6 @@ import { act, fireEvent, render } from "@testing-library/react";
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
import { sleep } from "matrix-js-sdk/src/utils"; import { sleep } from "matrix-js-sdk/src/utils";
import { PUSHER_DEVICE_ID, PUSHER_ENABLED } from "matrix-js-sdk/src/@types/event"; import { PUSHER_DEVICE_ID, PUSHER_ENABLED } from "matrix-js-sdk/src/@types/event";
import { DeviceTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
import DevicesPanel from "../../../../src/components/views/settings/DevicesPanel"; import DevicesPanel from "../../../../src/components/views/settings/DevicesPanel";
import { flushPromises, getMockClientWithEventEmitter, mkPusher, mockClientMethodsUser } from "../../../test-utils"; import { flushPromises, getMockClientWithEventEmitter, mkPusher, mockClientMethodsUser } from "../../../test-utils";
@ -29,16 +28,21 @@ describe("<DevicesPanel />", () => {
const device1 = { device_id: "device_1" }; const device1 = { device_id: "device_1" };
const device2 = { device_id: "device_2" }; const device2 = { device_id: "device_2" };
const device3 = { device_id: "device_3" }; const device3 = { device_id: "device_3" };
const mockCrypto = {
getDeviceVerificationStatus: jest.fn().mockResolvedValue({
crossSigningVerified: false,
}),
};
const mockClient = getMockClientWithEventEmitter({ const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId), ...mockClientMethodsUser(userId),
getDevices: jest.fn(), getDevices: jest.fn(),
getDeviceId: jest.fn().mockReturnValue(device1.device_id), getDeviceId: jest.fn().mockReturnValue(device1.device_id),
deleteMultipleDevices: jest.fn(), deleteMultipleDevices: jest.fn(),
checkDeviceTrust: jest.fn().mockReturnValue(new DeviceTrustLevel(false, false, false, false)),
getStoredDevice: jest.fn().mockReturnValue(new DeviceInfo("id")), getStoredDevice: jest.fn().mockReturnValue(new DeviceInfo("id")),
generateClientSecret: jest.fn(), generateClientSecret: jest.fn(),
getPushers: jest.fn(), getPushers: jest.fn(),
setPusher: jest.fn(), setPusher: jest.fn(),
getCrypto: jest.fn().mockReturnValue(mockCrypto),
}); });
const getComponent = () => ( const getComponent = () => (

View file

@ -18,7 +18,6 @@ import React from "react";
import { act, fireEvent, render, RenderResult } from "@testing-library/react"; import { act, fireEvent, render, RenderResult } from "@testing-library/react";
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { DeviceTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import { defer, sleep } from "matrix-js-sdk/src/utils"; import { defer, sleep } from "matrix-js-sdk/src/utils";
import { import {
@ -30,7 +29,10 @@ import {
PUSHER_ENABLED, PUSHER_ENABLED,
IAuthData, IAuthData,
UNSTABLE_MSC3882_CAPABILITY, UNSTABLE_MSC3882_CAPABILITY,
CryptoApi,
DeviceVerificationStatus,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";
import { clearAllModals } from "../../../../../test-utils"; import { clearAllModals } from "../../../../../test-utils";
import SessionManagerTab from "../../../../../../src/components/views/settings/tabs/user/SessionManagerTab"; import SessionManagerTab from "../../../../../../src/components/views/settings/tabs/user/SessionManagerTab";
@ -78,9 +80,14 @@ describe("<SessionManagerTab />", () => {
cancel: jest.fn(), cancel: jest.fn(),
on: jest.fn(), on: jest.fn(),
} as unknown as VerificationRequest; } as unknown as VerificationRequest;
const mockCrypto = mocked({
getDeviceVerificationStatus: jest.fn(),
} as unknown as CryptoApi);
const mockClient = getMockClientWithEventEmitter({ const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(aliceId), ...mockClientMethodsUser(aliceId),
checkDeviceTrust: jest.fn(), getCrypto: jest.fn().mockReturnValue(mockCrypto),
getDevices: jest.fn(), getDevices: jest.fn(),
getStoredDevice: jest.fn(), getStoredDevice: jest.fn(),
getDeviceId: jest.fn().mockReturnValue(deviceId), getDeviceId: jest.fn().mockReturnValue(deviceId),
@ -171,7 +178,7 @@ describe("<SessionManagerTab />", () => {
const device = [alicesDevice, alicesMobileDevice].find((device) => device.device_id === id); const device = [alicesDevice, alicesMobileDevice].find((device) => device.device_id === id);
return device ? new DeviceInfo(device.device_id) : null; return device ? new DeviceInfo(device.device_id) : null;
}); });
mockClient.checkDeviceTrust.mockReset().mockReturnValue(new DeviceTrustLevel(false, false, false, false)); mockCrypto.getDeviceVerificationStatus.mockReset().mockResolvedValue(new DeviceVerificationStatus({}));
mockClient.getDevices.mockReset().mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] }); mockClient.getDevices.mockReset().mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] });
@ -221,13 +228,13 @@ describe("<SessionManagerTab />", () => {
}); });
it("does not fail when checking device verification fails", async () => { it("does not fail when checking device verification fails", async () => {
const logSpy = jest.spyOn(console, "error").mockImplementation(() => {}); const logSpy = jest.spyOn(console, "error").mockImplementation((e) => {});
mockClient.getDevices.mockResolvedValue({ mockClient.getDevices.mockResolvedValue({
devices: [alicesDevice, alicesMobileDevice], devices: [alicesDevice, alicesMobileDevice],
}); });
const noCryptoError = new Error("End-to-end encryption disabled"); const failError = new Error("non-specific failure");
mockClient.checkDeviceTrust.mockImplementation(() => { mockCrypto.getDeviceVerificationStatus.mockImplementation(() => {
throw noCryptoError; throw failError;
}); });
render(getComponent()); render(getComponent());
@ -236,9 +243,9 @@ describe("<SessionManagerTab />", () => {
}); });
// called for each device despite error // called for each device despite error
expect(mockClient.checkDeviceTrust).toHaveBeenCalledWith(aliceId, alicesDevice.device_id); expect(mockCrypto.getDeviceVerificationStatus).toHaveBeenCalledWith(aliceId, alicesDevice.device_id);
expect(mockClient.checkDeviceTrust).toHaveBeenCalledWith(aliceId, alicesMobileDevice.device_id); expect(mockCrypto.getDeviceVerificationStatus).toHaveBeenCalledWith(aliceId, alicesMobileDevice.device_id);
expect(logSpy).toHaveBeenCalledWith("Error getting device cross-signing info", noCryptoError); expect(logSpy).toHaveBeenCalledWith("Error getting device cross-signing info", failError);
}); });
it("sets device verification status correctly", async () => { it("sets device verification status correctly", async () => {
@ -246,14 +253,14 @@ describe("<SessionManagerTab />", () => {
devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice], devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice],
}); });
mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId)); mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId));
mockClient.checkDeviceTrust.mockImplementation((_userId, deviceId) => { mockCrypto.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => {
// alices device is trusted // alices device is trusted
if (deviceId === alicesDevice.device_id) { if (deviceId === alicesDevice.device_id) {
return new DeviceTrustLevel(true, true, false, false); return new DeviceVerificationStatus({ crossSigningVerified: true, localVerified: true });
} }
// alices mobile device is not // alices mobile device is not
if (deviceId === alicesMobileDevice.device_id) { if (deviceId === alicesMobileDevice.device_id) {
return new DeviceTrustLevel(false, false, false, false); return new DeviceVerificationStatus({});
} }
// alicesOlderMobileDevice does not support encryption // alicesOlderMobileDevice does not support encryption
throw new Error("encryption not supported"); throw new Error("encryption not supported");
@ -265,7 +272,7 @@ describe("<SessionManagerTab />", () => {
await flushPromises(); await flushPromises();
}); });
expect(mockClient.checkDeviceTrust).toHaveBeenCalledTimes(3); expect(mockCrypto.getDeviceVerificationStatus).toHaveBeenCalledTimes(3);
expect( expect(
getByTestId(`device-tile-${alicesDevice.device_id}`).querySelector('[aria-label="Verified"]'), getByTestId(`device-tile-${alicesDevice.device_id}`).querySelector('[aria-label="Verified"]'),
).toBeTruthy(); ).toBeTruthy();
@ -418,7 +425,9 @@ describe("<SessionManagerTab />", () => {
devices: [alicesDevice, alicesMobileDevice], devices: [alicesDevice, alicesMobileDevice],
}); });
mockClient.getStoredDevice.mockImplementation(() => new DeviceInfo(alicesDevice.device_id)); mockClient.getStoredDevice.mockImplementation(() => new DeviceInfo(alicesDevice.device_id));
mockClient.checkDeviceTrust.mockReturnValue(new DeviceTrustLevel(true, true, false, false)); mockCrypto.getDeviceVerificationStatus.mockResolvedValue(
new DeviceVerificationStatus({ crossSigningVerified: true, localVerified: true }),
);
const { getByTestId } = render(getComponent()); const { getByTestId } = render(getComponent());
@ -520,11 +529,11 @@ describe("<SessionManagerTab />", () => {
devices: [alicesDevice, alicesMobileDevice], devices: [alicesDevice, alicesMobileDevice],
}); });
mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId)); mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId));
mockClient.checkDeviceTrust.mockImplementation((_userId, deviceId) => { mockCrypto.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => {
if (deviceId === alicesDevice.device_id) { if (deviceId === alicesDevice.device_id) {
return new DeviceTrustLevel(true, true, false, false); return new DeviceVerificationStatus({ crossSigningVerified: true, localVerified: true });
} }
return new DeviceTrustLevel(false, false, false, false); return new DeviceVerificationStatus({});
}); });
const { getByTestId } = render(getComponent()); const { getByTestId } = render(getComponent());
@ -547,12 +556,13 @@ describe("<SessionManagerTab />", () => {
devices: [alicesDevice, alicesMobileDevice], devices: [alicesDevice, alicesMobileDevice],
}); });
mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId)); mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId));
mockClient.checkDeviceTrust.mockImplementation((_userId, deviceId) => { mockCrypto.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => {
// current session verified = able to verify other sessions // current session verified = able to verify other sessions
if (deviceId === alicesDevice.device_id) { if (deviceId === alicesDevice.device_id) {
return new DeviceTrustLevel(true, true, false, false); return new DeviceVerificationStatus({ crossSigningVerified: true, localVerified: true });
} }
// but alicesMobileDevice doesn't support encryption // but alicesMobileDevice doesn't support encryption
// XXX this is not what happens if a device doesn't support encryption.
throw new Error("encryption not supported"); throw new Error("encryption not supported");
}); });
@ -581,11 +591,11 @@ describe("<SessionManagerTab />", () => {
devices: [alicesDevice, alicesMobileDevice], devices: [alicesDevice, alicesMobileDevice],
}); });
mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId)); mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId));
mockClient.checkDeviceTrust.mockImplementation((_userId, deviceId) => { mockCrypto.getDeviceVerificationStatus.mockImplementation(async (_userId, deviceId) => {
if (deviceId === alicesDevice.device_id) { if (deviceId === alicesDevice.device_id) {
return new DeviceTrustLevel(true, true, false, false); return new DeviceVerificationStatus({ crossSigningVerified: true, localVerified: true });
} }
return new DeviceTrustLevel(false, false, false, false); return new DeviceVerificationStatus({});
}); });
const { getByTestId } = render(getComponent()); const { getByTestId } = render(getComponent());

View file

@ -99,7 +99,6 @@ export function createTestClient(): MatrixClient {
getDevice: jest.fn(), getDevice: jest.fn(),
getDeviceId: jest.fn().mockReturnValue("ABCDEFGHI"), getDeviceId: jest.fn().mockReturnValue("ABCDEFGHI"),
getStoredCrossSigningForUser: jest.fn(), getStoredCrossSigningForUser: jest.fn(),
checkDeviceTrust: jest.fn(),
getStoredDevice: jest.fn(), getStoredDevice: jest.fn(),
requestVerification: jest.fn(), requestVerification: jest.fn(),
deviceId: "ABCDEFGHI", deviceId: "ABCDEFGHI",
@ -234,6 +233,7 @@ export function createTestClient(): MatrixClient {
}), }),
searchUserDirectory: jest.fn().mockResolvedValue({ limited: false, results: [] }), searchUserDirectory: jest.fn().mockResolvedValue({ limited: false, results: [] }),
getCrypto: jest.fn(),
} as unknown as MatrixClient; } as unknown as MatrixClient;
client.reEmitter = new ReEmitter(client); client.reEmitter = new ReEmitter(client);

View file

@ -18,9 +18,8 @@ import React from "react";
import { render, RenderResult, screen } from "@testing-library/react"; import { render, RenderResult, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { mocked, Mocked } from "jest-mock"; import { mocked, Mocked } from "jest-mock";
import { IMyDevice, MatrixClient } from "matrix-js-sdk/src/matrix"; import { CryptoApi, DeviceVerificationStatus, IMyDevice, MatrixClient } from "matrix-js-sdk/src/matrix";
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
import { DeviceTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
import dis from "../../src/dispatcher/dispatcher"; import dis from "../../src/dispatcher/dispatcher";
import { showToast } from "../../src/toasts/UnverifiedSessionToast"; import { showToast } from "../../src/toasts/UnverifiedSessionToast";
@ -55,7 +54,11 @@ describe("UnverifiedSessionToast", () => {
return null; return null;
}); });
client.checkDeviceTrust.mockReturnValue(new DeviceTrustLevel(true, false, false, false)); client.getCrypto.mockReturnValue({
getDeviceVerificationStatus: jest
.fn()
.mockResolvedValue(new DeviceVerificationStatus({ crossSigningVerified: true })),
} as unknown as CryptoApi);
jest.spyOn(dis, "dispatch"); jest.spyOn(dis, "dispatch");
jest.spyOn(DeviceListener.sharedInstance(), "dismissUnverifiedSessions"); jest.spyOn(DeviceListener.sharedInstance(), "dismissUnverifiedSessions");
}); });

View file

@ -22,21 +22,25 @@ import DMRoomMap from "../../src/utils/DMRoomMap";
function mkClient(selfTrust = false) { function mkClient(selfTrust = false) {
return { return {
getUserId: () => "@self:localhost", getUserId: () => "@self:localhost",
getCrypto: () => ({
getDeviceVerificationStatus: (userId: string, deviceId: string) =>
Promise.resolve({
isVerified: () => (userId === "@self:localhost" ? selfTrust : userId[2] == "T"),
}),
}),
checkUserTrust: (userId: string) => ({ checkUserTrust: (userId: string) => ({
isCrossSigningVerified: () => userId[1] == "T", isCrossSigningVerified: () => userId[1] == "T",
wasCrossSigningVerified: () => userId[1] == "T" || userId[1] == "W", wasCrossSigningVerified: () => userId[1] == "T" || userId[1] == "W",
}), }),
checkDeviceTrust: (userId: string, deviceId: string) => ({
isVerified: () => (userId === "@self:localhost" ? selfTrust : userId[2] == "T"),
}),
getStoredDevicesForUser: (userId: string) => ["DEVICE"], getStoredDevicesForUser: (userId: string) => ["DEVICE"],
} as unknown as MatrixClient; } as unknown as MatrixClient;
} }
describe("mkClient self-test", function () { describe("mkClient self-test", function () {
test.each([true, false])("behaves well for self-trust=%s", (v) => { test.each([true, false])("behaves well for self-trust=%s", async (v) => {
const client = mkClient(v); const client = mkClient(v);
expect(client.checkDeviceTrust("@self:localhost", "DEVICE").isVerified()).toBe(v); const status = await client.getCrypto()!.getDeviceVerificationStatus("@self:localhost", "DEVICE");
expect(status?.isVerified()).toBe(v);
}); });
test.each([ test.each([
@ -53,8 +57,9 @@ describe("mkClient self-test", function () {
["@TF:h", false], ["@TF:h", false],
["@FT:h", true], ["@FT:h", true],
["@FF:h", false], ["@FF:h", false],
])("behaves well for device trust %s", (userId, trust) => { ])("behaves well for device trust %s", async (userId, trust) => {
expect(mkClient().checkDeviceTrust(userId, "device").isVerified()).toBe(trust); const status = await mkClient().getCrypto()!.getDeviceVerificationStatus(userId, "device");
expect(status?.isVerified()).toBe(trust);
}); });
}); });

View file

@ -30,6 +30,7 @@ import {
GroupedArray, GroupedArray,
concat, concat,
asyncEvery, asyncEvery,
asyncSome,
} from "../../src/utils/arrays"; } from "../../src/utils/arrays";
type TestParams = { input: number[]; output: number[] }; type TestParams = { input: number[]; output: number[] };
@ -444,4 +445,27 @@ describe("arrays", () => {
expect(predicate).toHaveBeenCalledWith(2); expect(predicate).toHaveBeenCalledWith(2);
}); });
}); });
describe("asyncSome", () => {
it("when called with an empty array, it should return false", async () => {
expect(await asyncSome([], jest.fn().mockResolvedValue(true))).toBe(false);
});
it("when called with some items and the predicate resolves to false for all of them, it should return false", async () => {
const predicate = jest.fn().mockResolvedValue(false);
expect(await asyncSome([1, 2, 3], predicate)).toBe(false);
expect(predicate).toHaveBeenCalledTimes(3);
expect(predicate).toHaveBeenCalledWith(1);
expect(predicate).toHaveBeenCalledWith(2);
expect(predicate).toHaveBeenCalledWith(3);
});
it("when called with some items and the predicate resolves to true, it should short-circuit and return true", async () => {
const predicate = jest.fn().mockResolvedValueOnce(false).mockResolvedValueOnce(true);
expect(await asyncSome([1, 2, 3], predicate)).toBe(true);
expect(predicate).toHaveBeenCalledTimes(2);
expect(predicate).toHaveBeenCalledWith(1);
expect(predicate).toHaveBeenCalledWith(2);
});
});
}); });