From 0b8e1cb1d202bac99cbdcb7670c979f45cce7627 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 22 Jan 2024 22:05:05 +0000 Subject: [PATCH] Introduce `Room.hasEncryptionStateEvent` ... and replace a lot of calls to `MatrixClient.isRoomEncrypted` with it. This is a lesser check (since it can be tricked by servers withholding the state event), but for most cases it is sufficient. At the end of the day, if the server witholds the state, the room is pretty much bricked anyway. The one thing we *mustn't* do is allow users to send *unencrypted* events to the room. --- spec/unit/matrix-client.spec.ts | 18 +----------------- src/client.ts | 7 +++---- src/models/room.ts | 23 +++++++++++++++++++---- src/sliding-sync-sdk.ts | 2 +- src/sync.ts | 4 ++-- 5 files changed, 26 insertions(+), 28 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 8f87d6f8cc6..2a79293e2ad 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -1450,23 +1450,7 @@ describe("MatrixClient", function () { const mockRoom = { getMyMembership: () => "join", updatePendingEvent: (event: MatrixEvent, status: EventStatus) => event.setStatus(status), - currentState: { - getStateEvents: (eventType, stateKey) => { - if (eventType === EventType.RoomCreate) { - expect(stateKey).toEqual(""); - return new MatrixEvent({ - content: { - [RoomCreateTypeField]: RoomType.Space, - }, - }); - } else if (eventType === EventType.RoomEncryption) { - expect(stateKey).toEqual(""); - return new MatrixEvent({ content: {} }); - } else { - throw new Error("Unexpected event type or state key"); - } - }, - } as Room["currentState"], + hasEncryptionStateEvent: jest.fn().mockReturnValue(true), } as unknown as Room; let event: MatrixEvent; diff --git a/src/client.ts b/src/client.ts index bb294f87e38..44d9fbd33a9 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1445,7 +1445,7 @@ export class MatrixClient extends TypedEventEmitter { - if (room && this.isRoomEncrypted(room.roomId)) { + if (room?.hasEncryptionStateEvent()) { // Figure out if we've read something or if it's just informational const content = event.getContent(); const isSelf = @@ -3268,8 +3268,7 @@ export class MatrixClient extends TypedEventEmitter { // that this function is only called once (unless loading the members // fails), since loadMembersIfNeeded always returns this.membersPromise // if set, which will be the result of the first (successful) call. - if (rawMembersEvents === null || (this.client.isCryptoEnabled() && this.client.isRoomEncrypted(this.roomId))) { + if (rawMembersEvents === null || this.hasEncryptionStateEvent()) { fromServer = true; rawMembersEvents = await this.loadMembersFromServer(); logger.log(`LL: got ${rawMembersEvents.length} ` + `members from server for room ${this.roomId}`); @@ -1275,9 +1275,12 @@ export class Room extends ReadReceipt { * error will be thrown. * * @returns the result + * + * @deprecated Not supported under rust crypto. Instead, call {@link Room.getEncryptionTargetMembers}, + * {@link CryptoApi.getUserDeviceInfo}, and {@link CryptoApi.getDeviceVerificationStatus}. */ public async hasUnverifiedDevices(): Promise { - if (!this.client.isRoomEncrypted(this.roomId)) { + if (!this.hasEncryptionStateEvent()) { return false; } const e2eMembers = await this.getEncryptionTargetMembers(); @@ -2565,7 +2568,7 @@ export class Room extends ReadReceipt { .filter((event) => { // Filter out the unencrypted messages if the room is encrypted const isEventEncrypted = event.type === EventType.RoomMessageEncrypted; - const isRoomEncrypted = this.client.isRoomEncrypted(this.roomId); + const isRoomEncrypted = this.hasEncryptionStateEvent(); return isEventEncrypted || !isRoomEncrypted; }); @@ -3170,7 +3173,7 @@ export class Room extends ReadReceipt { public maySendMessage(): boolean { return ( this.getMyMembership() === "join" && - (this.client.isRoomEncrypted(this.roomId) + (this.hasEncryptionStateEvent() ? this.currentState.maySendEvent(EventType.RoomMessageEncrypted, this.myUserId) : this.currentState.maySendEvent(EventType.RoomMessage, this.myUserId)) ); @@ -3672,6 +3675,18 @@ export class Room extends ReadReceipt { public compareEventOrdering(leftEventId: string, rightEventId: string): number | null { return compareEventOrdering(this, leftEventId, rightEventId); } + + /** + * Return true if this room has an `m.room.encryption` state event. + * + * If this returns `true`, events sent to this room should be encrypted (and `MatrixClient.sendEvent` and friends + * will encrypt outgoing events). + */ + public hasEncryptionStateEvent(): boolean { + return Boolean( + this.getLiveTimeline().getState(EventTimeline.FORWARDS)?.getStateEvents(EventType.RoomEncryption, ""), + ); + } } // a map from current event status to a list of allowed next statuses diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index eea6656d98f..bfe32448bd2 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -615,7 +615,7 @@ export class SlidingSyncSdk { } } - const encrypted = this.client.isRoomEncrypted(room.roomId); + const encrypted = room.hasEncryptionStateEvent(); // we do this first so it's correct when any of the events fire if (roomData.notification_count != null) { room.setUnreadNotificationCount(NotificationCountType.Total, roomData.notification_count); diff --git a/src/sync.ts b/src/sync.ts index 57afaff24c4..372260d5615 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -1760,11 +1760,11 @@ export class SyncApi { return events?.find((e) => e.getType() === EventType.RoomEncryption && e.getStateKey() === ""); } - // When processing the sync response we cannot rely on MatrixClient::isRoomEncrypted before we actually + // When processing the sync response we cannot rely on Room.hasEncryptionStateEvent we actually // inject the events into the room object, so we have to inspect the events themselves. private isRoomEncrypted(room: Room, stateEventList: MatrixEvent[], timelineEventList?: MatrixEvent[]): boolean { return ( - this.client.isRoomEncrypted(room.roomId) || + room.hasEncryptionStateEvent() || !!this.findEncryptionEvent(stateEventList) || !!this.findEncryptionEvent(timelineEventList) );