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

Commit cf2340b

Browse files
Replace event verification logic with new code in js-sdk (#11528)
* Use new crypto-api for cross user verification * update verification flow with new APIs * Replace some calls to `checkUserTrust` A start on element-hq/crypto-internal#147 * Enable cypress tests * update tests * Delegate decisions on event shields to the js-sdk * rerender after editing events This is required because a transition from "valid event" to "unencrypted event" no longer triggers a state change, so the component does not render itself. Previously, this would be a transition from `verified: E2EState.Normal` to `verified: null`. * Update tests * prettier * Test coverage --------- Co-authored-by: Florian Duros <[email protected]>
1 parent 579b0dd commit cf2340b

File tree

5 files changed

+215
-166
lines changed

5 files changed

+215
-166
lines changed

cypress/e2e/crypto/crypto.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ describe("Cryptography", function () {
409409
.should("contain", "test encrypted from unverified")
410410
.find(".mx_EventTile_e2eIcon", { timeout: 100000 })
411411
.should("have.class", "mx_EventTile_e2eIcon_warning")
412-
.should("have.attr", "aria-label", "Encrypted by an unverified session");
412+
.should("have.attr", "aria-label", "Encrypted by an unverified user.");
413413

414414
/* Should show a grey padlock for a message from an unknown device */
415415

@@ -422,7 +422,7 @@ describe("Cryptography", function () {
422422
.should("contain", "test encrypted from unverified")
423423
.find(".mx_EventTile_e2eIcon")
424424
.should("have.class", "mx_EventTile_e2eIcon_normal")
425-
.should("have.attr", "aria-label", "Encrypted by a deleted session");
425+
.should("have.attr", "aria-label", "Encrypted by an unknown or deleted device.");
426426
});
427427

428428
it("Should show a grey padlock for a key restored from backup", () => {

res/css/views/rooms/_EventTile.pcss

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -846,17 +846,17 @@ $left-gutter: 64px;
846846
}
847847

848848
&.mx_EventTile_e2eIcon_warning::after {
849-
mask-image: url("$(res)/img/e2e/warning.svg");
850-
background-color: $e2e-warning-color;
849+
mask-image: url("$(res)/img/e2e/warning.svg"); // (!) in a shield
850+
background-color: $e2e-warning-color; // red
851851
}
852852

853853
&.mx_EventTile_e2eIcon_normal::after {
854-
mask-image: url("$(res)/img/e2e/normal.svg");
855-
background-color: $header-panel-text-primary-color;
854+
mask-image: url("$(res)/img/e2e/normal.svg"); // regular shield
855+
background-color: $header-panel-text-primary-color; // grey
856856
}
857857

858858
&.mx_EventTile_e2eIcon_decryption_failure::after {
859-
mask-image: url("$(res)/img/e2e/decryption-failure.svg");
859+
mask-image: url("$(res)/img/e2e/decryption-failure.svg"); // key in a circle
860860
background-color: $secondary-content;
861861
}
862862
}

src/components/views/rooms/EventTile.tsx

Lines changed: 77 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,25 @@ limitations under the License.
1818
import React, { createRef, forwardRef, MouseEvent, ReactNode, useRef } from "react";
1919
import classNames from "classnames";
2020
import {
21-
EventType,
22-
MsgType,
23-
RelationType,
2421
EventStatus,
22+
EventType,
2523
MatrixEvent,
2624
MatrixEventEvent,
27-
RoomMember,
25+
MsgType,
2826
NotificationCountType,
27+
Relations,
28+
RelationType,
2929
Room,
3030
RoomEvent,
31-
Relations,
31+
RoomMember,
3232
Thread,
3333
ThreadEvent,
3434
} from "matrix-js-sdk/src/matrix";
3535
import { logger } from "matrix-js-sdk/src/logger";
3636
import { CallErrorCode } from "matrix-js-sdk/src/webrtc/call";
3737
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
3838
import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
39+
import { EventShieldColour, EventShieldReason } from "matrix-js-sdk/src/crypto-api";
3940

4041
import ReplyChain from "../elements/ReplyChain";
4142
import { _t } from "../../../languageHandler";
@@ -44,7 +45,6 @@ import { Layout } from "../../../settings/enums/Layout";
4445
import { formatTime } from "../../../DateUtils";
4546
import { MatrixClientPeg } from "../../../MatrixClientPeg";
4647
import { DecryptionFailureBody } from "../messages/DecryptionFailureBody";
47-
import { E2EState } from "./E2EIcon";
4848
import RoomAvatar from "../avatars/RoomAvatar";
4949
import MessageContextMenu from "../context_menus/MessageContextMenu";
5050
import { aboveRightOf } from "../../structures/ContextMenu";
@@ -236,8 +236,19 @@ export interface EventTileProps {
236236
interface IState {
237237
// Whether the action bar is focused.
238238
actionBarFocused: boolean;
239-
// Whether the event's sender has been verified.
240-
verified: string | null;
239+
240+
/**
241+
* E2EE shield we should show for decryption problems.
242+
*
243+
* Note this will be `EventShieldColour.NONE` for all unencrypted events, **including those in encrypted rooms**.
244+
*/
245+
shieldColour: EventShieldColour;
246+
247+
/**
248+
* Reason code for the E2EE shield. `null` if `shieldColour` is `EventShieldColour.NONE`
249+
*/
250+
shieldReason: EventShieldReason | null;
251+
241252
// The Relations model from the JS SDK for reactions to `mxEvent`
242253
reactions?: Relations | null | undefined;
243254

@@ -299,9 +310,10 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
299310
this.state = {
300311
// Whether the action bar is focused.
301312
actionBarFocused: false,
302-
// Whether the event's sender has been verified. `null` if no attempt has yet been made to verify
303-
// (including if the event is not encrypted).
304-
verified: null,
313+
314+
shieldColour: EventShieldColour.NONE,
315+
shieldReason: null,
316+
305317
// The Relations model from the JS SDK for reactions to `mxEvent`
306318
reactions: this.getReactions(),
307319

@@ -437,8 +449,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
437449
}
438450

439451
public componentDidUpdate(prevProps: Readonly<EventTileProps>, prevState: Readonly<IState>): void {
440-
// If the verification state changed, the height might have changed
441-
if (prevState.verified !== this.state.verified && this.props.onHeightChanged) {
452+
// If the shield state changed, the height might have changed.
453+
// XXX: does the shield *actually* cause a change in height? Not sure.
454+
if (prevState.shieldColour !== this.state.shieldColour && this.props.onHeightChanged) {
442455
this.props.onHeightChanged();
443456
}
444457
// If we're not listening for receipts and expect to be, register a listener.
@@ -582,59 +595,20 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
582595
const mxEvent = this.props.mxEvent.replacingEvent() ?? this.props.mxEvent;
583596

584597
if (!mxEvent.isEncrypted() || mxEvent.isRedacted()) {
585-
this.setState({ verified: null });
598+
this.setState({ shieldColour: EventShieldColour.NONE, shieldReason: null });
586599
return;
587600
}
588601

589-
const encryptionInfo = MatrixClientPeg.safeGet().getEventEncryptionInfo(mxEvent);
590-
const senderId = mxEvent.getSender();
591-
if (!senderId) {
592-
// something definitely wrong is going on here
593-
this.setState({ verified: E2EState.Warning });
594-
return;
595-
}
596-
597-
const userTrust = MatrixClientPeg.safeGet().checkUserTrust(senderId);
598-
599-
if (encryptionInfo.mismatchedSender) {
600-
// something definitely wrong is going on here
601-
this.setState({ verified: E2EState.Warning });
602-
return;
603-
}
604-
605-
if (!userTrust.isCrossSigningVerified()) {
606-
// If the message is unauthenticated, then display a grey
607-
// shield, otherwise if the user isn't cross-signed then
608-
// nothing's needed
609-
this.setState({ verified: encryptionInfo.authenticated ? E2EState.Normal : E2EState.Unauthenticated });
610-
return;
611-
}
612-
613-
const eventSenderTrust =
614-
senderId &&
615-
encryptionInfo.sender &&
616-
(await MatrixClientPeg.safeGet()
617-
.getCrypto()
618-
?.getDeviceVerificationStatus(senderId, encryptionInfo.sender.deviceId));
619-
602+
const encryptionInfo =
603+
(await MatrixClientPeg.safeGet().getCrypto()?.getEncryptionInfoForEvent(mxEvent)) ?? null;
620604
if (this.unmounted) return;
621-
622-
if (!eventSenderTrust) {
623-
this.setState({ verified: E2EState.Unknown });
624-
return;
625-
}
626-
627-
if (!eventSenderTrust.isVerified()) {
628-
this.setState({ verified: E2EState.Warning });
605+
if (encryptionInfo === null) {
606+
// likely a decryption error
607+
this.setState({ shieldColour: EventShieldColour.NONE, shieldReason: null });
629608
return;
630609
}
631610

632-
if (!encryptionInfo.authenticated) {
633-
this.setState({ verified: E2EState.Unauthenticated });
634-
return;
635-
}
636-
637-
this.setState({ verified: E2EState.Verified });
611+
this.setState({ shieldColour: encryptionInfo.shieldColour, shieldReason: encryptionInfo.shieldReason });
638612
}
639613

640614
private propsEqual(objA: EventTileProps, objB: EventTileProps): boolean {
@@ -751,18 +725,42 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
751725
return <E2ePadlockDecryptionFailure />;
752726
}
753727

754-
// event is encrypted and not redacted, display padlock corresponding to whether or not it is verified
755-
if (ev.isEncrypted() && !ev.isRedacted()) {
756-
if (this.state.verified === E2EState.Normal) {
757-
return null; // no icon if we've not even cross-signed the user
758-
} else if (this.state.verified === E2EState.Verified) {
759-
return null; // no icon for verified
760-
} else if (this.state.verified === E2EState.Unauthenticated) {
761-
return <E2ePadlockUnauthenticated />;
762-
} else if (this.state.verified === E2EState.Unknown) {
763-
return <E2ePadlockUnknown />;
728+
if (this.state.shieldColour !== EventShieldColour.NONE) {
729+
let shieldReasonMessage: string;
730+
switch (this.state.shieldReason) {
731+
case null:
732+
case EventShieldReason.UNKNOWN:
733+
shieldReasonMessage = _t("Unknown error");
734+
break;
735+
736+
case EventShieldReason.UNVERIFIED_IDENTITY:
737+
shieldReasonMessage = _t("Encrypted by an unverified user.");
738+
break;
739+
740+
case EventShieldReason.UNSIGNED_DEVICE:
741+
shieldReasonMessage = _t("Encrypted by a device not verified by its owner.");
742+
break;
743+
744+
case EventShieldReason.UNKNOWN_DEVICE:
745+
shieldReasonMessage = _t("Encrypted by an unknown or deleted device.");
746+
break;
747+
748+
case EventShieldReason.AUTHENTICITY_NOT_GUARANTEED:
749+
shieldReasonMessage = _t(
750+
"The authenticity of this encrypted message can't be guaranteed on this device.",
751+
);
752+
break;
753+
754+
case EventShieldReason.MISMATCHED_SENDER_KEY:
755+
shieldReasonMessage = _t("Encrypted by an unverified session");
756+
break;
757+
}
758+
759+
if (this.state.shieldColour === EventShieldColour.GREY) {
760+
return <E2ePadlock icon={E2ePadlockIcon.Normal} title={shieldReasonMessage} />;
764761
} else {
765-
return <E2ePadlockUnverified />;
762+
// red, by elimination
763+
return <E2ePadlock icon={E2ePadlockIcon.Warning} title={shieldReasonMessage} />;
766764
}
767765
}
768766

@@ -781,8 +779,10 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
781779
if (ev.isRedacted()) {
782780
return null; // we expect this to be unencrypted
783781
}
784-
// if the event is not encrypted, but it's an e2e room, show the open padlock
785-
return <E2ePadlockUnencrypted />;
782+
if (!ev.isEncrypted()) {
783+
// if the event is not encrypted, but it's an e2e room, show a warning
784+
return <E2ePadlockUnencrypted />;
785+
}
786786
}
787787

788788
// no padlock needed
@@ -1460,28 +1460,10 @@ const SafeEventTile = forwardRef<UnwrappedEventTile, EventTileProps>((props, ref
14601460
});
14611461
export default SafeEventTile;
14621462

1463-
function E2ePadlockUnverified(props: Omit<IE2ePadlockProps, "title" | "icon">): JSX.Element {
1464-
return <E2ePadlock title={_t("Encrypted by an unverified session")} icon={E2ePadlockIcon.Warning} {...props} />;
1465-
}
1466-
14671463
function E2ePadlockUnencrypted(props: Omit<IE2ePadlockProps, "title" | "icon">): JSX.Element {
14681464
return <E2ePadlock title={_t("Unencrypted")} icon={E2ePadlockIcon.Warning} {...props} />;
14691465
}
14701466

1471-
function E2ePadlockUnknown(props: Omit<IE2ePadlockProps, "title" | "icon">): JSX.Element {
1472-
return <E2ePadlock title={_t("Encrypted by a deleted session")} icon={E2ePadlockIcon.Normal} {...props} />;
1473-
}
1474-
1475-
function E2ePadlockUnauthenticated(props: Omit<IE2ePadlockProps, "title" | "icon">): JSX.Element {
1476-
return (
1477-
<E2ePadlock
1478-
title={_t("The authenticity of this encrypted message can't be guaranteed on this device.")}
1479-
icon={E2ePadlockIcon.Normal}
1480-
{...props}
1481-
/>
1482-
);
1483-
}
1484-
14851467
function E2ePadlockDecryptionFailure(props: Omit<IE2ePadlockProps, "title" | "icon">): JSX.Element {
14861468
return (
14871469
<E2ePadlock
@@ -1493,8 +1475,13 @@ function E2ePadlockDecryptionFailure(props: Omit<IE2ePadlockProps, "title" | "ic
14931475
}
14941476

14951477
enum E2ePadlockIcon {
1478+
/** grey shield */
14961479
Normal = "normal",
1480+
1481+
/** red shield with (!) */
14971482
Warning = "warning",
1483+
1484+
/** key in grey circle */
14981485
DecryptionFailure = "decryption_failure",
14991486
}
15001487

src/i18n/strings/en_EN.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2046,12 +2046,14 @@
20462046
"Everyone in this room is verified": "Everyone in this room is verified",
20472047
"Edit message": "Edit message",
20482048
"From a thread": "From a thread",
2049+
"Encrypted by an unverified user.": "Encrypted by an unverified user.",
2050+
"Encrypted by a device not verified by its owner.": "Encrypted by a device not verified by its owner.",
2051+
"Encrypted by an unknown or deleted device.": "Encrypted by an unknown or deleted device.",
2052+
"The authenticity of this encrypted message can't be guaranteed on this device.": "The authenticity of this encrypted message can't be guaranteed on this device.",
2053+
"Encrypted by an unverified session": "Encrypted by an unverified session",
20492054
"This event could not be displayed": "This event could not be displayed",
20502055
" in <strong>%(room)s</strong>": " in <strong>%(room)s</strong>",
2051-
"Encrypted by an unverified session": "Encrypted by an unverified session",
20522056
"Unencrypted": "Unencrypted",
2053-
"Encrypted by a deleted session": "Encrypted by a deleted session",
2054-
"The authenticity of this encrypted message can't be guaranteed on this device.": "The authenticity of this encrypted message can't be guaranteed on this device.",
20552057
"This message could not be decrypted": "This message could not be decrypted",
20562058
"Sending your message…": "Sending your message…",
20572059
"Encrypting your message…": "Encrypting your message…",

0 commit comments

Comments
 (0)