From a3863fb826ed8da78c4d01adadaec538687a3bf0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 31 Aug 2023 12:16:51 +0100 Subject: [PATCH] Delegate decisions on event shields to the js-sdk --- cypress/e2e/crypto/crypto.spec.ts | 4 +- res/css/views/rooms/_EventTile.pcss | 10 +- src/components/views/rooms/EventTile.tsx | 167 +++++++++++------------ src/i18n/strings/en_EN.json | 8 +- 4 files changed, 89 insertions(+), 100 deletions(-) diff --git a/cypress/e2e/crypto/crypto.spec.ts b/cypress/e2e/crypto/crypto.spec.ts index 8a8fa042e8a..4872ffe4bc5 100644 --- a/cypress/e2e/crypto/crypto.spec.ts +++ b/cypress/e2e/crypto/crypto.spec.ts @@ -410,7 +410,7 @@ describe("Cryptography", function () { .should("contain", "test encrypted from unverified") .find(".mx_EventTile_e2eIcon", { timeout: 100000 }) .should("have.class", "mx_EventTile_e2eIcon_warning") - .should("have.attr", "aria-label", "Encrypted by an unverified session"); + .should("have.attr", "aria-label", "Encrypted by an unverified user."); /* Should show a grey padlock for a message from an unknown device */ @@ -423,7 +423,7 @@ describe("Cryptography", function () { .should("contain", "test encrypted from unverified") .find(".mx_EventTile_e2eIcon") .should("have.class", "mx_EventTile_e2eIcon_normal") - .should("have.attr", "aria-label", "Encrypted by a deleted session"); + .should("have.attr", "aria-label", "Encrypted by an unknown or deleted device."); }); it("Should show a grey padlock for a key restored from backup", () => { diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index cbd35c914d3..b01d223d576 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -846,17 +846,17 @@ $left-gutter: 64px; } &.mx_EventTile_e2eIcon_warning::after { - mask-image: url("$(res)/img/e2e/warning.svg"); - background-color: $e2e-warning-color; + mask-image: url("$(res)/img/e2e/warning.svg"); // (!) in a shield + background-color: $e2e-warning-color; // red } &.mx_EventTile_e2eIcon_normal::after { - mask-image: url("$(res)/img/e2e/normal.svg"); - background-color: $header-panel-text-primary-color; + mask-image: url("$(res)/img/e2e/normal.svg"); // regular shield + background-color: $header-panel-text-primary-color; // grey } &.mx_EventTile_e2eIcon_decryption_failure::after { - mask-image: url("$(res)/img/e2e/decryption-failure.svg"); + mask-image: url("$(res)/img/e2e/decryption-failure.svg"); // key in a circle background-color: $secondary-content; } } diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 986eafcca9c..376571cadd0 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -18,17 +18,17 @@ limitations under the License. import React, { createRef, forwardRef, MouseEvent, ReactNode, useRef } from "react"; import classNames from "classnames"; import { - EventType, - MsgType, - RelationType, EventStatus, + EventType, MatrixEvent, MatrixEventEvent, - RoomMember, + MsgType, NotificationCountType, + Relations, + RelationType, Room, RoomEvent, - Relations, + RoomMember, Thread, ThreadEvent, } from "matrix-js-sdk/src/matrix"; @@ -36,6 +36,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { CallErrorCode } from "matrix-js-sdk/src/webrtc/call"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; +import { EventShieldColour, EventShieldReason } from "matrix-js-sdk/src/crypto-api"; import ReplyChain from "../elements/ReplyChain"; import { _t } from "../../../languageHandler"; @@ -44,7 +45,6 @@ import { Layout } from "../../../settings/enums/Layout"; import { formatTime } from "../../../DateUtils"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { DecryptionFailureBody } from "../messages/DecryptionFailureBody"; -import { E2EState } from "./E2EIcon"; import RoomAvatar from "../avatars/RoomAvatar"; import MessageContextMenu from "../context_menus/MessageContextMenu"; import { aboveRightOf } from "../../structures/ContextMenu"; @@ -236,8 +236,19 @@ export interface EventTileProps { interface IState { // Whether the action bar is focused. actionBarFocused: boolean; - // Whether the event's sender has been verified. - verified: string | null; + + /** + * E2EE shield we should show for decryption problems. + * + * Note this will be `EventShieldColour.NONE` for all unencrypted events, **including those in encrypted rooms**. + */ + shieldColour: EventShieldColour; + + /** + * Reason code for the E2EE shield. `null` if `shieldColour` is `EventShieldColour.NONE` + */ + shieldReason: EventShieldReason | null; + // The Relations model from the JS SDK for reactions to `mxEvent` reactions?: Relations | null | undefined; @@ -299,9 +310,10 @@ export class UnwrappedEventTile extends React.Component this.state = { // Whether the action bar is focused. actionBarFocused: false, - // Whether the event's sender has been verified. `null` if no attempt has yet been made to verify - // (including if the event is not encrypted). - verified: null, + + shieldColour: EventShieldColour.NONE, + shieldReason: null, + // The Relations model from the JS SDK for reactions to `mxEvent` reactions: this.getReactions(), @@ -437,8 +449,9 @@ export class UnwrappedEventTile extends React.Component } public componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { - // If the verification state changed, the height might have changed - if (prevState.verified !== this.state.verified && this.props.onHeightChanged) { + // If the shield state changed, the height might have changed. + // XXX: does the shield *actually* cause a change in height? Not sure. + if (prevState.shieldColour !== this.state.shieldColour && this.props.onHeightChanged) { this.props.onHeightChanged(); } // If we're not listening for receipts and expect to be, register a listener. @@ -582,59 +595,20 @@ export class UnwrappedEventTile extends React.Component const mxEvent = this.props.mxEvent.replacingEvent() ?? this.props.mxEvent; if (!mxEvent.isEncrypted() || mxEvent.isRedacted()) { - this.setState({ verified: null }); + this.setState({ shieldColour: EventShieldColour.NONE, shieldReason: null }); return; } - const encryptionInfo = MatrixClientPeg.safeGet().getEventEncryptionInfo(mxEvent); - const senderId = mxEvent.getSender(); - if (!senderId) { - // something definitely wrong is going on here - this.setState({ verified: E2EState.Warning }); - return; - } - - const userTrust = MatrixClientPeg.safeGet().checkUserTrust(senderId); - - if (encryptionInfo.mismatchedSender) { - // something definitely wrong is going on here - this.setState({ verified: E2EState.Warning }); - return; - } - - if (!userTrust.isCrossSigningVerified()) { - // If the message is unauthenticated, then display a grey - // shield, otherwise if the user isn't cross-signed then - // nothing's needed - this.setState({ verified: encryptionInfo.authenticated ? E2EState.Normal : E2EState.Unauthenticated }); - return; - } - - const eventSenderTrust = - senderId && - encryptionInfo.sender && - (await MatrixClientPeg.safeGet() - .getCrypto() - ?.getDeviceVerificationStatus(senderId, encryptionInfo.sender.deviceId)); - + const encryptionInfo = + (await MatrixClientPeg.safeGet().getCrypto()?.getEncryptionInfoForEvent(mxEvent)) ?? null; if (this.unmounted) return; - - if (!eventSenderTrust) { - this.setState({ verified: E2EState.Unknown }); - return; - } - - if (!eventSenderTrust.isVerified()) { - this.setState({ verified: E2EState.Warning }); + if (encryptionInfo === null) { + // likely a decryption error + this.setState({ shieldColour: EventShieldColour.NONE, shieldReason: null }); return; } - if (!encryptionInfo.authenticated) { - this.setState({ verified: E2EState.Unauthenticated }); - return; - } - - this.setState({ verified: E2EState.Verified }); + this.setState({ shieldColour: encryptionInfo.shieldColour, shieldReason: encryptionInfo.shieldReason }); } private propsEqual(objA: EventTileProps, objB: EventTileProps): boolean { @@ -751,18 +725,42 @@ export class UnwrappedEventTile extends React.Component return ; } - // event is encrypted and not redacted, display padlock corresponding to whether or not it is verified - if (ev.isEncrypted() && !ev.isRedacted()) { - if (this.state.verified === E2EState.Normal) { - return null; // no icon if we've not even cross-signed the user - } else if (this.state.verified === E2EState.Verified) { - return null; // no icon for verified - } else if (this.state.verified === E2EState.Unauthenticated) { - return ; - } else if (this.state.verified === E2EState.Unknown) { - return ; + if (this.state.shieldColour !== EventShieldColour.NONE) { + let shieldReasonMessage: string; + switch (this.state.shieldReason) { + case null: + case EventShieldReason.UNKNOWN: + shieldReasonMessage = _t("Unknown error"); + break; + + case EventShieldReason.UNVERIFIED_IDENTITY: + shieldReasonMessage = _t("Encrypted by an unverified user."); + break; + + case EventShieldReason.UNSIGNED_DEVICE: + shieldReasonMessage = _t("Encrypted by a device not verified by its owner."); + break; + + case EventShieldReason.UNKNOWN_DEVICE: + shieldReasonMessage = _t("Encrypted by an unknown or deleted device."); + break; + + case EventShieldReason.AUTHENTICITY_NOT_GUARANTEED: + shieldReasonMessage = _t( + "The authenticity of this encrypted message can't be guaranteed on this device.", + ); + break; + + case EventShieldReason.MISMATCHED_SENDER_KEY: + shieldReasonMessage = _t("Encrypted by an unverified session"); + break; + } + + if (this.state.shieldColour === EventShieldColour.GREY) { + return ; } else { - return ; + // red, by elimination + return ; } } @@ -781,8 +779,10 @@ export class UnwrappedEventTile extends React.Component if (ev.isRedacted()) { return null; // we expect this to be unencrypted } - // if the event is not encrypted, but it's an e2e room, show the open padlock - return ; + if (!ev.isEncrypted()) { + // if the event is not encrypted, but it's an e2e room, show a warning + return ; + } } // no padlock needed @@ -1460,28 +1460,10 @@ const SafeEventTile = forwardRef((props, ref }); export default SafeEventTile; -function E2ePadlockUnverified(props: Omit): JSX.Element { - return ; -} - function E2ePadlockUnencrypted(props: Omit): JSX.Element { return ; } -function E2ePadlockUnknown(props: Omit): JSX.Element { - return ; -} - -function E2ePadlockUnauthenticated(props: Omit): JSX.Element { - return ( - - ); -} - function E2ePadlockDecryptionFailure(props: Omit): JSX.Element { return ( %(room)s": " in %(room)s", - "Encrypted by an unverified session": "Encrypted by an unverified session", "Unencrypted": "Unencrypted", - "Encrypted by a deleted session": "Encrypted by a deleted session", - "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.", "This message could not be decrypted": "This message could not be decrypted", "Sending your message…": "Sending your message…", "Encrypting your message…": "Encrypting your message…",