1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-30 04:23:07 +03:00

Clean up code for handling decryption failures (#4126)

Various improvements, including:

* Defining an enum for decryption failure reasons
* Exposing the reason code as a property on Event
This commit is contained in:
Richard van der Hoff
2024-03-22 17:15:27 +00:00
committed by GitHub
parent a573727662
commit d1259b241c
14 changed files with 282 additions and 147 deletions

View File

@ -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<void>((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<void>((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<void>((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();
}

View File

@ -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);
});
});

View File

@ -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");

View File

@ -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",

View File

@ -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<string, string | Error>,
) {
super(msg);
this.name = "DecryptionError";
this.detailedString = detailedStringForDecryptionError(this, details);
}
}
function detailedStringForDecryptionError(err: DecryptionError, details?: Record<string, string | Error>): string {
let result = err.name + "[msg: " + err.message;
if (details) {
result +=
", " +
Object.keys(details)
.map((k) => k + ": " + details[k])
.join(", ");
}
result += "]";
return result;
}

View File

@ -498,6 +498,57 @@ export interface CryptoApi {
deleteKeyBackupVersion(version: string): Promise<void>;
}
/** 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`.
*/

View File

@ -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 ((<Error>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,

View File

@ -199,45 +199,6 @@ export abstract class DecryptionAlgorithm {
public sendSharedHistoryInboundSessions?(devicesByUser: Map<string, DeviceInfo[]>): Promise<void>;
}
/**
* 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<string, string | Error>,
) {
super(msg);
this.code = code;
this.name = "DecryptionError";
this.detailedString = detailedStringForDecryptionError(this, details);
}
}
function detailedStringForDecryptionError(err: DecryptionError, details?: Record<string, string | Error>): 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<P extends IParams = IParams>(
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";

View File

@ -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 ((<MatrixError>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 {

View File

@ -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 || {};

View File

@ -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<CryptoEvent, CryptoEventHandlerMap
const AlgClass = algorithms.DECRYPTION_CLASSES.get(algorithm);
if (!AlgClass) {
throw new algorithms.DecryptionError(
"UNKNOWN_ENCRYPTION_ALGORITHM",
throw new DecryptionError(
DecryptionFailureCode.UNKNOWN_ENCRYPTION_ALGORITHM,
'Unknown encryption algorithm "' + algorithm + '".',
);
}

View File

@ -41,13 +41,13 @@ import { TypedReEmitter } from "../ReEmitter";
import { MatrixError } from "../http-api";
import { TypedEventEmitter } from "./typed-event-emitter";
import { EventStatus } from "./event-status";
import { DecryptionError } from "../crypto/algorithms";
import { CryptoBackend } from "../common-crypto/CryptoBackend";
import { CryptoBackend, DecryptionError } from "../common-crypto/CryptoBackend";
import { WITHHELD_MESSAGES } from "../crypto/OlmDevice";
import { IAnnotatedPushRule } from "../@types/PushRules";
import { Room } from "./room";
import { EventTimeline } from "./event-timeline";
import { Membership } from "../@types/membership";
import { DecryptionFailureCode } from "../crypto-api";
export { EventStatus } from "./event-status";
@ -227,7 +227,18 @@ export interface IMessageVisibilityHidden {
const MESSAGE_VISIBLE: IMessageVisibilityVisible = Object.freeze({ visible: true });
export enum MatrixEventEvent {
/**
* An event has been decrypted, or we have failed to decrypt it.
*
* The payload consists of:
*
* * `event` - The {@link MatrixEvent} which we attempted to decrypt.
*
* * `err` - The error that occurred during decryption, or `undefined` if no error occurred.
* Avoid use of this: {@link MatrixEvent.decryptionFailureReason} is more useful.
*/
Decrypted = "Event.decrypted",
BeforeRedaction = "Event.beforeRedaction",
VisibilityChange = "Event.visibilityChange",
LocalEventIdReplaced = "Event.localEventIdReplaced",
@ -239,12 +250,6 @@ export enum MatrixEventEvent {
export type MatrixEventEmittedEvents = MatrixEventEvent | ThreadEvent.Update;
export type MatrixEventHandlerMap = {
/**
* Fires when an event is decrypted
*
* @param event - The matrix event which has been decrypted
* @param err - The error that occurred during decryption, or `undefined` if no error occurred.
*/
[MatrixEventEvent.Decrypted]: (event: MatrixEvent, err?: Error) => void;
[MatrixEventEvent.BeforeRedaction]: (event: MatrixEvent, redactionEvent: MatrixEvent) => void;
[MatrixEventEvent.VisibilityChange]: (event: MatrixEvent, visible: boolean) => void;
@ -274,6 +279,9 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
private _hasCachedExtEv = false;
private _cachedExtEv: Optional<ExtensibleEvent> = 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 TypedEventEmitter<MatrixEventEmittedEvents, Mat
* couldn't decrypt.
*/
public isDecryptionFailure(): boolean {
return this.clearEvent?.content?.msgtype === "m.bad.encrypted";
return this._decryptionFailureReason !== null;
}
/** If we failed to decrypt this event, the reason for the failure. Otherwise, `null`. */
public get decryptionFailureReason(): DecryptionFailureCode | null {
return this._decryptionFailureReason;
}
/*
@ -884,17 +897,14 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
while (true) {
this.retryDecryption = false;
let res: IEventDecryptionResult;
let err: Error | undefined = undefined;
try {
if (!crypto) {
res = this.badEncryptedMessage("Encryption not enabled");
} else {
res = await crypto.decryptEvent(this);
if (options.isRetry === true) {
logger.info(`Decrypted event on retry (${this.getDetails()})`);
}
const res = await crypto.decryptEvent(this);
if (options.isRetry === true) {
logger.info(`Decrypted event on retry (${this.getDetails()})`);
}
this.setClearData(res);
this._decryptionFailureReason = null;
} catch (e) {
const detailedError = e instanceof DecryptionError ? (<DecryptionError>e).detailedString : String(e);
@ -927,14 +937,12 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
// so we don't bother to log `e` separately.
logger.warn(`Error decrypting event (${this.getDetails()}): ${detailedError}`);
res = this.badEncryptedMessage(String(e));
this.setClearDataForDecryptionFailure(String(e));
this._decryptionFailureReason =
e instanceof DecryptionError ? (<DecryptionError>e).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<MatrixEventEmittedEvents, Mat
//
this.decryptionPromise = null;
this.retryDecryption = false;
this.setClearData(res);
// Before we emit the event, clear the push actions so that they can be recalculated
// by relevant code. We do this because the clear event has now changed, making it
@ -960,19 +967,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
}
}
private badEncryptedMessage(reason: string): IEventDecryptionResult {
return {
clearEvent: {
type: EventType.RoomMessage,
content: {
msgtype: "m.bad.encrypted",
body: "** Unable to decrypt: " + reason + " **",
},
},
encryptedDisabledForUnverifiedDevices: reason === `DecryptionError: ${WITHHELD_MESSAGES["m.unverified"]}`,
};
}
/**
* Update the cleartext data on this event.
*
@ -981,9 +975,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* @internal
*
* @param decryptionResult - the decryption result, including the plaintext and some key info
*
* @remarks
* Fires {@link MatrixEventEvent.Decrypted}
*/
private setClearData(decryptionResult: IEventDecryptionResult): void {
this.clearEvent = decryptionResult.clearEvent;
@ -991,7 +982,28 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
this.claimedEd25519Key = decryptionResult.claimedEd25519Key ?? null;
this.forwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain || [];
this.untrusted = decryptionResult.untrusted || false;
this.encryptedDisabledForUnverifiedDevices = decryptionResult.encryptedDisabledForUnverifiedDevices || false;
this.encryptedDisabledForUnverifiedDevices = false;
this.invalidateExtensibleEvent();
}
/**
* Update the cleartext data on this event after a decryption failure.
*
* @param reason - the textual reason for the failure
*/
private setClearDataForDecryptionFailure(reason: string): void {
this.clearEvent = {
type: EventType.RoomMessage,
content: {
msgtype: "m.bad.encrypted",
body: `** Unable to decrypt: ${reason} **`,
},
};
this.senderCurve25519Key = null;
this.claimedEd25519Key = null;
this.forwardingCurve25519KeyChain = [];
this.untrusted = false;
this.encryptedDisabledForUnverifiedDevices = reason === `DecryptionError: ${WITHHELD_MESSAGES["m.unverified"]}`;
this.invalidateExtensibleEvent();
}

View File

@ -23,7 +23,7 @@ import type { IEncryptedEventInfo } from "../crypto/api";
import { IContent, MatrixEvent, MatrixEventEvent } from "../models/event";
import { Room } from "../models/room";
import { RoomMember } from "../models/room-member";
import { BackupDecryptor, CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
import { BackupDecryptor, CryptoBackend, DecryptionError, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
import { logger, Logger } from "../logger";
import { IHttpOpts, MatrixHttpApi, Method } from "../http-api";
import { RoomEncryptor } from "./RoomEncryptor";
@ -39,6 +39,7 @@ import {
CrossSigningStatus,
CryptoCallbacks,
Curve25519AuthData,
DecryptionFailureCode,
DeviceVerificationStatus,
EventEncryptionInfo,
EventShieldColour,
@ -70,7 +71,6 @@ import { randomString } from "../randomstring";
import { ClientStoppedError } from "../errors";
import { ISignatures } from "../@types/signed";
import { encodeBase64 } from "../base64";
import { DecryptionError } from "../crypto/algorithms";
import { OutgoingRequestsManager } from "./OutgoingRequestsManager";
import { PerSessionKeyBackupDownloader } from "./PerSessionKeyBackupDownloader";
@ -1685,7 +1685,7 @@ class EventDecryptor {
switch (err.code) {
case RustSdkCryptoJs.DecryptionErrorCode.MissingRoomKey: {
jsError = 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,
@ -1699,7 +1699,7 @@ class EventDecryptor {
}
case RustSdkCryptoJs.DecryptionErrorCode.UnknownMessageIndex: {
jsError = new DecryptionError(
"OLM_UNKNOWN_MESSAGE_INDEX",
DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX,
"The sender's device has not sent us the keys for this message at this index.",
{
session: content.sender_key + "|" + content.session_id,
@ -1712,9 +1712,9 @@ class EventDecryptor {
break;
}
// We don't map MismatchedIdentityKeys for now, as there is no equivalent in legacy.
// Just put it on the `UNABLE_TO_DECRYPT` bucket.
// Just put it on the `UNKNOWN_ERROR` bucket.
default: {
jsError = new DecryptionError("UNABLE_TO_DECRYPT", err.description, {
jsError = new DecryptionError(DecryptionFailureCode.UNKNOWN_ERROR, err.description, {
session: content.sender_key + "|" + content.session_id,
});
break;
@ -1722,7 +1722,7 @@ class EventDecryptor {
}
throw jsError;
}
throw new DecryptionError("UNABLE_TO_DECRYPT", "Unknown error");
throw new DecryptionError(DecryptionFailureCode.UNKNOWN_ERROR, "Unknown error");
}
}

View File

@ -27,6 +27,7 @@ import { RoomMember } from "./models/room-member";
import { EventType } from "./@types/event";
import { IEventDecryptionResult } from "./@types/crypto";
import { DecryptionError } from "./crypto/algorithms";
import { DecryptionFailureCode } from "./crypto-api";
/**
* Create a {@link MatrixEvent}.
@ -143,7 +144,7 @@ export async function mkDecryptionFailureMatrixEvent(opts: {
sender: string;
/** The reason code for the failure */
code: string;
code: DecryptionFailureCode;
/** A textual reason for the failure */
msg: string;