From 89aab0bbb149342c0081ab440fb98e659b957a62 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 4 Sep 2023 18:11:08 +0100 Subject: [PATCH] rust impl of `getEncryptionInfoForEvent` --- spec/unit/rust-crypto/rust-crypto.spec.ts | 114 +++++++++++++++++++++- src/rust-crypto/rust-crypto.ts | 84 +++++++++++++--- 2 files changed, 185 insertions(+), 13 deletions(-) diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index b786a086405..c0afcc524e0 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -30,6 +30,7 @@ import { IHttpOpts, IToDeviceEvent, MatrixClient, + MatrixEvent, MatrixHttpApi, TypedEventEmitter, } from "../../../src"; @@ -38,7 +39,13 @@ import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend"; import { IEventDecryptionResult } from "../../../src/@types/crypto"; import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor"; import { ServerSideSecretStorage } from "../../../src/secret-storage"; -import { CryptoCallbacks, ImportRoomKeysOpts, VerificationRequest } from "../../../src/crypto-api"; +import { + CryptoCallbacks, + EventShieldColour, + EventShieldReason, + ImportRoomKeysOpts, + VerificationRequest, +} from "../../../src/crypto-api"; import * as testData from "../../test-utils/test-data"; import { defer } from "../../../src/utils"; @@ -373,6 +380,111 @@ describe("RustCrypto", () => { }); }); + describe(".getEncryptionInfoForEvent", () => { + let rustCrypto: RustCrypto; + let olmMachine: Mocked; + + beforeEach(() => { + olmMachine = { + getRoomEventEncryptionInfo: jest.fn(), + } as unknown as Mocked; + rustCrypto = new RustCrypto( + olmMachine, + {} as MatrixClient["http"], + TEST_USER, + TEST_DEVICE_ID, + {} as ServerSideSecretStorage, + {} as CryptoCallbacks, + ); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + async function makeEncryptedEvent(): Promise { + const encryptedEvent = mkEvent({ + event: true, + type: "m.room.encrypted", + content: { algorithm: "fake_alg" }, + room: "!room:id", + }); + encryptedEvent.event.event_id = "$event:id"; + const mockCryptoBackend = { + decryptEvent: () => + ({ + clearEvent: { content: { body: "1234" } }, + } as unknown as IEventDecryptionResult), + } as unknown as CryptoBackend; + await encryptedEvent.attemptDecryption(mockCryptoBackend); + return encryptedEvent; + } + + it("should handle unencrypted events", async () => { + const event = mkEvent({ event: true, type: "m.room.message", content: { body: "xyz" } }); + const res = await rustCrypto.getEncryptionInfoForEvent(event); + expect(res).toBe(null); + expect(olmMachine.getRoomEventEncryptionInfo).not.toHaveBeenCalled(); + }); + + it("passes the event into the OlmMachine", async () => { + const encryptedEvent = await makeEncryptedEvent(); + const res = await rustCrypto.getEncryptionInfoForEvent(encryptedEvent); + expect(res).toBe(null); + expect(olmMachine.getRoomEventEncryptionInfo).toHaveBeenCalledTimes(1); + const [passedEvent, passedRoom] = olmMachine.getRoomEventEncryptionInfo.mock.calls[0]; + expect(passedRoom.toString()).toEqual("!room:id"); + expect(JSON.parse(passedEvent)).toStrictEqual( + expect.objectContaining({ + event_id: "$event:id", + }), + ); + }); + + it.each([ + [RustSdkCryptoJs.ShieldColor.None, EventShieldColour.NONE], + [RustSdkCryptoJs.ShieldColor.Grey, EventShieldColour.GREY], + [RustSdkCryptoJs.ShieldColor.Red, EventShieldColour.RED], + ])("gets the right shield color (%i)", async (rustShield, expectedShield) => { + const mockEncryptionInfo = { + shieldState: jest.fn().mockReturnValue({ color: rustShield, message: null }), + } as unknown as RustSdkCryptoJs.EncryptionInfo; + olmMachine.getRoomEventEncryptionInfo.mockResolvedValue(mockEncryptionInfo); + + const res = await rustCrypto.getEncryptionInfoForEvent(await makeEncryptedEvent()); + expect(mockEncryptionInfo.shieldState).toHaveBeenCalledWith(false); + expect(res).not.toBe(null); + expect(res!.shieldColour).toEqual(expectedShield); + }); + + it.each([ + [null, null], + ["Encrypted by an unverified user.", EventShieldReason.UNVERIFIED_IDENTITY], + ["Encrypted by a device not verified by its owner.", EventShieldReason.UNSIGNED_DEVICE], + [ + "The authenticity of this encrypted message can't be guaranteed on this device.", + EventShieldReason.AUTHENTICITY_NOT_GUARANTEED, + ], + ["Encrypted by an unknown or deleted device.", EventShieldReason.UNKNOWN_DEVICE], + ["bloop", EventShieldReason.UNKNOWN], + ])("gets the right shield reason (%s)", async (rustReason, expectedReason) => { + // suppress the warning from the unknown shield reason + jest.spyOn(console, "warn").mockImplementation(() => {}); + + const mockEncryptionInfo = { + shieldState: jest + .fn() + .mockReturnValue({ color: RustSdkCryptoJs.ShieldColor.None, message: rustReason }), + } as unknown as RustSdkCryptoJs.EncryptionInfo; + olmMachine.getRoomEventEncryptionInfo.mockResolvedValue(mockEncryptionInfo); + + const res = await rustCrypto.getEncryptionInfoForEvent(await makeEncryptedEvent()); + expect(mockEncryptionInfo.shieldState).toHaveBeenCalledWith(false); + expect(res).not.toBe(null); + expect(res!.shieldReason).toEqual(expectedReason); + }); + }); + describe("get|setTrustCrossSignedDevices", () => { let rustCrypto: RustCrypto; diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index c6ff61e78e6..5f08b6dee1a 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -42,6 +42,7 @@ import { DeviceVerificationStatus, EventEncryptionInfo, EventShieldColour, + EventShieldReason, GeneratedSecretStorageKey, ImportRoomKeyProgressData, ImportRoomKeysOpts, @@ -788,10 +789,7 @@ export class RustCrypto extends TypedEventEmitter { - return { - shieldColour: EventShieldColour.NONE, - shieldReason: null, - }; + return this.eventDecryptor.getEncryptionInfoForEvent(event); } /** @@ -1484,14 +1482,7 @@ class EventDecryptor { try { const res = (await this.olmMachine.decryptRoomEvent( - JSON.stringify({ - event_id: event.getId(), - type: event.getWireType(), - sender: event.getSender(), - state_key: event.getStateKey(), - content: event.getWireContent(), - origin_server_ts: event.getTs(), - }), + stringifyEvent(event), new RustSdkCryptoJs.RoomId(event.getRoomId()!), )) as RustSdkCryptoJs.DecryptedRoomEvent; @@ -1549,6 +1540,20 @@ class EventDecryptor { } } + public async getEncryptionInfoForEvent(event: MatrixEvent): Promise { + if (!event.getClearContent()) { + // not successfully decrypted + return null; + } + + const encryptionInfo = await this.olmMachine.getRoomEventEncryptionInfo( + stringifyEvent(event), + new RustSdkCryptoJs.RoomId(event.getRoomId()!), + ); + + return rustEncryptionInfoToJsEncryptionInfo(encryptionInfo); + } + /** * Look for events which are waiting for a given megolm session * @@ -1606,6 +1611,61 @@ class EventDecryptor { } } +function stringifyEvent(event: MatrixEvent): string { + return JSON.stringify({ + event_id: event.getId(), + type: event.getWireType(), + sender: event.getSender(), + state_key: event.getStateKey(), + content: event.getWireContent(), + origin_server_ts: event.getTs(), + }); +} + +function rustEncryptionInfoToJsEncryptionInfo( + encryptionInfo: RustSdkCryptoJs.EncryptionInfo | undefined, +): EventEncryptionInfo | null { + if (encryptionInfo === undefined) { + // not decrypted here + return null; + } + + // TODO: use strict shield semantics. + const shieldState = encryptionInfo.shieldState(false); + + let shieldColour: EventShieldColour; + switch (shieldState.color) { + case RustSdkCryptoJs.ShieldColor.Grey: + shieldColour = EventShieldColour.GREY; + break; + case RustSdkCryptoJs.ShieldColor.None: + shieldColour = EventShieldColour.NONE; + break; + default: + shieldColour = EventShieldColour.RED; + } + + let shieldReason: EventShieldReason | null; + if (shieldState.message === null) { + shieldReason = null; + } else if (shieldState.message === "Encrypted by an unverified user.") { + shieldReason = EventShieldReason.UNVERIFIED_IDENTITY; + } else if (shieldState.message === "Encrypted by a device not verified by its owner.") { + shieldReason = EventShieldReason.UNSIGNED_DEVICE; + } else if ( + shieldState.message === "The authenticity of this encrypted message can't be guaranteed on this device." + ) { + shieldReason = EventShieldReason.AUTHENTICITY_NOT_GUARANTEED; + } else if (shieldState.message === "Encrypted by an unknown or deleted device.") { + shieldReason = EventShieldReason.UNKNOWN_DEVICE; + } else { + logger.warn(`Unknown shield state message '${shieldState.message}'`); + shieldReason = EventShieldReason.UNKNOWN; + } + + return { shieldColour, shieldReason }; +} + type RustCryptoEvents = | CryptoEvent.VerificationRequestReceived | CryptoEvent.UserTrustStatusChanged