1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-06 12:02:40 +03:00

Use a different error code for UTDs when user was not in the room (#4172)

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

* if user is invited, treat it as unexpected UTD
This commit is contained in:
Hubert Chathi
2024-04-26 09:38:10 -04:00
committed by GitHub
parent 65d858f9a3
commit 64505de36b
5 changed files with 108 additions and 1 deletions

View File

@@ -101,6 +101,7 @@ import {
} from "./olm-utils"; } from "./olm-utils";
import { ToDevicePayload } from "../../../src/models/ToDeviceMessage"; import { ToDevicePayload } from "../../../src/models/ToDeviceMessage";
import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator"; import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator";
import { UNSIGNED_MEMBERSHIP_FIELD } from "../../../src/@types/event";
import { KnownMembership } from "../../../src/@types/membership"; import { KnownMembership } from "../../../src/@types/membership";
afterEach(() => { afterEach(() => {
@@ -533,7 +534,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
}); });
describe("Historical events", () => { 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. // A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
const awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted); const awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
@@ -541,6 +542,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const encryptedEvent = { const encryptedEvent = {
...testData.ENCRYPTED_EVENT, ...testData.ENCRYPTED_EVENT,
origin_server_ts: Date.now() - 24 * 3600 * 1000, origin_server_ts: Date.now() - 24 * 3600 * 1000,
...props,
}; };
const syncResponse = { const syncResponse = {
@@ -611,6 +613,69 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const ev = await sendEventAndAwaitDecryption(); const ev = await sendEventAndAwaitDecryption();
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP); 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 () => { it("Decryption fails with Unable to decrypt for other errors", async () => {

View File

@@ -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"); 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} * @deprecated in favour of {@link EncryptedFile}
*/ */

View File

@@ -560,6 +560,11 @@ export enum DecryptionFailureCode {
*/ */
HISTORICAL_MESSAGE_WORKING_BACKUP = "HISTORICAL_MESSAGE_WORKING_BACKUP", 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 or unclassified error. */
UNKNOWN_ERROR = "UNKNOWN_ERROR", UNKNOWN_ERROR = "UNKNOWN_ERROR",

View File

@@ -31,6 +31,7 @@ import {
RelationType, RelationType,
ToDeviceMessageId, ToDeviceMessageId,
UNSIGNED_THREAD_ID_FIELD, UNSIGNED_THREAD_ID_FIELD,
UNSIGNED_MEMBERSHIP_FIELD,
} from "../@types/event"; } from "../@types/event";
import { Crypto } from "../crypto"; import { Crypto } from "../crypto";
import { deepSortedObjectEntries, internaliseString } from "../utils"; import { deepSortedObjectEntries, internaliseString } from "../utils";
@@ -76,6 +77,7 @@ export interface IUnsigned {
"invite_room_state"?: StrippedState[]; "invite_room_state"?: StrippedState[];
"m.relations"?: Record<RelationType | string, any>; // No common pattern for aggregated relations "m.relations"?: Record<RelationType | string, any>; // No common pattern for aggregated relations
[UNSIGNED_THREAD_ID_FIELD.name]?: string; [UNSIGNED_THREAD_ID_FIELD.name]?: string;
[UNSIGNED_MEMBERSHIP_FIELD.name]?: Membership | string;
} }
export interface IThreadBundledRelationship { export interface IThreadBundledRelationship {
@@ -721,6 +723,22 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
return this.event.state_key !== undefined; 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. * Replace the content of this event with encrypted versions.
* (This is used when sending an event; it should not be used by applications). * (This is used when sending an event; it should not be used by applications).

View File

@@ -18,6 +18,7 @@ import anotherjson from "another-json";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm"; import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto"; import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
import { KnownMembership } from "../@types/membership";
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator"; import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator";
import type { IEncryptedEventInfo } from "../crypto/api"; import type { IEncryptedEventInfo } from "../crypto/api";
import { IContent, MatrixEvent, MatrixEventEvent } from "../models/event"; import { IContent, MatrixEvent, MatrixEventEvent } from "../models/event";
@@ -1741,6 +1742,17 @@ class EventDecryptor {
) { ) {
this.perSessionBackupDownloader.onDecryptionKeyMissingError(event.getRoomId()!, content.session_id!); 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 the event was sent before this device was created, we use some different error codes.
if (event.getTs() <= this.olmMachine.deviceCreationTimeMs) { if (event.getTs() <= this.olmMachine.deviceCreationTimeMs) {
if (serverBackupInfo === null) { if (serverBackupInfo === null) {