Skip to content

Commit

Permalink
Use a different error code for UTDs when user was not in the room (#4172
Browse files Browse the repository at this point in the history
)

* use a different error code for UTDs when user was not in the room

* if user is invited, treat it as unexpected UTD
  • Loading branch information
uhoreg authored Apr 26, 2024
1 parent 65d858f commit 64505de
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 1 deletion.
67 changes: 66 additions & 1 deletion spec/integ/crypto/crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ import {
} from "./olm-utils";
import { ToDevicePayload } from "../../../src/models/ToDeviceMessage";
import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator";
import { UNSIGNED_MEMBERSHIP_FIELD } from "../../../src/@types/event";
import { KnownMembership } from "../../../src/@types/membership";

afterEach(() => {
Expand Down Expand Up @@ -533,14 +534,15 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});

describe("Historical events", () => {
async function sendEventAndAwaitDecryption(): Promise<MatrixEvent> {
async function sendEventAndAwaitDecryption(props: Partial<IEvent> = {}): Promise<MatrixEvent> {
// A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
const awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);

// Ensure that the timestamp pre-dates the creation of our device: set it to 24 hours ago
const encryptedEvent = {
...testData.ENCRYPTED_EVENT,
origin_server_ts: Date.now() - 24 * 3600 * 1000,
...props,
};

const syncResponse = {
Expand Down Expand Up @@ -611,6 +613,69 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const ev = await sendEventAndAwaitDecryption();
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP);
});

newBackendOnly("fails with NOT_JOINED if user is not member of room", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
});
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();

const ev = await sendEventAndAwaitDecryption({
unsigned: {
[UNSIGNED_MEMBERSHIP_FIELD.name]: "leave",
},
});
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED);
});

newBackendOnly(
"fails with another error when the server reports user was a member of the room",
async () => {
// This tests that when the server reports that the user
// was invited at the time the event was sent, then we
// don't get a HISTORICAL_MESSAGE_USER_NOT_JOINED error,
// and instead get some other error, since the user should
// have gotten the key for the event.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
});
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();

const ev = await sendEventAndAwaitDecryption({
unsigned: {
[UNSIGNED_MEMBERSHIP_FIELD.name]: "invite",
},
});
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
},
);

newBackendOnly(
"fails with another error when the server reports user was a member of the room",
async () => {
// This tests that when the server reports the user's
// membership, and reports that the user was joined, then we
// don't get a HISTORICAL_MESSAGE_USER_NOT_JOINED error, and
// instead get some other error.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
});
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();

const ev = await sendEventAndAwaitDecryption({
unsigned: {
[UNSIGNED_MEMBERSHIP_FIELD.name]: "join",
},
});
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
},
);
});

it("Decryption fails with Unable to decrypt for other errors", async () => {
Expand Down
7 changes: 7 additions & 0 deletions src/@types/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,13 @@ export const LOCAL_NOTIFICATION_SETTINGS_PREFIX = new UnstableValue(
*/
export const UNSIGNED_THREAD_ID_FIELD = new UnstableValue("thread_id", "org.matrix.msc4023.thread_id");

/**
* https://github.com/matrix-org/matrix-spec-proposals/pull/4115
*
* @experimental
*/
export const UNSIGNED_MEMBERSHIP_FIELD = new UnstableValue("membership", "io.element.msc4115.membership");

/**
* @deprecated in favour of {@link EncryptedFile}
*/
Expand Down
5 changes: 5 additions & 0 deletions src/crypto-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,11 @@ export enum DecryptionFailureCode {
*/
HISTORICAL_MESSAGE_WORKING_BACKUP = "HISTORICAL_MESSAGE_WORKING_BACKUP",

/**
* Message was sent when the user was not a member of the room.
*/
HISTORICAL_MESSAGE_USER_NOT_JOINED = "HISTORICAL_MESSAGE_USER_NOT_JOINED",

/** Unknown or unclassified error. */
UNKNOWN_ERROR = "UNKNOWN_ERROR",

Expand Down
18 changes: 18 additions & 0 deletions src/models/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
RelationType,
ToDeviceMessageId,
UNSIGNED_THREAD_ID_FIELD,
UNSIGNED_MEMBERSHIP_FIELD,
} from "../@types/event";
import { Crypto } from "../crypto";
import { deepSortedObjectEntries, internaliseString } from "../utils";
Expand Down Expand Up @@ -76,6 +77,7 @@ export interface IUnsigned {
"invite_room_state"?: StrippedState[];
"m.relations"?: Record<RelationType | string, any>; // No common pattern for aggregated relations
[UNSIGNED_THREAD_ID_FIELD.name]?: string;
[UNSIGNED_MEMBERSHIP_FIELD.name]?: Membership | string;
}

export interface IThreadBundledRelationship {
Expand Down Expand Up @@ -721,6 +723,22 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
return this.event.state_key !== undefined;
}

/**
* Get the user's room membership at the time the event was sent, as reported
* by the server. This uses MSC4115.
*
* @returns The user's room membership, or `undefined` if the server does
* not report it.
*/
public getMembershipAtEvent(): Membership | string | undefined {
const unsigned = this.getUnsigned();
if (typeof unsigned[UNSIGNED_MEMBERSHIP_FIELD.name] === "string") {
return unsigned[UNSIGNED_MEMBERSHIP_FIELD.name];
} else {
return undefined;
}
}

/**
* Replace the content of this event with encrypted versions.
* (This is used when sending an event; it should not be used by applications).
Expand Down
12 changes: 12 additions & 0 deletions src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import anotherjson from "another-json";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";

import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
import { KnownMembership } from "../@types/membership";
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator";
import type { IEncryptedEventInfo } from "../crypto/api";
import { IContent, MatrixEvent, MatrixEventEvent } from "../models/event";
Expand Down Expand Up @@ -1741,6 +1742,17 @@ class EventDecryptor {
) {
this.perSessionBackupDownloader.onDecryptionKeyMissingError(event.getRoomId()!, content.session_id!);

// If the server is telling us our membership at the time the event
// was sent, and it isn't "join", we use a different error code.
const membership = event.getMembershipAtEvent();
if (membership && membership !== KnownMembership.Join && membership !== KnownMembership.Invite) {
throw new DecryptionError(
DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED,
"This message was sent when we were not a member of the room.",
errorDetails,
);
}

// If the event was sent before this device was created, we use some different error codes.
if (event.getTs() <= this.olmMachine.deviceCreationTimeMs) {
if (serverBackupInfo === null) {
Expand Down

0 comments on commit 64505de

Please sign in to comment.