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

Commit f28f1d9

Browse files
authored
Improve error display for messages sent from insecure devices (#93)
* Add labs option to exclude unverified devices Add a labs option which will, when set, switch into the "invisible crypto" mode of refusing to send keys to, or decrypt messages from, devices that have not been signed by their owner. * DecryptionFailureBody: better error messages Improve the error messages shown for messages from insecure devices. * playwright: factor out `createSecondBotDevice` utility * Playwright test for messages from insecure devices * fixup! DecryptionFailureBody: better error messages Use compound colour tokens, and add a background colour. * fixup! DecryptionFailureBody: better error messages Use compound spacing tokens
1 parent be2c1fc commit f28f1d9

File tree

8 files changed

+174
-17
lines changed

8 files changed

+174
-17
lines changed

playwright/e2e/crypto/event-shields.spec.ts

+9-15
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
66
Please see LICENSE files in the repository root for full details.
77
*/
88

9-
import { Page } from "@playwright/test";
10-
119
import { expect, test } from "../../element-web-test";
12-
import { autoJoin, createSharedRoomWithUser, enableKeyBackup, logIntoElement, logOutOfElement, verify } from "./utils";
13-
import { Bot } from "../../pages/bot";
14-
import { HomeserverInstance } from "../../plugins/homeserver";
10+
import {
11+
autoJoin,
12+
createSecondBotDevice,
13+
createSharedRoomWithUser,
14+
enableKeyBackup,
15+
logIntoElement,
16+
logOutOfElement,
17+
verify,
18+
} from "./utils";
1519

1620
test.describe("Cryptography", function () {
1721
test.use({
@@ -296,13 +300,3 @@ test.describe("Cryptography", function () {
296300
});
297301
});
298302
});
299-
300-
async function createSecondBotDevice(page: Page, homeserver: HomeserverInstance, bob: Bot) {
301-
const bobSecondDevice = new Bot(page, homeserver, {
302-
bootstrapSecretStorage: false,
303-
bootstrapCrossSigning: false,
304-
});
305-
bobSecondDevice.setCredentials(await homeserver.loginUser(bob.credentials.userId, bob.credentials.password));
306-
await bobSecondDevice.prepareClient();
307-
return bobSecondDevice;
308-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
Copyright 2024 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import { expect, test } from "../../element-web-test";
9+
import { autoJoin, createSecondBotDevice, createSharedRoomWithUser, verify } from "./utils";
10+
import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
11+
12+
/** Tests for the "invisible crypto" behaviour -- i.e., when the "exclude insecure devices" setting is enabled */
13+
test.describe("Invisible cryptography", () => {
14+
test.use({
15+
displayName: "Alice",
16+
botCreateOpts: { displayName: "Bob" },
17+
labsFlags: ["feature_exclude_insecure_devices"],
18+
});
19+
20+
test("Messages fail to decrypt when sender is previously verified", async ({
21+
page,
22+
bot: bob,
23+
user: aliceCredentials,
24+
app,
25+
homeserver,
26+
}) => {
27+
await app.client.bootstrapCrossSigning(aliceCredentials);
28+
await autoJoin(bob);
29+
30+
// create an encrypted room
31+
const testRoomId = await createSharedRoomWithUser(app, bob.credentials.userId, {
32+
name: "TestRoom",
33+
initial_state: [
34+
{
35+
type: "m.room.encryption",
36+
state_key: "",
37+
content: {
38+
algorithm: "m.megolm.v1.aes-sha2",
39+
},
40+
},
41+
],
42+
});
43+
44+
// Verify Bob
45+
await verify(app, bob);
46+
47+
// Bob logs in a new device and resets cross-signing
48+
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
49+
await bootstrapCrossSigningForClient(await bobSecondDevice.prepareClient(), bob.credentials, true);
50+
51+
/* should show an error for a message from a previously verified device */
52+
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
53+
const lastTile = page.locator(".mx_EventTile_last");
54+
await expect(lastTile).toContainText("Verified identity has changed");
55+
});
56+
});

playwright/e2e/crypto/utils.ts

+11
Original file line numberDiff line numberDiff line change
@@ -377,3 +377,14 @@ export async function awaitVerifier(
377377
return verificationRequest.verifier;
378378
});
379379
}
380+
381+
/** Log in a second device for the given bot user */
382+
export async function createSecondBotDevice(page: Page, homeserver: HomeserverInstance, bob: Bot) {
383+
const bobSecondDevice = new Bot(page, homeserver, {
384+
bootstrapSecretStorage: false,
385+
bootstrapCrossSigning: false,
386+
});
387+
bobSecondDevice.setCredentials(await homeserver.loginUser(bob.credentials.userId, bob.credentials.password));
388+
await bobSecondDevice.prepareClient();
389+
return bobSecondDevice;
390+
}

res/css/views/messages/_DecryptionFailureBody.pcss

+21
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,24 @@ Please see LICENSE files in the repository root for full details.
1010
color: $secondary-content;
1111
font-style: italic;
1212
}
13+
14+
/* Formatting for the "Verified identity has changed" error */
15+
.mx_DecryptionFailureVerifiedIdentityChanged > span {
16+
/* Show it in red */
17+
color: var(--cpd-color-text-critical-primary);
18+
background-color: var(--cpd-color-bg-critical-subtle);
19+
20+
/* With a red border */
21+
border: 1px solid var(--cpd-color-border-critical-subtle);
22+
border-radius: $font-16px;
23+
24+
/* Some space inside the border */
25+
padding: var(--cpd-space-1x) var(--cpd-space-3x) var(--cpd-space-1x) var(--cpd-space-2x);
26+
27+
/* some space between the (!) icon and text */
28+
display: inline-flex;
29+
gap: var(--cpd-space-2x);
30+
31+
/* Center vertically */
32+
align-items: center;
33+
}

src/components/views/messages/DecryptionFailureBody.tsx

+32-2
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
66
Please see LICENSE files in the repository root for full details.
77
*/
88

9+
import classNames from "classnames";
910
import React, { forwardRef, ForwardRefExoticComponent, useContext } from "react";
1011
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
1112
import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api";
1213

1314
import { _t } from "../../../languageHandler";
1415
import { IBodyProps } from "./IBodyProps";
1516
import { LocalDeviceVerificationStateContext } from "../../../contexts/LocalDeviceVerificationStateContext";
17+
import { Icon as WarningBadgeIcon } from "../../../../res/img/compound/error-16px.svg";
1618

17-
function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined): string {
19+
function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined): string | React.JSX.Element {
1820
switch (mxEvent.decryptionFailureReason) {
1921
case DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE:
2022
return _t("timeline|decryption_failure|blocked");
@@ -32,16 +34,44 @@ function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined):
3234
break;
3335

3436
case DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED:
37+
// TODO: event should be hidden instead of showing this error.
38+
// To be revisited as part of https://github.com/element-hq/element-meta/issues/2449
3539
return _t("timeline|decryption_failure|historical_event_user_not_joined");
40+
41+
case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED:
42+
return (
43+
<span>
44+
<WarningBadgeIcon className="mx_Icon mx_Icon_16" />
45+
{_t("timeline|decryption_failure|sender_identity_previously_verified")}
46+
</span>
47+
);
48+
49+
case DecryptionFailureCode.UNSIGNED_SENDER_DEVICE:
50+
// TODO: event should be hidden instead of showing this error.
51+
// To be revisited as part of https://github.com/element-hq/element-meta/issues/2449
52+
return _t("timeline|decryption_failure|sender_unsigned_device");
3653
}
3754
return _t("timeline|decryption_failure|unable_to_decrypt");
3855
}
3956

57+
/** Get an extra CSS class, specific to the decryption failure reason */
58+
function errorClassName(mxEvent: MatrixEvent): string | null {
59+
switch (mxEvent.decryptionFailureReason) {
60+
case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED:
61+
return "mx_DecryptionFailureVerifiedIdentityChanged";
62+
63+
default:
64+
return null;
65+
}
66+
}
67+
4068
// A placeholder element for messages that could not be decrypted
4169
export const DecryptionFailureBody = forwardRef<HTMLDivElement, IBodyProps>(({ mxEvent }, ref): React.JSX.Element => {
4270
const verificationState = useContext(LocalDeviceVerificationStateContext);
71+
const classes = classNames("mx_DecryptionFailureBody", "mx_EventTile_content", errorClassName(mxEvent));
72+
4373
return (
44-
<div className="mx_DecryptionFailureBody mx_EventTile_content" ref={ref}>
74+
<div className={classes} ref={ref}>
4575
{getErrorMessage(mxEvent, verificationState)}
4676
</div>
4777
);

src/i18n/strings/en_EN.json

+2
Original file line numberDiff line numberDiff line change
@@ -3303,6 +3303,8 @@
33033303
"historical_event_no_key_backup": "Historical messages are not available on this device",
33043304
"historical_event_unverified_device": "You need to verify this device for access to historical messages",
33053305
"historical_event_user_not_joined": "You don't have access to this message",
3306+
"sender_identity_previously_verified": "Verified identity has changed",
3307+
"sender_unsigned_device": "Encrypted by a device not verified by its owner.",
33063308
"unable_to_decrypt": "Unable to decrypt message"
33073309
},
33083310
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",

test/components/views/messages/DecryptionFailureBody-test.tsx

+28
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,32 @@ describe("DecryptionFailureBody", () => {
103103
// Then
104104
expect(container).toHaveTextContent("You don't have access to this message");
105105
});
106+
107+
it("should handle messages from users who change identities after verification", async () => {
108+
// When
109+
const event = await mkDecryptionFailureMatrixEvent({
110+
code: DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED,
111+
msg: "User previously verified",
112+
roomId: "fakeroom",
113+
sender: "fakesender",
114+
});
115+
const { container } = customRender(event);
116+
117+
// Then
118+
expect(container).toMatchSnapshot();
119+
});
120+
121+
it("should handle messages from unverified devices", async () => {
122+
// When
123+
const event = await mkDecryptionFailureMatrixEvent({
124+
code: DecryptionFailureCode.UNSIGNED_SENDER_DEVICE,
125+
msg: "Unsigned device",
126+
roomId: "fakeroom",
127+
sender: "fakesender",
128+
});
129+
const { container } = customRender(event);
130+
131+
// Then
132+
expect(container).toHaveTextContent("Encrypted by a device not verified by its owner");
133+
});
106134
});

test/components/views/messages/__snapshots__/DecryptionFailureBody-test.tsx.snap

+15
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,18 @@ exports[`DecryptionFailureBody Should display "Unable to decrypt message" 1`] =
1919
</div>
2020
</div>
2121
`;
22+
23+
exports[`DecryptionFailureBody should handle messages from users who change identities after verification 1`] = `
24+
<div>
25+
<div
26+
class="mx_DecryptionFailureBody mx_EventTile_content mx_DecryptionFailureVerifiedIdentityChanged"
27+
>
28+
<span>
29+
<div
30+
class="mx_Icon mx_Icon_16"
31+
/>
32+
Verified identity has changed
33+
</span>
34+
</div>
35+
</div>
36+
`;

0 commit comments

Comments
 (0)