Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 5593872

Browse files
authored
Fix display of devices without encryption support in Settings dialog (#10977)
* Update tests to demonstrate broken behaviour * Fixes and comments * Remove exception swallowing This seems like it causes more problems than it solves.
1 parent 796ed35 commit 5593872

File tree

5 files changed

+193
-79
lines changed

5 files changed

+193
-79
lines changed

src/components/views/settings/devices/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ import { IMyDevice } from "matrix-js-sdk/src/matrix";
1818

1919
import { ExtendedDeviceInformation } from "../../../../utils/device/parseUserAgent";
2020

21-
export type DeviceWithVerification = IMyDevice & { isVerified: boolean | null };
21+
export type DeviceWithVerification = IMyDevice & {
22+
/**
23+
* `null` if the device is unknown or has not published encryption keys; otherwise a boolean
24+
* indicating whether the device has been cross-signed by a cross-signing key we trust.
25+
*/
26+
isVerified: boolean | null;
27+
};
2228
export type ExtendedDeviceAppInfo = {
2329
// eg Element Web
2430
appName?: string;

src/utils/device/isDeviceVerified.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,14 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix";
2222
* @param client - reference to the MatrixClient
2323
* @param deviceId - ID of the device to be checked
2424
*
25-
* @returns `true` if the device has been correctly cross-signed. `false` if the device is unknown or not correctly
26-
* cross-signed. `null` if there was an error fetching the device info.
25+
* @returns `null` if the device is unknown or has not published encryption keys; otherwise a boolean
26+
* indicating whether the device has been cross-signed by a cross-signing key we trust.
2727
*/
2828
export const isDeviceVerified = async (client: MatrixClient, deviceId: string): Promise<boolean | null> => {
29-
try {
30-
const trustLevel = await client.getCrypto()?.getDeviceVerificationStatus(client.getSafeUserId(), deviceId);
31-
return trustLevel?.crossSigningVerified ?? false;
32-
} catch (e) {
33-
console.error("Error getting device cross-signing info", e);
29+
const trustLevel = await client.getCrypto()?.getDeviceVerificationStatus(client.getSafeUserId(), deviceId);
30+
if (!trustLevel) {
31+
// either no crypto, or an unknown/no-e2e device
3432
return null;
3533
}
34+
return trustLevel.crossSigningVerified;
3635
};

test/components/views/settings/DevicesPanel-test.tsx

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,40 @@ import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
2525

2626
describe("<DevicesPanel />", () => {
2727
const userId = "@alice:server.org";
28-
const device1 = { device_id: "device_1" };
29-
const device2 = { device_id: "device_2" };
30-
const device3 = { device_id: "device_3" };
28+
29+
// the local device
30+
const ownDevice = { device_id: "device_1" };
31+
32+
// a device which we have verified via cross-signing
33+
const verifiedDevice = { device_id: "device_2" };
34+
35+
// a device which we have *not* verified via cross-signing
36+
const unverifiedDevice = { device_id: "device_3" };
37+
38+
// a device which is returned by `getDevices` but getDeviceVerificationStatus returns `null` for
39+
// (as it would for a device with no E2E keys).
40+
const nonCryptoDevice = { device_id: "non_crypto" };
41+
3142
const mockCrypto = {
32-
getDeviceVerificationStatus: jest.fn().mockResolvedValue({
33-
crossSigningVerified: false,
43+
getDeviceVerificationStatus: jest.fn().mockImplementation((_userId, deviceId) => {
44+
if (_userId !== userId) {
45+
throw new Error(`bad user id ${_userId}`);
46+
}
47+
if (deviceId === ownDevice.device_id || deviceId === verifiedDevice.device_id) {
48+
return { crossSigningVerified: true };
49+
} else if (deviceId === unverifiedDevice.device_id) {
50+
return {
51+
crossSigningVerified: false,
52+
};
53+
} else {
54+
return null;
55+
}
3456
}),
3557
};
3658
const mockClient = getMockClientWithEventEmitter({
3759
...mockClientMethodsUser(userId),
3860
getDevices: jest.fn(),
39-
getDeviceId: jest.fn().mockReturnValue(device1.device_id),
61+
getDeviceId: jest.fn().mockReturnValue(ownDevice.device_id),
4062
deleteMultipleDevices: jest.fn(),
4163
getStoredDevice: jest.fn().mockReturnValue(new DeviceInfo("id")),
4264
generateClientSecret: jest.fn(),
@@ -54,12 +76,14 @@ describe("<DevicesPanel />", () => {
5476
beforeEach(() => {
5577
jest.clearAllMocks();
5678

57-
mockClient.getDevices.mockReset().mockResolvedValue({ devices: [device1, device2, device3] });
79+
mockClient.getDevices
80+
.mockReset()
81+
.mockResolvedValue({ devices: [ownDevice, verifiedDevice, unverifiedDevice, nonCryptoDevice] });
5882

5983
mockClient.getPushers.mockReset().mockResolvedValue({
6084
pushers: [
6185
mkPusher({
62-
[PUSHER_DEVICE_ID.name]: device1.device_id,
86+
[PUSHER_DEVICE_ID.name]: ownDevice.device_id,
6387
[PUSHER_ENABLED.name]: true,
6488
}),
6589
],
@@ -88,16 +112,16 @@ describe("<DevicesPanel />", () => {
88112
it("deletes selected devices when interactive auth is not required", async () => {
89113
mockClient.deleteMultipleDevices.mockResolvedValue({});
90114
mockClient.getDevices
91-
.mockResolvedValueOnce({ devices: [device1, device2, device3] })
115+
.mockResolvedValueOnce({ devices: [ownDevice, verifiedDevice, unverifiedDevice] })
92116
// pretend it was really deleted on refresh
93-
.mockResolvedValueOnce({ devices: [device1, device3] });
117+
.mockResolvedValueOnce({ devices: [ownDevice, unverifiedDevice] });
94118

95119
const { container, getByTestId } = render(getComponent());
96120
await flushPromises();
97121

98122
expect(container.getElementsByClassName("mx_DevicesPanel_device").length).toEqual(3);
99123

100-
toggleDeviceSelection(container, device2.device_id);
124+
toggleDeviceSelection(container, verifiedDevice.device_id);
101125

102126
mockClient.getDevices.mockClear();
103127

@@ -106,7 +130,7 @@ describe("<DevicesPanel />", () => {
106130
});
107131

108132
expect(container.getElementsByClassName("mx_Spinner").length).toBeTruthy();
109-
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith([device2.device_id], undefined);
133+
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith([verifiedDevice.device_id], undefined);
110134

111135
await flushPromises();
112136

@@ -124,9 +148,9 @@ describe("<DevicesPanel />", () => {
124148
.mockResolvedValueOnce({});
125149

126150
mockClient.getDevices
127-
.mockResolvedValueOnce({ devices: [device1, device2, device3] })
151+
.mockResolvedValueOnce({ devices: [ownDevice, verifiedDevice, unverifiedDevice] })
128152
// pretend it was really deleted on refresh
129-
.mockResolvedValueOnce({ devices: [device1, device3] });
153+
.mockResolvedValueOnce({ devices: [ownDevice, unverifiedDevice] });
130154

131155
const { container, getByTestId, getByLabelText } = render(getComponent());
132156

@@ -135,7 +159,7 @@ describe("<DevicesPanel />", () => {
135159
// reset mock count after initial load
136160
mockClient.getDevices.mockClear();
137161

138-
toggleDeviceSelection(container, device2.device_id);
162+
toggleDeviceSelection(container, verifiedDevice.device_id);
139163

140164
act(() => {
141165
fireEvent.click(getByTestId("sign-out-devices-btn"));
@@ -145,7 +169,7 @@ describe("<DevicesPanel />", () => {
145169
// modal rendering has some weird sleeps
146170
await sleep(100);
147171

148-
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith([device2.device_id], undefined);
172+
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith([verifiedDevice.device_id], undefined);
149173

150174
const modal = document.getElementsByClassName("mx_Dialog");
151175
expect(modal).toMatchSnapshot();
@@ -159,7 +183,7 @@ describe("<DevicesPanel />", () => {
159183
await flushPromises();
160184

161185
// called again with auth
162-
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith([device2.device_id], {
186+
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith([verifiedDevice.device_id], {
163187
identifier: {
164188
type: "m.id.user",
165189
user: userId,
@@ -182,9 +206,9 @@ describe("<DevicesPanel />", () => {
182206
.mockResolvedValueOnce({});
183207

184208
mockClient.getDevices
185-
.mockResolvedValueOnce({ devices: [device1, device2, device3] })
209+
.mockResolvedValueOnce({ devices: [ownDevice, verifiedDevice, unverifiedDevice] })
186210
// pretend it was really deleted on refresh
187-
.mockResolvedValueOnce({ devices: [device1, device3] });
211+
.mockResolvedValueOnce({ devices: [ownDevice, unverifiedDevice] });
188212

189213
const { container, getByTestId } = render(getComponent());
190214

@@ -193,7 +217,7 @@ describe("<DevicesPanel />", () => {
193217
// reset mock count after initial load
194218
mockClient.getDevices.mockClear();
195219

196-
toggleDeviceSelection(container, device2.device_id);
220+
toggleDeviceSelection(container, verifiedDevice.device_id);
197221

198222
act(() => {
199223
fireEvent.click(getByTestId("sign-out-devices-btn"));

0 commit comments

Comments
 (0)