diff --git a/spec/integ/crypto/crypto.spec.ts b/spec/integ/crypto/crypto.spec.ts index 9e2106f0f..0d348caef 100644 --- a/spec/integ/crypto/crypto.spec.ts +++ b/spec/integ/crypto/crypto.spec.ts @@ -80,12 +80,12 @@ import { SecretStorageKeyDescription } from "../../../src/secret-storage"; import { CrossSigningKey, CryptoCallbacks, + DecryptionFailureCode, EventShieldColour, EventShieldReason, KeyBackupInfo, } from "../../../src/crypto-api"; import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder"; -import { DecryptionError } from "../../../src/crypto/algorithms"; import { IKeyBackup } from "../../../src/crypto/backup"; import { createOlmAccount, @@ -470,9 +470,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, await startClientAndAwaitFirstSync(); const awaitUISI = new Promise((resolve) => { - aliceClient.on(MatrixEventEvent.Decrypted, (ev, err) => { - const error = err as DecryptionError; - if (error.code == "MEGOLM_UNKNOWN_INBOUND_SESSION_ID") { + aliceClient.on(MatrixEventEvent.Decrypted, (ev) => { + if (ev.decryptionFailureReason === DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID) { resolve(); } }); @@ -499,9 +498,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, await startClientAndAwaitFirstSync(); const awaitUnknownIndex = new Promise((resolve) => { - aliceClient.on(MatrixEventEvent.Decrypted, (ev, err) => { - const error = err as DecryptionError; - if (error.code == "OLM_UNKNOWN_MESSAGE_INDEX") { + aliceClient.on(MatrixEventEvent.Decrypted, (ev) => { + if (ev.decryptionFailureReason === DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX) { resolve(); } }); @@ -532,13 +530,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, await aliceClient.getCrypto()!.importRoomKeys([testData.MEGOLM_SESSION_DATA]); const awaitDecryptionError = new Promise((resolve) => { - aliceClient.on(MatrixEventEvent.Decrypted, (ev, err) => { - const error = err as DecryptionError; + aliceClient.on(MatrixEventEvent.Decrypted, (ev) => { // rust and libolm can't have an exact 1:1 mapping for all errors, // but some errors are part of API and should match if ( - error.code != "MEGOLM_UNKNOWN_INBOUND_SESSION_ID" && - error.code != "OLM_UNKNOWN_MESSAGE_INDEX" + ev.decryptionFailureReason !== DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID && + ev.decryptionFailureReason !== DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX ) { resolve(); } diff --git a/spec/unit/models/event.spec.ts b/spec/unit/models/event.spec.ts index 5ff9d745d..0b9416fd8 100644 --- a/spec/unit/models/event.spec.ts +++ b/spec/unit/models/event.spec.ts @@ -27,6 +27,8 @@ import { THREAD_RELATION_TYPE, TweakName, } from "../../../src"; +import { DecryptionFailureCode } from "../../../src/crypto-api"; +import { DecryptionError } from "../../../src/common-crypto/CryptoBackend"; describe("MatrixEvent", () => { it("should create copies of itself", () => { @@ -360,20 +362,50 @@ describe("MatrixEvent", () => { }); }); - it("should report decryption errors", async () => { + it("should report unknown decryption errors", async () => { + const decryptionListener = jest.fn(); + encryptedEvent.addListener(MatrixEventEvent.Decrypted, decryptionListener); + + const testError = new Error("test error"); const crypto = { - decryptEvent: jest.fn().mockRejectedValue(new Error("test error")), + decryptEvent: jest.fn().mockRejectedValue(testError), } as unknown as Crypto; await encryptedEvent.attemptDecryption(crypto); expect(encryptedEvent.isEncrypted()).toBeTruthy(); expect(encryptedEvent.isBeingDecrypted()).toBeFalsy(); expect(encryptedEvent.isDecryptionFailure()).toBeTruthy(); + expect(encryptedEvent.decryptionFailureReason).toEqual(DecryptionFailureCode.UNKNOWN_ERROR); expect(encryptedEvent.isEncryptedDisabledForUnverifiedDevices).toBeFalsy(); expect(encryptedEvent.getContent()).toEqual({ msgtype: "m.bad.encrypted", body: "** Unable to decrypt: Error: test error **", }); + expect(decryptionListener).toHaveBeenCalledWith(encryptedEvent, testError); + }); + + it("should report known decryption errors", async () => { + const decryptionListener = jest.fn(); + encryptedEvent.addListener(MatrixEventEvent.Decrypted, decryptionListener); + + const testError = new DecryptionError(DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID, "uisi"); + const crypto = { + decryptEvent: jest.fn().mockRejectedValue(testError), + } as unknown as Crypto; + + await encryptedEvent.attemptDecryption(crypto); + expect(encryptedEvent.isEncrypted()).toBeTruthy(); + expect(encryptedEvent.isBeingDecrypted()).toBeFalsy(); + expect(encryptedEvent.isDecryptionFailure()).toBeTruthy(); + expect(encryptedEvent.decryptionFailureReason).toEqual( + DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID, + ); + expect(encryptedEvent.isEncryptedDisabledForUnverifiedDevices).toBeFalsy(); + expect(encryptedEvent.getContent()).toEqual({ + msgtype: "m.bad.encrypted", + body: "** Unable to decrypt: DecryptionError: uisi **", + }); + expect(decryptionListener).toHaveBeenCalledWith(encryptedEvent, testError); }); it(`should report "DecryptionError: The sender has disabled encrypting to unverified devices."`, async () => { @@ -423,6 +455,8 @@ describe("MatrixEvent", () => { expect(eventAttemptDecryptionSpy).toHaveBeenCalledTimes(2); expect(crypto.decryptEvent).toHaveBeenCalledTimes(2); expect(encryptedEvent.getType()).toEqual("m.room.message"); + expect(encryptedEvent.isDecryptionFailure()).toBe(false); + expect(encryptedEvent.decryptionFailureReason).toBe(null); }); }); diff --git a/spec/unit/room-state.spec.ts b/spec/unit/room-state.spec.ts index 64a268dde..9e0c29f35 100644 --- a/spec/unit/room-state.spec.ts +++ b/spec/unit/room-state.spec.ts @@ -25,10 +25,11 @@ import { EventType, RelationType, UNSTABLE_MSC2716_MARKER } from "../../src/@typ import { MatrixEvent, MatrixEventEvent } from "../../src/models/event"; import { M_BEACON } from "../../src/@types/beacon"; import { MatrixClient } from "../../src/client"; -import { DecryptionError } from "../../src/crypto/algorithms"; import { defer } from "../../src/utils"; import { Room } from "../../src/models/room"; import { KnownMembership } from "../../src/@types/membership"; +import { DecryptionFailureCode } from "../../src/crypto-api"; +import { DecryptionError } from "../../src/common-crypto/CryptoBackend"; describe("RoomState", function () { const roomId = "!foo:bar"; @@ -1040,7 +1041,9 @@ describe("RoomState", function () { content: beacon1RelationContent, }); jest.spyOn(failedDecryptionRelatedEvent, "isDecryptionFailure").mockReturnValue(true); - mockClient.decryptEventIfNeeded.mockRejectedValue(new DecryptionError("ERR", "msg")); + mockClient.decryptEventIfNeeded.mockRejectedValue( + new DecryptionError(DecryptionFailureCode.UNKNOWN_ERROR, "msg"), + ); // spy on event.once const eventOnceSpy = jest.spyOn(failedDecryptionRelatedEvent, "once"); diff --git a/spec/unit/testing.spec.ts b/spec/unit/testing.spec.ts index a18147d86..49181b98b 100644 --- a/spec/unit/testing.spec.ts +++ b/spec/unit/testing.spec.ts @@ -16,6 +16,7 @@ limitations under the License. import { mkDecryptionFailureMatrixEvent, mkEncryptedMatrixEvent, mkMatrixEvent } from "../../src/testing"; import { EventType } from "../../src"; +import { DecryptionFailureCode } from "../../src/crypto-api"; describe("testing", () => { describe("mkMatrixEvent", () => { @@ -60,6 +61,7 @@ describe("testing", () => { expect(event.sender?.userId).toEqual("@alice:test"); expect(event.isEncrypted()).toBe(true); expect(event.isDecryptionFailure()).toBe(false); + expect(event.decryptionFailureReason).toBe(null); expect(event.getContent()).toEqual({ body: "blah" }); expect(event.getType()).toEqual("m.room.message"); }); @@ -70,13 +72,14 @@ describe("testing", () => { const event = await mkDecryptionFailureMatrixEvent({ sender: "@alice:test", roomId: "!test:room", - code: "UNKNOWN", + code: DecryptionFailureCode.UNKNOWN_ERROR, msg: "blah", }); expect(event.sender?.userId).toEqual("@alice:test"); expect(event.isEncrypted()).toBe(true); expect(event.isDecryptionFailure()).toBe(true); + expect(event.decryptionFailureReason).toEqual(DecryptionFailureCode.UNKNOWN_ERROR); expect(event.getContent()).toEqual({ body: "** Unable to decrypt: DecryptionError: blah **", msgtype: "m.bad.encrypted", diff --git a/src/common-crypto/CryptoBackend.ts b/src/common-crypto/CryptoBackend.ts index 0aadf93b4..401e4a091 100644 --- a/src/common-crypto/CryptoBackend.ts +++ b/src/common-crypto/CryptoBackend.ts @@ -17,7 +17,7 @@ limitations under the License. import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator"; import { IClearEvent, MatrixEvent } from "../models/event"; import { Room } from "../models/room"; -import { CryptoApi, ImportRoomKeysOpts } from "../crypto-api"; +import { CryptoApi, DecryptionFailureCode, ImportRoomKeysOpts } from "../crypto-api"; import { CrossSigningInfo, UserTrustLevel } from "../crypto/CrossSigning"; import { IEncryptedEventInfo } from "../crypto/api"; import { KeyBackupInfo, KeyBackupSession } from "../crypto-api/keybackup"; @@ -226,10 +226,6 @@ export interface EventDecryptionResult { * restored from backup) */ untrusted?: boolean; - /** - * The sender doesn't authorize the unverified devices to decrypt his messages - */ - encryptedDisabledForUnverifiedDevices?: boolean; } /** @@ -263,3 +259,43 @@ export interface BackupDecryptor { */ free(): void; } + +/** + * Exception thrown when decryption fails + * + * @param code - Reason code for the failure. + * + * @param msg - user-visible message describing the problem + * + * @param details - key/value pairs reported in the logs but not shown + * to the user. + */ +export class DecryptionError extends Error { + public readonly detailedString: string; + + public constructor( + public readonly code: DecryptionFailureCode, + msg: string, + details?: Record, + ) { + super(msg); + this.name = "DecryptionError"; + this.detailedString = detailedStringForDecryptionError(this, details); + } +} + +function detailedStringForDecryptionError(err: DecryptionError, details?: Record): string { + let result = err.name + "[msg: " + err.message; + + if (details) { + result += + ", " + + Object.keys(details) + .map((k) => k + ": " + details[k]) + .join(", "); + } + + result += "]"; + + return result; +} diff --git a/src/crypto-api.ts b/src/crypto-api.ts index 13d94a295..7d7eba937 100644 --- a/src/crypto-api.ts +++ b/src/crypto-api.ts @@ -498,6 +498,57 @@ export interface CryptoApi { deleteKeyBackupVersion(version: string): Promise; } +/** A reason code for a failure to decrypt an event. */ +export enum DecryptionFailureCode { + /** Message was encrypted with a Megolm session whose keys have not been shared with us. */ + MEGOLM_UNKNOWN_INBOUND_SESSION_ID = "MEGOLM_UNKNOWN_INBOUND_SESSION_ID", + + /** Message was encrypted with a Megolm session which has been shared with us, but in a later ratchet state. */ + OLM_UNKNOWN_MESSAGE_INDEX = "OLM_UNKNOWN_MESSAGE_INDEX", + + /** Unknown or unclassified error. */ + UNKNOWN_ERROR = "UNKNOWN_ERROR", + + /** @deprecated only used in legacy crypto */ + MEGOLM_BAD_ROOM = "MEGOLM_BAD_ROOM", + + /** @deprecated only used in legacy crypto */ + MEGOLM_MISSING_FIELDS = "MEGOLM_MISSING_FIELDS", + + /** @deprecated only used in legacy crypto */ + OLM_DECRYPT_GROUP_MESSAGE_ERROR = "OLM_DECRYPT_GROUP_MESSAGE_ERROR", + + /** @deprecated only used in legacy crypto */ + OLM_BAD_ENCRYPTED_MESSAGE = "OLM_BAD_ENCRYPTED_MESSAGE", + + /** @deprecated only used in legacy crypto */ + OLM_BAD_RECIPIENT = "OLM_BAD_RECIPIENT", + + /** @deprecated only used in legacy crypto */ + OLM_BAD_RECIPIENT_KEY = "OLM_BAD_RECIPIENT_KEY", + + /** @deprecated only used in legacy crypto */ + OLM_BAD_ROOM = "OLM_BAD_ROOM", + + /** @deprecated only used in legacy crypto */ + OLM_BAD_SENDER_CHECK_FAILED = "OLM_BAD_SENDER_CHECK_FAILED", + + /** @deprecated only used in legacy crypto */ + OLM_BAD_SENDER = "OLM_BAD_SENDER", + + /** @deprecated only used in legacy crypto */ + OLM_FORWARDED_MESSAGE = "OLM_FORWARDED_MESSAGE", + + /** @deprecated only used in legacy crypto */ + OLM_MISSING_CIPHERTEXT = "OLM_MISSING_CIPHERTEXT", + + /** @deprecated only used in legacy crypto */ + OLM_NOT_INCLUDED_IN_RECIPIENTS = "OLM_NOT_INCLUDED_IN_RECIPIENTS", + + /** @deprecated only used in legacy crypto */ + UNKNOWN_ENCRYPTION_ALGORITHM = "UNKNOWN_ENCRYPTION_ALGORITHM", +} + /** * Options object for `CryptoApi.bootstrapCrossSigning`. */ diff --git a/src/crypto/OlmDevice.ts b/src/crypto/OlmDevice.ts index f1e2dd9fa..6e645b72b 100644 --- a/src/crypto/OlmDevice.ts +++ b/src/crypto/OlmDevice.ts @@ -18,11 +18,12 @@ import { Account, InboundGroupSession, OutboundGroupSession, Session, Utility } import { logger, Logger } from "../logger"; import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store"; -import * as algorithms from "./algorithms"; import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base"; import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm"; import { IMegolmSessionData, OlmGroupSessionExtraData } from "../@types/crypto"; import { IMessage } from "./algorithms/olm"; +import { DecryptionFailureCode } from "../crypto-api"; +import { DecryptionError } from "../common-crypto/CryptoBackend"; // The maximum size of an event is 65K, and we base64 the content, so this is a // reasonable approximation to the biggest plaintext we can encrypt. @@ -1220,8 +1221,8 @@ export class OlmDevice { this.getInboundGroupSession(roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => { if (session === null || sessionData === null) { if (withheld) { - error = new algorithms.DecryptionError( - "MEGOLM_UNKNOWN_INBOUND_SESSION_ID", + error = new DecryptionError( + DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID, calculateWithheldMessage(withheld), { session: senderKey + "|" + sessionId, @@ -1236,8 +1237,8 @@ export class OlmDevice { res = session.decrypt(body); } catch (e) { if ((e)?.message === "OLM.UNKNOWN_MESSAGE_INDEX" && withheld) { - error = new algorithms.DecryptionError( - "MEGOLM_UNKNOWN_INBOUND_SESSION_ID", + error = new DecryptionError( + DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID, calculateWithheldMessage(withheld), { session: senderKey + "|" + sessionId, diff --git a/src/crypto/algorithms/base.ts b/src/crypto/algorithms/base.ts index d571cd220..37dae34bd 100644 --- a/src/crypto/algorithms/base.ts +++ b/src/crypto/algorithms/base.ts @@ -199,45 +199,6 @@ export abstract class DecryptionAlgorithm { public sendSharedHistoryInboundSessions?(devicesByUser: Map): Promise; } -/** - * Exception thrown when decryption fails - * - * @param msg - user-visible message describing the problem - * - * @param details - key/value pairs reported in the logs but not shown - * to the user. - */ -export class DecryptionError extends Error { - public readonly detailedString: string; - - public constructor( - public readonly code: string, - msg: string, - details?: Record, - ) { - super(msg); - this.code = code; - this.name = "DecryptionError"; - this.detailedString = detailedStringForDecryptionError(this, details); - } -} - -function detailedStringForDecryptionError(err: DecryptionError, details?: Record): string { - let result = err.name + "[msg: " + err.message; - - if (details) { - result += - ", " + - Object.keys(details) - .map((k) => k + ": " + details[k]) - .join(", "); - } - - result += "]"; - - return result; -} - export class UnknownDeviceError extends Error { /** * Exception thrown specifically when we want to warn the user to consider @@ -274,3 +235,6 @@ export function registerAlgorithm

( ENCRYPTION_CLASSES.set(algorithm, encryptor as new (params: IParams) => EncryptionAlgorithm); DECRYPTION_CLASSES.set(algorithm, decryptor as new (params: DecryptionClassParams) => DecryptionAlgorithm); } + +/* Re-export for backwards compatibility. Deprecated: this is an internal class. */ +export { DecryptionError } from "../../common-crypto/CryptoBackend"; diff --git a/src/crypto/algorithms/megolm.ts b/src/crypto/algorithms/megolm.ts index 610961eb3..5fa4a1b75 100644 --- a/src/crypto/algorithms/megolm.ts +++ b/src/crypto/algorithms/megolm.ts @@ -26,7 +26,6 @@ import * as olmlib from "../olmlib"; import { DecryptionAlgorithm, DecryptionClassParams, - DecryptionError, EncryptionAlgorithm, IParams, registerAlgorithm, @@ -45,6 +44,8 @@ import { OlmGroupSessionExtraData } from "../../@types/crypto"; import { MatrixError } from "../../http-api"; import { immediate, MapWithDefault } from "../../utils"; import { KnownMembership } from "../../@types/membership"; +import { DecryptionFailureCode } from "../../crypto-api"; +import { DecryptionError } from "../../common-crypto/CryptoBackend"; // determine whether the key can be shared with invitees export function isRoomSharedHistory(room: Room): boolean { @@ -1313,7 +1314,7 @@ export class MegolmDecryption extends DecryptionAlgorithm { const content = event.getWireContent(); if (!content.sender_key || !content.session_id || !content.ciphertext) { - throw new DecryptionError("MEGOLM_MISSING_FIELDS", "Missing fields in input"); + throw new DecryptionError(DecryptionFailureCode.MEGOLM_MISSING_FIELDS, "Missing fields in input"); } // we add the event to the pending list *before* we start decryption. @@ -1339,12 +1340,12 @@ export class MegolmDecryption extends DecryptionAlgorithm { throw e; } - let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR"; + let errorCode = DecryptionFailureCode.OLM_DECRYPT_GROUP_MESSAGE_ERROR; if ((e)?.message === "OLM.UNKNOWN_MESSAGE_INDEX") { this.requestKeysForEvent(event); - errorCode = "OLM_UNKNOWN_MESSAGE_INDEX"; + errorCode = DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX; } throw new DecryptionError(errorCode, e instanceof Error ? e.message : "Unknown Error: Error is undefined", { @@ -1377,13 +1378,13 @@ export class MegolmDecryption extends DecryptionAlgorithm { if (problem.fixed) { problemDescription += " Trying to create a new secure channel and re-requesting the keys."; } - throw new DecryptionError("MEGOLM_UNKNOWN_INBOUND_SESSION_ID", problemDescription, { + throw new DecryptionError(DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID, problemDescription, { session: content.sender_key + "|" + content.session_id, }); } throw new DecryptionError( - "MEGOLM_UNKNOWN_INBOUND_SESSION_ID", + DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID, "The sender's device has not sent us the keys for this message.", { session: content.sender_key + "|" + content.session_id, @@ -1405,7 +1406,10 @@ export class MegolmDecryption extends DecryptionAlgorithm { // (this is somewhat redundant, since the megolm session is scoped to the // room, so neither the sender nor a MITM can lie about the room_id). if (payload.room_id !== event.getRoomId()) { - throw new DecryptionError("MEGOLM_BAD_ROOM", "Message intended for room " + payload.room_id); + throw new DecryptionError( + DecryptionFailureCode.MEGOLM_BAD_ROOM, + "Message intended for room " + payload.room_id, + ); } return { diff --git a/src/crypto/algorithms/olm.ts b/src/crypto/algorithms/olm.ts index d5cec6f70..3bcff5872 100644 --- a/src/crypto/algorithms/olm.ts +++ b/src/crypto/algorithms/olm.ts @@ -22,11 +22,13 @@ import type { IEventDecryptionResult } from "../../@types/crypto"; import { logger } from "../../logger"; import * as olmlib from "../olmlib"; import { DeviceInfo } from "../deviceinfo"; -import { DecryptionAlgorithm, DecryptionError, EncryptionAlgorithm, registerAlgorithm } from "./base"; +import { DecryptionAlgorithm, EncryptionAlgorithm, registerAlgorithm } from "./base"; import { Room } from "../../models/room"; import { IContent, MatrixEvent } from "../../models/event"; import { IEncryptedContent, IOlmEncryptedContent } from "../index"; import { IInboundSession } from "../OlmDevice"; +import { DecryptionFailureCode } from "../../crypto-api"; +import { DecryptionError } from "../../common-crypto/CryptoBackend"; const DeviceVerification = DeviceInfo.DeviceVerification; @@ -159,11 +161,14 @@ class OlmDecryption extends DecryptionAlgorithm { const ciphertext = content.ciphertext; if (!ciphertext) { - throw new DecryptionError("OLM_MISSING_CIPHERTEXT", "Missing ciphertext"); + throw new DecryptionError(DecryptionFailureCode.OLM_MISSING_CIPHERTEXT, "Missing ciphertext"); } if (!(this.olmDevice.deviceCurve25519Key! in ciphertext)) { - throw new DecryptionError("OLM_NOT_INCLUDED_IN_RECIPIENTS", "Not included in recipients"); + throw new DecryptionError( + DecryptionFailureCode.OLM_NOT_INCLUDED_IN_RECIPIENTS, + "Not included in recipients", + ); } const message = ciphertext[this.olmDevice.deviceCurve25519Key!]; let payloadString: string; @@ -171,7 +176,7 @@ class OlmDecryption extends DecryptionAlgorithm { try { payloadString = await this.decryptMessage(deviceKey, message); } catch (e) { - throw new DecryptionError("OLM_BAD_ENCRYPTED_MESSAGE", "Bad Encrypted Message", { + throw new DecryptionError(DecryptionFailureCode.OLM_BAD_ENCRYPTED_MESSAGE, "Bad Encrypted Message", { sender: deviceKey, err: e as Error, }); @@ -182,14 +187,21 @@ class OlmDecryption extends DecryptionAlgorithm { // check that we were the intended recipient, to avoid unknown-key attack // https://github.com/vector-im/vector-web/issues/2483 if (payload.recipient != this.userId) { - throw new DecryptionError("OLM_BAD_RECIPIENT", "Message was intented for " + payload.recipient); + throw new DecryptionError( + DecryptionFailureCode.OLM_BAD_RECIPIENT, + "Message was intended for " + payload.recipient, + ); } if (payload.recipient_keys.ed25519 != this.olmDevice.deviceEd25519Key) { - throw new DecryptionError("OLM_BAD_RECIPIENT_KEY", "Message not intended for this device", { - intended: payload.recipient_keys.ed25519, - our_key: this.olmDevice.deviceEd25519Key!, - }); + throw new DecryptionError( + DecryptionFailureCode.OLM_BAD_RECIPIENT_KEY, + "Message not intended for this device", + { + intended: payload.recipient_keys.ed25519, + our_key: this.olmDevice.deviceEd25519Key!, + }, + ); } // check that the device that encrypted the event belongs to the user that the event claims it's from. @@ -216,18 +228,26 @@ class OlmDecryption extends DecryptionAlgorithm { try { await this.crypto.deviceList.downloadKeys([event.getSender()!], false); } catch (e) { - throw new DecryptionError("OLM_BAD_SENDER_CHECK_FAILED", "Could not verify sender identity", { - sender: deviceKey, - err: e as Error, - }); + throw new DecryptionError( + DecryptionFailureCode.OLM_BAD_SENDER_CHECK_FAILED, + "Could not verify sender identity", + { + sender: deviceKey, + err: e as Error, + }, + ); } senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, deviceKey); } if (senderKeyUser !== event.getSender() && senderKeyUser !== undefined && senderKeyUser !== null) { - throw new DecryptionError("OLM_BAD_SENDER", "Message claimed to be from " + event.getSender(), { - real_sender: senderKeyUser, - }); + throw new DecryptionError( + DecryptionFailureCode.OLM_BAD_SENDER, + "Message claimed to be from " + event.getSender(), + { + real_sender: senderKeyUser, + }, + ); } // check that the original sender matches what the homeserver told us, to @@ -235,16 +255,24 @@ class OlmDecryption extends DecryptionAlgorithm { // (this check is also provided via the sender's embedded ed25519 key, // which is checked elsewhere). if (payload.sender != event.getSender()) { - throw new DecryptionError("OLM_FORWARDED_MESSAGE", "Message forwarded from " + payload.sender, { - reported_sender: event.getSender()!, - }); + throw new DecryptionError( + DecryptionFailureCode.OLM_FORWARDED_MESSAGE, + "Message forwarded from " + payload.sender, + { + reported_sender: event.getSender()!, + }, + ); } // Olm events intended for a room have a room_id. if (payload.room_id !== event.getRoomId()) { - throw new DecryptionError("OLM_BAD_ROOM", "Message intended for room " + payload.room_id, { - reported_room: event.getRoomId() || "ROOM_ID_UNDEFINED", - }); + throw new DecryptionError( + DecryptionFailureCode.OLM_BAD_ROOM, + "Message intended for room " + payload.room_id, + { + reported_room: event.getRoomId() || "ROOM_ID_UNDEFINED", + }, + ); } const claimedKeys = payload.keys || {}; diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 64ce90551..83b45a82c 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -73,7 +73,7 @@ import { TypedEventEmitter } from "../models/typed-event-emitter"; import { IDeviceLists, ISyncResponse, IToDeviceEvent } from "../sync-accumulator"; import { ISignatures } from "../@types/signed"; import { IMessage } from "./algorithms/olm"; -import { BackupDecryptor, CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend"; +import { BackupDecryptor, CryptoBackend, DecryptionError, OnSyncCompletedData } from "../common-crypto/CryptoBackend"; import { RoomState, RoomStateEvent } from "../models/room-state"; import { MapWithDefault, recursiveMapToObject } from "../utils"; import { @@ -90,6 +90,7 @@ import { BackupTrustInfo, BootstrapCrossSigningOpts, CrossSigningStatus, + DecryptionFailureCode, DeviceVerificationStatus, EventEncryptionInfo, EventShieldColour, @@ -97,8 +98,8 @@ import { ImportRoomKeysOpts, KeyBackupCheck, KeyBackupInfo, - VerificationRequest as CryptoApiVerificationRequest, OwnDeviceKeys, + VerificationRequest as CryptoApiVerificationRequest, } from "../crypto-api"; import { Device, DeviceMap } from "../models/device"; import { deviceInfoToDevice } from "./device-converter"; @@ -4209,8 +4210,8 @@ export class Crypto extends TypedEventEmitter void; [MatrixEventEvent.BeforeRedaction]: (event: MatrixEvent, redactionEvent: MatrixEvent) => void; [MatrixEventEvent.VisibilityChange]: (event: MatrixEvent, visible: boolean) => void; @@ -274,6 +279,9 @@ export class MatrixEvent extends TypedEventEmitter = undefined; + /** If we failed to decrypt this event, the reason for the failure. Otherwise, `null`. */ + private _decryptionFailureReason: DecryptionFailureCode | null = null; + /* curve25519 key which we believe belongs to the sender of the event. See * getSenderKey() */ @@ -771,7 +779,12 @@ export class MatrixEvent extends TypedEventEmittere).detailedString : String(e); @@ -927,14 +937,12 @@ export class MatrixEvent extends TypedEventEmittere).code : DecryptionFailureCode.UNKNOWN_ERROR; } - // at this point, we've either successfully decrypted the event, or have given up - // (and set res to a 'badEncryptedMessage'). Either way, we can now set the - // cleartext of the event and raise Event.decrypted. - // - // make sure we clear 'decryptionPromise' before sending the 'Event.decrypted' event, + // Make sure we clear 'decryptionPromise' before sending the 'Event.decrypted' event, // otherwise the app will be confused to see `isBeingDecrypted` still set when // there isn't an `Event.decrypted` on the way. // @@ -942,7 +950,6 @@ export class MatrixEvent extends TypedEventEmitter