Skip to content

Commit 1a02835

Browse files
committed
Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into staging
2 parents 4d90fec + 6f63ff1 commit 1a02835

File tree

13 files changed

+854
-1265
lines changed

13 files changed

+854
-1265
lines changed

.github/workflows/tests.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
strategy:
1919
matrix:
2020
specs: [integ, unit]
21-
node: ["lts/*", 21, 22]
21+
node: ["lts/*", 22]
2222
steps:
2323
- name: Checkout code
2424
uses: actions/checkout@v4
@@ -63,6 +63,16 @@ jobs:
6363
coverage
6464
!coverage/lcov-report
6565
66+
# Dummy completion job to simplify branch protections
67+
jest-complete:
68+
name: Jest tests
69+
needs: jest
70+
if: always()
71+
runs-on: ubuntu-latest
72+
steps:
73+
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
74+
run: exit 1
75+
6676
matrix-react-sdk:
6777
name: Downstream test matrix-react-sdk
6878
if: github.event_name == 'merge_group'

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ client.publicRooms(function (err, data) {
3939
```
4040

4141
See below for how to include libolm to enable end-to-end-encryption. Please check
42-
[the Node.js terminal app](examples/node) for a more complex example.
42+
[the Node.js terminal app](examples/node/README.md) for a more complex example.
4343

4444
To start the client:
4545

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@
118118
"matrix-mock-request": "^2.5.0",
119119
"node-fetch": "^2.7.0",
120120
"prettier": "3.3.2",
121-
"rimraf": "^5.0.0",
121+
"rimraf": "^6.0.0",
122122
"ts-node": "^10.9.2",
123123
"typedoc": "^0.26.0",
124124
"typedoc-plugin-coverage": "^3.0.0",

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

spec/unit/room.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1394,6 +1394,22 @@ describe("Room", function () {
13941394
expect(name).toEqual(userB);
13951395
});
13961396
});
1397+
1398+
it("recalculates in acceptable time without heroes", function () {
1399+
for (let i = 0; i < 5000; i++) {
1400+
addMember(`@person${i}:bar`, KnownMembership.Join, { name: `Person ${i % 20} ${i % 10} ${i % 3}` });
1401+
}
1402+
1403+
// This isn't a real performance test and has plenty of headroom because github
1404+
// runners don't offer any kind of speed consistency guarantee, but this should at
1405+
// least assert that the perf doesn't suddenly become n^2.
1406+
const start = performance.now();
1407+
for (let i = 0; i < 50; i++) {
1408+
room.recalculate();
1409+
}
1410+
const duration = performance.now() - start;
1411+
expect(duration).toBeLessThan(200);
1412+
});
13971413
});
13981414

13991415
describe("receipts", function () {

src/crypto-api/index.ts

Lines changed: 13 additions & 2 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

@@ -848,9 +854,14 @@ export interface CreateSecretStorageOpts {
848854
setupNewSecretStorage?: boolean;
849855

850856
/**
851-
* Function called to get the user's
852-
* current key backup passphrase. Should return a promise that resolves with a Uint8Array
857+
* Function called to get the user's current key backup passphrase.
858+
*
859+
* Should return a promise that resolves with a Uint8Array
853860
* containing the key, or rejects if the key cannot be obtained.
861+
*
862+
* Only used when the client has existing key backup, but no secret storage.
863+
*
864+
* @deprecated Not used by the Rust crypto stack.
854865
*/
855866
getKeyBackupPassphrase?: () => Promise<Uint8Array>;
856867
}

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/models/room.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
} from "./event-timeline-set";
2525
import { Direction, EventTimeline } from "./event-timeline";
2626
import { getHttpUriForMxc } from "../content-repo";
27-
import { compare, removeElement } from "../utils";
27+
import { removeElement } from "../utils";
2828
import { normalize, noUnsafeEventProps } from "../utils";
2929
import { IEvent, IThreadBundledRelationship, MatrixEvent, MatrixEventEvent, MatrixEventHandlerMap } from "./event";
3030
import { EventStatus } from "./event-status";
@@ -3451,7 +3451,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
34513451
return true;
34523452
});
34533453
// make sure members have stable order
3454-
otherMembers.sort((a, b) => compare(a.userId, b.userId));
3454+
const collator = new Intl.Collator();
3455+
otherMembers.sort((a, b) => collator.compare(a.userId, b.userId));
34553456
// only 5 first members, immitate summaryHeroes
34563457
otherMembers = otherMembers.slice(0, 5);
34573458
otherNames = otherMembers.map((m) => m.name);

0 commit comments

Comments
 (0)