Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide more options for starting dehydration #4619

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
Changes in [36.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v36.0.0) (2025-01-14)
==================================================================================================
## 🚨 BREAKING CHANGES

* Remove support for "legacy" MSC3898 group calling in MatrixRTCSession and CallMembership ([#4583](https://github.com/matrix-org/matrix-js-sdk/pull/4583)). Contributed by @toger5.

## ✨ Features

* MatrixRTC: Implement expiry logic for CallMembership and additional test coverage ([#4587](https://github.com/matrix-org/matrix-js-sdk/pull/4587)). Contributed by @toger5.

## 🐛 Bug Fixes

* Don't retry on 4xx responses ([#4601](https://github.com/matrix-org/matrix-js-sdk/pull/4601)). Contributed by @dbkr.
* Upgrade matrix-sdk-crypto-wasm to 12.1.0 ([#4596](https://github.com/matrix-org/matrix-js-sdk/pull/4596)). Contributed by @andybalaam.
* Avoid key prompts when resetting crypto ([#4586](https://github.com/matrix-org/matrix-js-sdk/pull/4586)). Contributed by @dbkr.
* Handle when aud OIDC claim is an Array ([#4584](https://github.com/matrix-org/matrix-js-sdk/pull/4584)). Contributed by @liamdiprose.
* Save the key backup key to 4S during `bootstrapSecretStorage ` ([#4542](https://github.com/matrix-org/matrix-js-sdk/pull/4542)). Contributed by @dbkr.
* Only re-prepare MatrixrRTC delayed disconnection event on 404 ([#4575](https://github.com/matrix-org/matrix-js-sdk/pull/4575)). Contributed by @toger5.


Changes in [35.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v35.1.0) (2024-12-18)
==================================================================================================
This release updates matrix-sdk-crypto-wasm to fix a bug which could prevent loading stored crypto state from storage.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "35.1.0",
"version": "36.0.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=20.0.0"
Expand Down Expand Up @@ -50,7 +50,7 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^12.1.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^13.0.0",
"@matrix-org/olm": "3.2.15",
"another-json": "^0.2.0",
"bs58": "^6.0.0",
Expand Down
37 changes: 36 additions & 1 deletion spec/integ/crypto/device-dehydration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ limitations under the License.
import "fake-indexeddb/auto";
import fetchMock from "fetch-mock-jest";

import { createClient, ClientEvent, MatrixClient, MatrixEvent } from "../../../src";
import { ClientEvent, createClient, MatrixClient, MatrixEvent } from "../../../src";
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
import { AddSecretStorageKeyOpts } from "../../../src/secret-storage";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { DehydratedDevicesEvents } from "../../../src/crypto-api";

describe("Device dehydration", () => {
it("should rehydrate and dehydrate a device", async () => {
Expand All @@ -40,6 +41,29 @@ describe("Device dehydration", () => {

await initializeSecretStorage(matrixClient, "@alice:localhost", "http://test.server");

const dehydratedDevices = matrixClient.getCrypto()!.dehydratedDevices();
let creationEventCount = 0;
let pickleKeyCachedEventCount = 0;
let rehydrationStartedCount = 0;
let rehydrationEndedCount = 0;
let rehydrationProgressEvent = 0;

dehydratedDevices.on(DehydratedDevicesEvents.DeviceCreated, () => {
creationEventCount++;
});
dehydratedDevices.on(DehydratedDevicesEvents.PickleKeyCached, () => {
pickleKeyCachedEventCount++;
});
dehydratedDevices.on(DehydratedDevicesEvents.RehydrationStarted, () => {
rehydrationStartedCount++;
});
dehydratedDevices.on(DehydratedDevicesEvents.RehydrationEnded, () => {
rehydrationEndedCount++;
});
dehydratedDevices.on(DehydratedDevicesEvents.RehydrationProgress, (roomKeyCount, toDeviceCount) => {
rehydrationProgressEvent++;
});

// count the number of times the dehydration key gets set
let setDehydrationCount = 0;
matrixClient.on(ClientEvent.AccountData, (event: MatrixEvent) => {
Expand Down Expand Up @@ -74,14 +98,19 @@ describe("Device dehydration", () => {
await crypto.startDehydration();

expect(dehydrationCount).toEqual(1);
expect(creationEventCount).toEqual(1);
expect(pickleKeyCachedEventCount).toEqual(1);

// a week later, we should have created another dehydrated device
const dehydrationPromise = new Promise<void>((resolve, reject) => {
resolveDehydrationPromise = resolve;
});
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
await dehydrationPromise;

expect(pickleKeyCachedEventCount).toEqual(1);
expect(dehydrationCount).toEqual(2);
expect(creationEventCount).toEqual(2);

// restart dehydration -- rehydrate the device that we created above,
// and create a new dehydrated device. We also set `createNewKey`, so
Expand Down Expand Up @@ -113,6 +142,12 @@ describe("Device dehydration", () => {
expect(setDehydrationCount).toEqual(2);
expect(eventsResponse.mock.calls).toHaveLength(2);

expect(rehydrationStartedCount).toEqual(1);
expect(rehydrationEndedCount).toEqual(1);
expect(creationEventCount).toEqual(3);
expect(rehydrationProgressEvent).toEqual(1);
expect(pickleKeyCachedEventCount).toEqual(2);

matrixClient.stopClient();
});
});
Expand Down
6 changes: 3 additions & 3 deletions spec/integ/matrix-client-syncing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ describe("MatrixClient syncing", () => {
});

it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
await client!.initLegacyCrypto();
await client!.initRustCrypto();

const roomId = "!cycles:example.org";

Expand Down Expand Up @@ -234,7 +234,7 @@ describe("MatrixClient syncing", () => {
});

it("should emit RoomEvent.MyMembership for knock->leave->knock cycles", async () => {
await client!.initLegacyCrypto();
await client!.initRustCrypto();

const roomId = "!cycles:example.org";

Expand Down Expand Up @@ -2580,7 +2580,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
idbHttpBackend.when("GET", "/pushrules/").respond(200, {});
idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });

await idbClient.initLegacyCrypto();
await idbClient.initRustCrypto();

const roomId = "!invite:example.org";

Expand Down
22 changes: 9 additions & 13 deletions spec/integ/sliding-sync-sdk.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { logger } from "../../src/logger";
import { emitPromise } from "../test-utils/test-utils";
import { defer } from "../../src/utils";
import { KnownMembership } from "../../src/@types/membership";
import { SyncCryptoCallbacks } from "../../src/common-crypto/CryptoBackend";

declare module "../../src/@types/event" {
interface AccountDataEvents {
Expand All @@ -57,6 +58,7 @@ describe("SlidingSyncSdk", () => {
let httpBackend: MockHttpBackend | undefined;
let sdk: SlidingSyncSdk | undefined;
let mockSlidingSync: SlidingSync | undefined;
let syncCryptoCallback: SyncCryptoCallbacks | undefined;
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";

Expand Down Expand Up @@ -126,8 +128,9 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync = mockifySlidingSync(new SlidingSync("", new Map(), {}, client, 0));
if (testOpts.withCrypto) {
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
await client!.initLegacyCrypto();
syncOpts.cryptoCallbacks = syncOpts.crypto = client!.crypto;
await client!.initRustCrypto({ useIndexedDB: false });
syncCryptoCallback = client!.getCrypto() as unknown as SyncCryptoCallbacks;
syncOpts.cryptoCallbacks = syncCryptoCallback;
}
httpBackend!.when("GET", "/_matrix/client/v3/pushrules").respond(200, {});
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts, syncOpts);
Expand Down Expand Up @@ -640,13 +643,6 @@ describe("SlidingSyncSdk", () => {
ext = findExtension("e2ee");
});

afterAll(async () => {
// needed else we do some async operations in the background which can cause Jest to whine:
// "Cannot log after tests are done. Did you forget to wait for something async in your test?"
// Attempted to log "Saving device tracking data null"."
client!.crypto!.stop();
});

it("gets enabled on the initial request only", () => {
expect(ext.onRequest(true)).toEqual({
enabled: true,
Expand All @@ -655,28 +651,28 @@ describe("SlidingSyncSdk", () => {
});

it("can update device lists", () => {
client!.crypto!.processDeviceLists = jest.fn();
syncCryptoCallback!.processDeviceLists = jest.fn();
ext.onResponse({
device_lists: {
changed: ["@alice:localhost"],
left: ["@bob:localhost"],
},
});
expect(client!.crypto!.processDeviceLists).toHaveBeenCalledWith({
expect(syncCryptoCallback!.processDeviceLists).toHaveBeenCalledWith({
changed: ["@alice:localhost"],
left: ["@bob:localhost"],
});
});

it("can update OTK counts and unused fallback keys", () => {
client!.crypto!.processKeyCounts = jest.fn();
syncCryptoCallback!.processKeyCounts = jest.fn();
ext.onResponse({
device_one_time_keys_count: {
signed_curve25519: 42,
},
device_unused_fallback_key_types: ["signed_curve25519"],
});
expect(client!.crypto!.processKeyCounts).toHaveBeenCalledWith({ signed_curve25519: 42 }, [
expect(syncCryptoCallback!.processKeyCounts).toHaveBeenCalledWith({ signed_curve25519: 42 }, [
"signed_curve25519",
]);
});
Expand Down
7 changes: 4 additions & 3 deletions spec/test-utils/E2EKeyReceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Debugger } from "debug";
import fetchMock from "fetch-mock-jest";

import type { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto";
import { deepCompare, sleep } from "../../src/utils";

/** Interface implemented by classes that intercept `/keys/upload` requests from test clients to catch the uploaded keys
*
Expand Down Expand Up @@ -82,9 +83,9 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
private async onKeyUploadRequest(onOnTimeKeysUploaded: () => void, options: RequestInit): Promise<object> {
const content = JSON.parse(options.body as string);

// device keys may only be uploaded once
// device keys should not change after they've been uploaded
if (content.device_keys && Object.keys(content.device_keys).length > 0) {
if (this.deviceKeys) {
if (this.deviceKeys && !deepCompare(this.deviceKeys.keys, content.device_keys.keys)) {
throw new Error("Application attempted to upload E2E device keys multiple times");
}
this.debug(`received device keys`);
Expand All @@ -98,7 +99,7 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
// otherwise the client ends up tight-looping one-time-key-uploads and filling the logs with junk.
if (Object.keys(this.oneTimeKeys).length > 0) {
this.debug(`received second batch of one-time keys: blocking response`);
await new Promise(() => {});
await sleep(50);
}

this.debug(`received ${Object.keys(content.one_time_keys).length} one-time keys`);
Expand Down
56 changes: 40 additions & 16 deletions spec/unit/embedded.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
WidgetApiToWidgetAction,
MatrixCapabilities,
ITurnServer,
IRoomEvent,
IOpenIDCredentials,
ISendEventFromWidgetResponseData,
WidgetApiResponseError,
Expand Down Expand Up @@ -635,12 +634,20 @@ describe("RoomWidgetClient", () => {
});

it("receives", async () => {
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
const init = makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
// Client needs to be told that the room state is loaded
widgetApi.emit(
`action:${WidgetApiToWidgetAction.UpdateState}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, { detail: { data: { state: [] } } }),
);
await init;

const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve));
const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, resolve));
// Let's assume that a state event comes in but it doesn't actually
// update the state of the room just yet (maybe it's unauthorized)
widgetApi.emit(
`action:${WidgetApiToWidgetAction.SendEvent}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
Expand All @@ -649,26 +656,43 @@ describe("RoomWidgetClient", () => {
// The client should've emitted about the received event
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
expect(await emittedSync).toEqual(SyncState.Syncing);
// It should've also inserted the event into the room object
// However it should not have changed the room state
const room = client.getRoom("!1:example.org");
expect(room).not.toBeNull();
expect(room!.currentState.getStateEvents("org.example.foo", "bar")).toBe(null);

// Now assume that the state event becomes favored by state
// resolution for whatever reason and enters into the current state
// of the room
widgetApi.emit(
`action:${WidgetApiToWidgetAction.UpdateState}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, {
detail: { data: { state: [event] } },
}),
);
// It should now have changed the room state
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
});

it("backfills", async () => {
widgetApi.readStateEvents.mockImplementation(async (eventType, limit, stateKey) =>
eventType === "org.example.foo" && (limit ?? Infinity) > 0 && stateKey === "bar"
? [event as IRoomEvent]
: [],
it("ignores state updates for other rooms", async () => {
const init = makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
// Client needs to be told that the room state is loaded
widgetApi.emit(
`action:${WidgetApiToWidgetAction.UpdateState}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, { detail: { data: { state: [] } } }),
);
await init;

await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");

const room = client.getRoom("!1:example.org");
expect(room).not.toBeNull();
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
// Now a room we're not interested in receives a state update
widgetApi.emit(
`action:${WidgetApiToWidgetAction.UpdateState}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, {
detail: { data: { state: [{ ...event, room_id: "!other-room:example.org" }] } },
}),
);
// No change to the room state
for (const room of client.getRooms()) {
expect(room.currentState.getStateEvents("org.example.foo", "bar")).toBe(null);
}
});
});

Expand Down
34 changes: 34 additions & 0 deletions spec/unit/room-member.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,40 @@ describe("RoomMember", function () {
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false, false);
expect(url).toEqual(null);
});

it("should return unauthenticated media URL if useAuthentication is not set", function () {
member.events.member = utils.mkEvent({
event: true,
type: "m.room.member",
skey: userA,
room: roomId,
user: userA,
content: {
membership: KnownMembership.Join,
avatar_url: "mxc://flibble/wibble",
},
});
const url = member.getAvatarUrl(hsUrl, 1, 1, "", false, false);
// Check for unauthenticated media prefix
expect(url?.indexOf("/_matrix/media/v3/")).not.toEqual(-1);
});

it("should return authenticated media URL if useAuthentication=true", function () {
member.events.member = utils.mkEvent({
event: true,
type: "m.room.member",
skey: userA,
room: roomId,
user: userA,
content: {
membership: KnownMembership.Join,
avatar_url: "mxc://flibble/wibble",
},
});
const url = member.getAvatarUrl(hsUrl, 1, 1, "", false, false, true);
// Check for authenticated media prefix
expect(url?.indexOf("/_matrix/client/v1/media/")).not.toEqual(-1);
});
});

describe("setPowerLevelEvent", function () {
Expand Down
Loading
Loading