Skip to content

Commit 5320168

Browse files
authored
Element-R: detect "withheld key" UTD errors, and mark them as such (#4302)
Partial fix to element-hq/element-web#27653
1 parent 996663b commit 5320168

File tree

6 files changed

+117
-28
lines changed

6 files changed

+117
-28
lines changed

spec/integ/crypto/crypto.spec.ts

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2333,8 +2333,82 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
23332333
});
23342334

23352335
describe("m.room_key.withheld handling", () => {
2336-
// TODO: there are a bunch more tests for this sort of thing in spec/unit/crypto/algorithms/megolm.spec.ts.
2337-
// They should be converted to integ tests and moved.
2336+
describe.each([
2337+
["m.blacklisted", "The sender has blocked you.", DecryptionFailureCode.MEGOLM_KEY_WITHHELD],
2338+
[
2339+
"m.unverified",
2340+
"The sender has disabled encrypting to unverified devices.",
2341+
DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
2342+
],
2343+
])(
2344+
"Decryption fails with withheld error if a withheld notice with code '%s' is received",
2345+
(withheldCode, expectedMessage, expectedErrorCode) => {
2346+
// TODO: test arrival after the event too.
2347+
it.each(["before"])("%s the event", async (when) => {
2348+
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
2349+
await startClientAndAwaitFirstSync();
2350+
2351+
// A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
2352+
const awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
2353+
2354+
// Send Alice an encrypted room event which looks like it was encrypted with a megolm session
2355+
async function sendEncryptedEvent() {
2356+
const event = {
2357+
...testData.ENCRYPTED_EVENT,
2358+
origin_server_ts: Date.now(),
2359+
};
2360+
const syncResponse = {
2361+
next_batch: 1,
2362+
rooms: { join: { [ROOM_ID]: { timeline: { events: [event] } } } },
2363+
};
2364+
2365+
syncResponder.sendOrQueueSyncResponse(syncResponse);
2366+
await syncPromise(aliceClient);
2367+
}
2368+
2369+
// Send Alice a withheld notice
2370+
async function sendWithheldMessage() {
2371+
const withheldMessage = {
2372+
type: "m.room_key.withheld",
2373+
sender: "@bob:example.com",
2374+
content: {
2375+
algorithm: "m.megolm.v1.aes-sha2",
2376+
room_id: ROOM_ID,
2377+
sender_key: testData.ENCRYPTED_EVENT.content!.sender_key,
2378+
session_id: testData.ENCRYPTED_EVENT.content!.session_id,
2379+
code: withheldCode,
2380+
reason: "zzz",
2381+
},
2382+
};
2383+
2384+
syncResponder.sendOrQueueSyncResponse({
2385+
next_batch: 1,
2386+
to_device: { events: [withheldMessage] },
2387+
});
2388+
await syncPromise(aliceClient);
2389+
}
2390+
2391+
if (when === "before") {
2392+
await sendWithheldMessage();
2393+
await sendEncryptedEvent();
2394+
} else {
2395+
await sendEncryptedEvent();
2396+
await sendWithheldMessage();
2397+
}
2398+
2399+
const ev = await awaitDecryption;
2400+
expect(ev.getContent()).toEqual({
2401+
body: `** Unable to decrypt: DecryptionError: ${expectedMessage} **`,
2402+
msgtype: "m.bad.encrypted",
2403+
});
2404+
2405+
expect(ev.decryptionFailureReason).toEqual(expectedErrorCode);
2406+
2407+
// `isEncryptedDisabledForUnverifiedDevices` should be true for `m.unverified` and false for other errors.
2408+
expect(ev.isEncryptedDisabledForUnverifiedDevices).toEqual(withheldCode === "m.unverified");
2409+
});
2410+
},
2411+
);
23382412

23392413
oldBackendOnly("does not block decryption on an 'm.unavailable' report", async function () {
23402414
// there may be a key downloads for alice

spec/unit/models/event.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,12 @@ describe("MatrixEvent", () => {
412412
const crypto = {
413413
decryptEvent: jest
414414
.fn()
415-
.mockRejectedValue("DecryptionError: The sender has disabled encrypting to unverified devices."),
415+
.mockRejectedValue(
416+
new DecryptionError(
417+
DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
418+
"The sender has disabled encrypting to unverified devices.",
419+
),
420+
),
416421
} as unknown as Crypto;
417422

418423
await encryptedEvent.attemptDecryption(crypto);

src/crypto-api/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,12 @@ export enum DecryptionFailureCode {
557557
/** Message was encrypted with a Megolm session whose keys have not been shared with us. */
558558
MEGOLM_UNKNOWN_INBOUND_SESSION_ID = "MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
559559

560+
/** A special case of {@link MEGOLM_UNKNOWN_INBOUND_SESSION_ID}: the sender has told us it is withholding the key. */
561+
MEGOLM_KEY_WITHHELD = "MEGOLM_KEY_WITHHELD",
562+
563+
/** A special case of {@link MEGOLM_KEY_WITHHELD}: the sender has told us it is withholding the key, because the current device is unverified. */
564+
MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE = "MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE",
565+
560566
/** Message was encrypted with a Megolm session which has been shared with us, but in a later ratchet state. */
561567
OLM_UNKNOWN_MESSAGE_INDEX = "OLM_UNKNOWN_MESSAGE_INDEX",
562568

src/crypto/OlmDevice.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,13 +1221,13 @@ export class OlmDevice {
12211221
this.getInboundGroupSession(roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => {
12221222
if (session === null || sessionData === null) {
12231223
if (withheld) {
1224-
error = new DecryptionError(
1225-
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
1226-
calculateWithheldMessage(withheld),
1227-
{
1228-
session: senderKey + "|" + sessionId,
1229-
},
1230-
);
1224+
const failureCode =
1225+
withheld.code === "m.unverified"
1226+
? DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE
1227+
: DecryptionFailureCode.MEGOLM_KEY_WITHHELD;
1228+
error = new DecryptionError(failureCode, calculateWithheldMessage(withheld), {
1229+
session: senderKey + "|" + sessionId,
1230+
});
12311231
}
12321232
result = null;
12331233
return;
@@ -1237,13 +1237,13 @@ export class OlmDevice {
12371237
res = session.decrypt(body);
12381238
} catch (e) {
12391239
if ((<Error>e)?.message === "OLM.UNKNOWN_MESSAGE_INDEX" && withheld) {
1240-
error = new DecryptionError(
1241-
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
1242-
calculateWithheldMessage(withheld),
1243-
{
1244-
session: senderKey + "|" + sessionId,
1245-
},
1246-
);
1240+
const failureCode =
1241+
withheld.code === "m.unverified"
1242+
? DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE
1243+
: DecryptionFailureCode.MEGOLM_KEY_WITHHELD;
1244+
error = new DecryptionError(failureCode, calculateWithheldMessage(withheld), {
1245+
session: senderKey + "|" + sessionId,
1246+
});
12471247
} else {
12481248
error = <Error>e;
12491249
}

src/models/event.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ import { MatrixError } from "../http-api";
4343
import { TypedEventEmitter } from "./typed-event-emitter";
4444
import { EventStatus } from "./event-status";
4545
import { CryptoBackend, DecryptionError } from "../common-crypto/CryptoBackend";
46-
import { WITHHELD_MESSAGES } from "../crypto/OlmDevice";
4746
import { IAnnotatedPushRule } from "../@types/PushRules";
4847
import { Room } from "./room";
4948
import { EventTimeline } from "./event-timeline";
@@ -312,12 +311,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
312311
private thread?: Thread;
313312
private threadId?: string;
314313

315-
/*
316-
* True if this event is an encrypted event which we failed to decrypt, the receiver's device is unverified and
317-
* the sender has disabled encrypting to unverified devices.
318-
*/
319-
private encryptedDisabledForUnverifiedDevices = false;
320-
321314
/* Set an approximate timestamp for the event relative the local clock.
322315
* This will inherently be approximate because it doesn't take into account
323316
* the time between the server putting the 'age' field on the event as it sent
@@ -787,12 +780,14 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
787780
return this._decryptionFailureReason;
788781
}
789782

790-
/*
783+
/**
791784
* True if this event is an encrypted event which we failed to decrypt, the receiver's device is unverified and
792785
* the sender has disabled encrypting to unverified devices.
786+
*
787+
* @deprecated: Prefer `event.decryptionFailureReason === DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE`.
793788
*/
794789
public get isEncryptedDisabledForUnverifiedDevices(): boolean {
795-
return this.isDecryptionFailure() && this.encryptedDisabledForUnverifiedDevices;
790+
return this.decryptionFailureReason === DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE;
796791
}
797792

798793
public shouldAttemptDecryption(): boolean {
@@ -982,7 +977,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
982977
this.claimedEd25519Key = decryptionResult.claimedEd25519Key ?? null;
983978
this.forwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain || [];
984979
this.untrusted = decryptionResult.untrusted || false;
985-
this.encryptedDisabledForUnverifiedDevices = false;
986980
this.invalidateExtensibleEvent();
987981
}
988982

@@ -1003,7 +997,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
1003997
this.claimedEd25519Key = null;
1004998
this.forwardingCurve25519KeyChain = [];
1005999
this.untrusted = false;
1006-
this.encryptedDisabledForUnverifiedDevices = reason === `DecryptionError: ${WITHHELD_MESSAGES["m.unverified"]}`;
10071000
this.invalidateExtensibleEvent();
10081001
}
10091002

src/rust-crypto/rust-crypto.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1787,6 +1787,17 @@ class EventDecryptor {
17871787
}
17881788
}
17891789

1790+
// If we got a withheld code, expose that.
1791+
if (err.maybe_withheld) {
1792+
// Unfortunately the Rust SDK API doesn't let us distinguish between different withheld cases, other than
1793+
// by string-matching.
1794+
const failureCode =
1795+
err.maybe_withheld === "The sender has disabled encrypting to unverified devices."
1796+
? DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE
1797+
: DecryptionFailureCode.MEGOLM_KEY_WITHHELD;
1798+
throw new DecryptionError(failureCode, err.maybe_withheld, errorDetails);
1799+
}
1800+
17901801
switch (err.code) {
17911802
case RustSdkCryptoJs.DecryptionErrorCode.MissingRoomKey:
17921803
throw new DecryptionError(

0 commit comments

Comments
 (0)