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

Commit c2fc124

Browse files
committed
display invisible crypto decryption errors
1 parent 9420138 commit c2fc124

File tree

6 files changed

+138
-2
lines changed

6 files changed

+138
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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, createSharedRoomWithUser, verify } from "./utils";
10+
import { Bot } from "../../pages/bot";
11+
12+
test.describe("Invisible cryptography", () => {
13+
test.use({
14+
displayName: "Alice",
15+
botCreateOpts: { displayName: "Bob", autoAcceptInvites: true },
16+
labsFlags: ["feature_invisible_crypto"],
17+
});
18+
19+
test("Messages fail to decrypt when sender is previously verified", async ({
20+
page,
21+
bot: bob,
22+
user: aliceCredentials,
23+
app,
24+
homeserver,
25+
}) => {
26+
await app.client.bootstrapCrossSigning(aliceCredentials);
27+
await autoJoin(bob);
28+
29+
// create an encrypted room
30+
const testRoomId = await createSharedRoomWithUser(app, bob.credentials.userId, {
31+
name: "TestRoom",
32+
initial_state: [
33+
{
34+
type: "m.room.encryption",
35+
state_key: "",
36+
content: {
37+
algorithm: "m.megolm.v1.aes-sha2",
38+
},
39+
},
40+
],
41+
});
42+
43+
// Verify Bob
44+
await verify(app, bob);
45+
46+
// Bob logs in a new device and resets cross-signing
47+
const bobSecondDevice = new Bot(page, homeserver, {
48+
bootstrapSecretStorage: true,
49+
bootstrapCrossSigning: true,
50+
setupNewCrossSigning: true,
51+
});
52+
bobSecondDevice.setCredentials(await homeserver.loginUser(bob.credentials.userId, bob.credentials.password));
53+
await bobSecondDevice.prepareClient();
54+
55+
/* should show an error for a message from a previously verified device */
56+
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from previously verified");
57+
const lastTile = page.locator(".mx_EventTile_last");
58+
await expect(lastTile).toContainText("Verified identity has changed");
59+
});
60+
});

playwright/pages/bot.ts

+5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ export interface CreateBotOpts {
3737
* Whether to generate cross-signing keys
3838
*/
3939
bootstrapCrossSigning?: boolean;
40+
/**
41+
* Whether to reset the cross-signing keys even if keys already exist
42+
*/
43+
setupNewCrossSigning?: boolean;
4044
/**
4145
* Whether to bootstrap the secret storage
4246
*/
@@ -186,6 +190,7 @@ export class Bot extends Client {
186190
await cli.getCrypto()!.getUserDeviceInfo([credentials.userId]);
187191

188192
await cli.getCrypto()!.bootstrapCrossSigning({
193+
setupNewCrossSigning: opts.setupNewCrossSigning,
189194
authUploadDeviceSigningKeys: async (func) => {
190195
await func({
191196
type: "m.login.password",

res/css/views/messages/_DecryptionFailureBody.pcss

+16
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,19 @@ Please see LICENSE files in the repository root for full details.
1010
color: $secondary-content;
1111
font-style: italic;
1212
}
13+
14+
.mx_DecryptionFailureVerifiedIdentityChanged > span {
15+
color: $e2e-warning-color;
16+
border-radius: $font-16px;
17+
border-width: 1px;
18+
border-color: $e2e-warning-color;
19+
border-style: solid;
20+
padding: $font-1px 0.4em $font-1px 0.4em;
21+
display: inline-flex;
22+
align-items: center;
23+
24+
.mx_Icon {
25+
margin-inline-start: -0.3em;
26+
margin-inline-end: 0.2em;
27+
}
28+
}

src/components/views/messages/DecryptionFailureBody.tsx

+28-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");
@@ -33,15 +35,39 @@ function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined):
3335

3436
case DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED:
3537
return _t("timeline|decryption_failure|historical_event_user_not_joined");
38+
39+
case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED:
40+
return (
41+
<span>
42+
<WarningBadgeIcon className="mx_Icon mx_Icon_16" />
43+
{_t("timeline|decryption_failure|sender_identity_previously_verified")}
44+
</span>
45+
);
46+
47+
case DecryptionFailureCode.UNSIGNED_SENDER_DEVICE:
48+
// TODO: event should be hidden instead of showing this error (only
49+
// happens when invisible crypto is enabled)
50+
return _t("encryption|event_shield_reason_unsigned_device");
3651
}
3752
return _t("timeline|decryption_failure|unable_to_decrypt");
3853
}
3954

55+
function getErrorExtraClass(mxEvent: MatrixEvent): Record<string, boolean> {
56+
switch (mxEvent.decryptionFailureReason) {
57+
case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED:
58+
return { mx_DecryptionFailureVerifiedIdentityChanged: true };
59+
60+
default:
61+
return {};
62+
}
63+
}
64+
4065
// A placeholder element for messages that could not be decrypted
4166
export const DecryptionFailureBody = forwardRef<HTMLDivElement, IBodyProps>(({ mxEvent }, ref): React.JSX.Element => {
4267
const verificationState = useContext(LocalDeviceVerificationStateContext);
68+
const classes = classNames("mx_DecryptionFailureBody", "mx_EventTile_content", getErrorExtraClass(mxEvent));
4369
return (
44-
<div className="mx_DecryptionFailureBody mx_EventTile_content" ref={ref}>
70+
<div className={classes} ref={ref}>
4571
{getErrorMessage(mxEvent, verificationState)}
4672
</div>
4773
);

src/i18n/strings/en_EN.json

+1
Original file line numberDiff line numberDiff line change
@@ -3293,6 +3293,7 @@
32933293
"historical_event_no_key_backup": "Historical messages are not available on this device",
32943294
"historical_event_unverified_device": "You need to verify this device for access to historical messages",
32953295
"historical_event_user_not_joined": "You don't have access to this message",
3296+
"sender_identity_previously_verified": "Verified identity has changed",
32963297
"unable_to_decrypt": "Unable to decrypt message"
32973298
},
32983299
"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).toHaveTextContent("Verified identity has changed");
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
});

0 commit comments

Comments
 (0)