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

Add CryptoApi.encryptToDeviceMessages() and deprecate Crypto.encryptAndSendToDevices() #4380

Merged
merged 27 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ca07b18
Add CryptoApi. encryptToDeviceMessages
hughns Aug 31, 2024
6a0d8e2
Overload MatrixClient. encryptAndSendToDevices instead of deprecating
hughns Sep 2, 2024
354f5b9
Revert "Overload MatrixClient. encryptAndSendToDevices instead of dep…
hughns Sep 2, 2024
28456ce
Feedback from code review
hughns Sep 2, 2024
94d68e8
Use temporary pre-release build of @matrix-org/matrix-sdk-crypto-wasm
hughns Sep 3, 2024
3266a69
Deduplicate user IDs
hughns Sep 4, 2024
8afcc05
Test for RustCrypto implementation
hughns Sep 4, 2024
7bb6a1b
Use ensureSessionsForUsers()
hughns Sep 4, 2024
687ce0d
Encrypt to-device messages in parallel
hughns Sep 6, 2024
5e978b9
Use release version of matrix-sdk-crypto-wasm
hughns Sep 6, 2024
e8bc1f5
Merge branch 'develop' into hughns/rust-send-to-device
hughns Sep 9, 2024
f8a6608
Upgrade matrix-sdk-crypto-wasm to v8
hughns Sep 9, 2024
eead466
Merge branch 'hughns/matrix-sdk-crypto-wasm-8' into hughns/rust-send-…
hughns Sep 9, 2024
da0a394
Merge branch 'develop' into hughns/rust-send-to-device
hughns Sep 13, 2024
6db31ab
Merge branch 'develop' into hughns/rust-send-to-device
hughns Sep 18, 2024
4326169
Sync with develop
hughns Sep 18, 2024
f6df6ba
Merge branch 'develop' into hughns/rust-send-to-device
hughns Oct 7, 2024
91cab8e
Add test for olmlib CryptoApi
hughns Oct 7, 2024
b3647a8
Fix link
hughns Oct 7, 2024
f29f40a
Feedback from review
hughns Oct 11, 2024
2be86fe
Move libolm implementation to better place in file
hughns Oct 11, 2024
538b39b
FIx doc
hughns Oct 11, 2024
a87a29f
Integration test
hughns Oct 22, 2024
505d15d
Make sure test device is known to client
hughns Oct 23, 2024
d5ab8d8
Merge branch 'develop' into hughns/rust-send-to-device
hughns Oct 23, 2024
9627f8c
Feedback from review
hughns Oct 25, 2024
f80d0ae
Merge branch 'develop' into hughns/rust-send-to-device
hughns Oct 28, 2024
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
152 changes: 152 additions & 0 deletions spec/integ/crypto/to-device-messages.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
Copyright 2024 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import fetchMock from "fetch-mock-jest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";

import { CRYPTO_BACKENDS, getSyncResponse, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import { createClient, MatrixClient } from "../../../src";
import * as testData from "../../test-utils/test-data";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { SyncResponder } from "../../test-utils/SyncResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";

afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
// eslint-disable-next-line no-global-assign
indexedDB = new IDBFactory();
});

/**
* Integration tests for to-device messages functionality.
*
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
* to provide the most effective integration tests possible.
*/
describe.each(Object.entries(CRYPTO_BACKENDS))("to-device-messages (%s)", (backend: string, initCrypto: InitCrypto) => {
let aliceClient: MatrixClient;

/** an object which intercepts `/keys/query` requests on the test homeserver */
let e2eKeyResponder: E2EKeyResponder;

beforeEach(
async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;

const homeserverUrl = "https://server.com";
aliceClient = createClient({
baseUrl: homeserverUrl,
userId: testData.TEST_USER_ID,
accessToken: "akjgkrgjsalice",
deviceId: testData.TEST_DEVICE_ID,
});

e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
new E2EKeyReceiver(homeserverUrl);
const syncResponder = new SyncResponder(homeserverUrl);

// add bob as known user
syncResponder.sendOrQueueSyncResponse(getSyncResponse([testData.BOB_TEST_USER_ID]));

// Silence warnings from the backup manager
fetchMock.getOnce(new URL("/_matrix/client/v3/room_keys/version", homeserverUrl).toString(), {
status: 404,
body: { errcode: "M_NOT_FOUND" },
});

fetchMock.get(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {});
fetchMock.get(new URL("/_matrix/client/versions/", homeserverUrl).toString(), {});
fetchMock.post(
new URL(
`/_matrix/client/v3/user/${encodeURIComponent(testData.TEST_USER_ID)}/filter`,
homeserverUrl,
).toString(),
{ filter_id: "fid" },
);

await initCrypto(aliceClient);
},
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
10000,
);

afterEach(async () => {
aliceClient.stopClient();
fetchMock.mockReset();
});

describe("encryptToDeviceMessages", () => {
it("returns empty batch for device that is not known", async () => {
await aliceClient.startClient();

const toDeviceBatch = await aliceClient
.getCrypto()
?.encryptToDeviceMessages(
"m.test.event",
[{ userId: testData.BOB_TEST_USER_ID, deviceId: testData.BOB_TEST_DEVICE_ID }],
{
some: "content",
},
);

expect(toDeviceBatch).toBeDefined();
const { batch, eventType } = toDeviceBatch!;
expect(eventType).toBe("m.room.encrypted");
expect(batch.length).toBe(0);
});

it("returns encrypted batch for known device", async () => {
await aliceClient.startClient();
e2eKeyResponder.addDeviceKeys(testData.BOB_SIGNED_TEST_DEVICE_DATA);
fetchMock.post("express:/_matrix/client/v3/keys/claim", () => ({
one_time_keys: testData.BOB_ONE_TIME_KEYS,
}));
await syncPromise(aliceClient);

const toDeviceBatch = await aliceClient
.getCrypto()
?.encryptToDeviceMessages(
"m.test.event",
[{ userId: testData.BOB_TEST_USER_ID, deviceId: testData.BOB_TEST_DEVICE_ID }],
{
some: "content",
},
);

expect(toDeviceBatch?.batch.length).toBe(1);
expect(toDeviceBatch?.eventType).toBe("m.room.encrypted");
const { deviceId, payload, userId } = toDeviceBatch!.batch[0];
expect(deviceId).toBe(testData.BOB_TEST_DEVICE_ID);
expect(userId).toBe(testData.BOB_TEST_USER_ID);
expect(payload.algorithm).toBe("m.olm.v1.curve25519-aes-sha2");
expect(payload.sender_key).toEqual(expect.any(String));
expect(payload.ciphertext).toEqual(
expect.objectContaining({
[testData.BOB_SIGNED_TEST_DEVICE_DATA.keys[`curve25519:${testData.BOB_TEST_DEVICE_ID}`]]: {
body: expect.any(String),
type: 0,
},
}),
);

// for future: check that bob's device can decrypt the ciphertext?
});
});
});
112 changes: 112 additions & 0 deletions spec/unit/crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
import { EventDecryptionResult } from "../../src/common-crypto/CryptoBackend";
import * as testData from "../test-utils/test-data";
import { KnownMembership } from "../../src/@types/membership";
import type { DeviceInfoMap } from "../../src/crypto/DeviceList";

const Olm = global.Olm;

Expand Down Expand Up @@ -1245,6 +1246,117 @@ describe("Crypto", function () {
});
});

describe("encryptToDeviceMessages", () => {
let client: TestClient;
let ensureOlmSessionsForDevices: jest.SpiedFunction<typeof olmlib.ensureOlmSessionsForDevices>;
let encryptMessageForDevice: jest.SpiedFunction<typeof olmlib.encryptMessageForDevice>;
const payload = { hello: "world" };
let encryptedPayload: object;
let crypto: Crypto;

beforeEach(async () => {
ensureOlmSessionsForDevices = jest.spyOn(olmlib, "ensureOlmSessionsForDevices");
ensureOlmSessionsForDevices.mockResolvedValue(new Map());
encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice");
encryptMessageForDevice.mockImplementation(async (...[result, , , , , , payload]) => {
result.plaintext = { type: 0, body: JSON.stringify(payload) };
});

client = new TestClient("@alice:example.org", "aliceweb");

// running initCrypto should trigger a key upload
client.httpBackend.when("POST", "/keys/upload").respond(200, {});
await Promise.all([client.client.initCrypto(), client.httpBackend.flush("/keys/upload", 1)]);

encryptedPayload = {
algorithm: "m.olm.v1.curve25519-aes-sha2",
sender_key: client.client.crypto!.olmDevice.deviceCurve25519Key,
ciphertext: { plaintext: { type: 0, body: JSON.stringify(payload) } },
};

crypto = client.client.getCrypto() as Crypto;
});

afterEach(async () => {
ensureOlmSessionsForDevices.mockRestore();
encryptMessageForDevice.mockRestore();
await client.stop();
});

it("returns encrypted batch where devices known", async () => {
const deviceInfoMap: DeviceInfoMap = new Map([
[
"@bob:example.org",
new Map([
["bobweb", new DeviceInfo("bobweb")],
["bobmobile", new DeviceInfo("bobmobile")],
]),
],
["@carol:example.org", new Map([["caroldesktop", new DeviceInfo("caroldesktop")]])],
]);
jest.spyOn(crypto.deviceList, "downloadKeys").mockResolvedValue(deviceInfoMap);
// const deviceInfoMap = await this.downloadKeys(Array.from(userIds), false);

const batch = await client.client.getCrypto()?.encryptToDeviceMessages(
"m.test.type",
[
{ userId: "@bob:example.org", deviceId: "bobweb" },
{ userId: "@bob:example.org", deviceId: "bobmobile" },
{ userId: "@carol:example.org", deviceId: "caroldesktop" },
{ userId: "@carol:example.org", deviceId: "carolmobile" }, // not known
],
payload,
);
expect(crypto.deviceList.downloadKeys).toHaveBeenCalledWith(
["@bob:example.org", "@carol:example.org"],
false,
);
expect(encryptMessageForDevice).toHaveBeenCalledTimes(3);
const expectedPayload = expect.objectContaining({
...encryptedPayload,
"org.matrix.msgid": expect.any(String),
"sender_key": expect.any(String),
});
expect(batch?.eventType).toEqual("m.room.encrypted");
expect(batch?.batch.length).toEqual(3);
expect(batch).toEqual({
eventType: "m.room.encrypted",
batch: expect.arrayContaining([
{
userId: "@bob:example.org",
deviceId: "bobweb",
payload: expectedPayload,
},
{
userId: "@bob:example.org",
deviceId: "bobmobile",
payload: expectedPayload,
},
{
userId: "@carol:example.org",
deviceId: "caroldesktop",
payload: expectedPayload,
},
]),
});
});

it("returns empty batch if no devices known", async () => {
jest.spyOn(crypto.deviceList, "downloadKeys").mockResolvedValue(new Map());
const batch = await crypto.encryptToDeviceMessages(
"m.test.type",
[
{ deviceId: "AAA", userId: "@user1:domain" },
{ deviceId: "BBB", userId: "@user1:domain" },
{ deviceId: "CCC", userId: "@user2:domain" },
],
payload,
);
expect(batch?.eventType).toEqual("m.room.encrypted");
expect(batch?.batch).toEqual([]);
});
});

describe("checkSecretStoragePrivateKey", () => {
let client: TestClient;

Expand Down
Loading
Loading