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

Element-R: wire up device lists #3272

Merged
merged 24 commits into from
Apr 21, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
58285ab
Add `getUserDeviceInfo` to `CryptoBackend` and old crypto impl
florianduros Apr 12, 2023
1c8a1cc
Add `getUserDeviceInfo` WIP impl to `rust-crypto`
florianduros Apr 13, 2023
d9e7d18
Merge branch 'develop' into florianduros/feat/wire-up-device-list
florianduros Apr 13, 2023
6b291b5
Add tests for `downloadUncached`
florianduros Apr 14, 2023
5a03708
WIP test
florianduros Apr 18, 2023
fe4cc2e
Fix typo and use `downloadDeviceToJsDevice`
florianduros Apr 18, 2023
adf8edb
Add `getUserDeviceInfo` to `client.ts`
florianduros Apr 19, 2023
fe8ff0f
Merge branch 'develop' into florianduros/feat/wire-up-device-list
florianduros Apr 19, 2023
cff1bee
Use new `Device` class instead of `IDevice`
florianduros Apr 19, 2023
c40b4d3
Add tests for `device-convertor`
florianduros Apr 19, 2023
0039589
Add method description for `isInRustUserIds` in `rust-crypto.ts`
florianduros Apr 19, 2023
edec5ae
Misc
florianduros Apr 19, 2023
d0c8776
Fix typo
florianduros Apr 19, 2023
9464d98
Merge branch 'develop' into florianduros/feat/wire-up-device-list
florianduros Apr 20, 2023
2fa63c3
Merge branch 'develop' into florianduros/feat/wire-up-device-list
florianduros Apr 21, 2023
f573511
Fix `rustDeviceToJsDevice`
florianduros Apr 21, 2023
d8a78c7
Fix comments and new one
florianduros Apr 21, 2023
74fae45
Review of `device.ts`
florianduros Apr 21, 2023
5c8176c
Remove `getUserDeviceInfo` from `client.ts`
florianduros Apr 21, 2023
420030e
Review of `getUserDeviceInfo` in `rust-crypto.ts`
florianduros Apr 21, 2023
f78b5f7
Fix typo in `index.ts`
florianduros Apr 21, 2023
0a1c376
Review `device-converter.ts`
florianduros Apr 21, 2023
ea3db72
Add documentation to `getUserDeviceInfo` in `crypto-api.ts`
florianduros Apr 21, 2023
ccabfeb
Last changes in comments
florianduros Apr 21, 2023
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
176 changes: 176 additions & 0 deletions spec/integ/crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import { DeviceInfo } from "../../src/crypto/deviceinfo";
import { E2EKeyReceiver, IE2EKeyReceiver } from "../test-utils/E2EKeyReceiver";
import { ISyncResponder, SyncResponder } from "../test-utils/SyncResponder";
import { escapeRegExp } from "../../src/utils";
import { downloadDeviceToJsDevice } from "../../src/rust-crypto/device-converter";
import { flushPromises } from "../test-utils/flushPromises";

const ROOM_ID = "!room:id";

Expand Down Expand Up @@ -1997,4 +1999,178 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
expect(res.fallbackKeysCount).toBeGreaterThan(0);
});
});

describe("getUserDeviceInfo", () => {
afterEach(() => {
jest.useRealTimers();
});

// From https://spec.matrix.org/v1.6/client-server-api/#post_matrixclientv3keysquery
// Using extracted response from matrix.org, it needs to have real keys etc to pass old crypto verification
const queryResponseBody = {
device_keys: {
"@testing_florian1:matrix.org": {
EBMMPAFOPU: {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "EBMMPAFOPU",
keys: {
"curve25519:EBMMPAFOPU": "HyhQD4mXwNViqns0noABW9NxHbCAOkriQ4QKGGndk3w",
"ed25519:EBMMPAFOPU": "xSQaxrFOTXH+7Zjo+iwb445hlNPFjnx1O3KaV3Am55k",
},
signatures: {
"@testing_florian1:matrix.org": {
"ed25519:EBMMPAFOPU":
"XFJVq9HmO5lfJN7l6muaUt887aUHg0/poR3p9XHGXBrLUqzfG7Qllq7jjtUjtcTc5CMD7/mpsXfuC2eV+X1uAw",
},
},
user_id: "@testing_florian1:matrix.org",
unsigned: {
device_display_name: "display name",
},
},
},
},
failures: {},
master_keys: {
"@testing_florian1:matrix.org": {
user_id: "@testing_florian1:matrix.org",
usage: ["master"],
keys: {
"ed25519:O5s5RoLaz93Bjf/pg55oJeCVeYYoruQhqEd0Mda6lq0":
"O5s5RoLaz93Bjf/pg55oJeCVeYYoruQhqEd0Mda6lq0",
},
signatures: {
"@testing_florian1:matrix.org": {
"ed25519:UKAQMJSJZC":
"q4GuzzuhZfTpwrlqnJ9+AEUtEfEQ0um1PO3puwp/+vidzFicw0xEPjedpJoASYQIJ8XJAAWX8Q235EKeCzEXCA",
},
},
},
},
self_signing_keys: {
"@testing_florian1:matrix.org": {
user_id: "@testing_florian1:matrix.org",
usage: ["self_signing"],
keys: {
"ed25519:YYWIHBCuKGEy9CXiVrfBVR0N1I60JtiJTNCWjiLAFzo":
"YYWIHBCuKGEy9CXiVrfBVR0N1I60JtiJTNCWjiLAFzo",
},
signatures: {
"@testing_florian1:matrix.org": {
"ed25519:O5s5RoLaz93Bjf/pg55oJeCVeYYoruQhqEd0Mda6lq0":
"yckmxgQ3JA5bb205/RunJipnpZ37ycGNf4OFzDwAad++chd71aGHqAMQ1f6D2GVfl8XdHmiRaohZf4mGnDL0AA",
},
},
},
},
user_signing_keys: {
"@testing_florian1:matrix.org": {
user_id: "@testing_florian1:matrix.org",
usage: ["user_signing"],
keys: {
"ed25519:Maa77okgZxnABGqaiChEUnV4rVsAI61WXWeL5TSEUhs":
"Maa77okgZxnABGqaiChEUnV4rVsAI61WXWeL5TSEUhs",
},
signatures: {
"@testing_florian1:matrix.org": {
"ed25519:O5s5RoLaz93Bjf/pg55oJeCVeYYoruQhqEd0Mda6lq0":
"WxNNXb13yCrBwXUQzdDWDvWSQ/qWCfwpvssOudlAgbtMzRESMbCTDkeA8sS1awaAtUmu7FrPtDb5LYfK/EE2CQ",
},
},
},
},
};

function awaitKeyQueryRequest(): Promise<Record<string, []>> {
return new Promise((resolve) => {
const listener = (url: string, options: RequestInit) => {
const content = JSON.parse(options.body as string);
// Resolve with request payload
resolve(content.device_keys);

// Return response of `/keys/query`
return queryResponseBody;
};

for (const path of ["/_matrix/client/r0/keys/query", "/_matrix/client/v3/keys/query"]) {
fetchMock.post(new URL(path, aliceClient.getHomeserverUrl()).toString(), listener);
}
});
}

it("Download uncached keys for known user", async () => {
const queryPromise = awaitKeyQueryRequest();

const user = "@testing_florian1:matrix.org";
const devicesInfo = await aliceClient.getCrypto()!.getUserDeviceInfo([user], true);

// Wait for `/keys/query` to be called
const deviceKeysPayload = await queryPromise;

expect(deviceKeysPayload).toStrictEqual({ [user]: [] });
expect(devicesInfo.get(user)?.size).toBe(1);

// Convert the expected device to IDevice and check
expect(devicesInfo.get(user)?.get("EBMMPAFOPU")).toStrictEqual(
downloadDeviceToJsDevice(queryResponseBody.device_keys[user]?.EBMMPAFOPU),
);
});

it("Download uncached keys for unknown user", async () => {
const queryPromise = awaitKeyQueryRequest();

const user = "@bob:xyz";
const devicesInfo = await aliceClient.getCrypto()!.getUserDeviceInfo([user], true);

// Wait for `/keys/query` to be called
const deviceKeysPayload = await queryPromise;

expect(deviceKeysPayload).toStrictEqual({ [user]: [] });
// The old crypto has an empty map for `@bob:xyz`
// The new crypto does not have the `@bob:xyz` entry in `devicesInfo`
expect(devicesInfo.get(user)?.size).toBeFalsy();
});

it("Get devices from tacked users", async () => {
jest.useFakeTimers();

expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const queryPromise = awaitKeyQueryRequest();

const user = "@testing_florian1:matrix.org";
// `user` will be added to the room
syncResponder.sendOrQueueSyncResponse(getSyncResponse([user, "@bob:xyz"]));

// Advance local date to 2 minutes
// The old crypto only runs the upload every 60 seconds
jest.setSystemTime(Date.now() + 2 * 60 * 1000);

await syncPromise(aliceClient);

// Old crypto: for alice: run over the `sleep(5)` in `doQueuedQueries` of `DeviceList`
jest.runAllTimers();
// Old crypto: for alice: run the `processQueryResponseForUser` in `doQueuedQueries` of `DeviceList`
await flushPromises();

// Wait for alice to query `user` keys
await queryPromise;

// Old crypto: for `user`: run over the `sleep(5)` in `doQueuedQueries` of `DeviceList`
jest.runAllTimers();
// Old crypto: for `user`: run the `processQueryResponseForUser` in `doQueuedQueries` of `DeviceList`
// It will add `@testing_florian1:matrix.org` devices to the DeviceList
await flushPromises();

const devicesInfo = await aliceClient.getCrypto()!.getUserDeviceInfo([user]);

// We should only have the `user` in it
expect(devicesInfo.size).toBe(1);
// We are expecting only the EBMMPAFOPU device
expect(devicesInfo.get(user)!.size).toBe(1);
expect(devicesInfo.get(user)!.get("EBMMPAFOPU")).toEqual(
downloadDeviceToJsDevice(queryResponseBody.device_keys[user]["EBMMPAFOPU"]),
);
});
});
});
58 changes: 58 additions & 0 deletions spec/unit/crypto/device-converter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
Copyright 2023 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 { DeviceInfo } from "../../../src/crypto/deviceinfo";
import { DeviceVerification } from "../../../src";
import { deviceInfoToDevice } from "../../../src/crypto/device-converter";

describe("device-converter", () => {
const userId = "@alice:example.com";
const deviceId = "xcvf";

// All parameters for DeviceInfo initialization
const keys = {
[`ed25519:${deviceId}`]: "key1",
[`curve25519:${deviceId}`]: "key2",
};
const algorithms = ["algo1", "algo2"];
const verified = DeviceVerification.Verified;
const signatures = { [userId]: { [deviceId]: "sign1" } };
const displayName = "display name";
const unsigned = {
device_display_name: displayName,
};

describe("deviceInfoToDevice", () => {
it("should convert a DeviceInfo to a Device", () => {
const deviceInfo = DeviceInfo.fromStorage({ keys, algorithms, verified, signatures, unsigned }, deviceId);
const device = deviceInfoToDevice(deviceInfo, userId);

expect(device.deviceId).toBe(deviceId);
expect(device.userId).toBe(userId);
expect(device.verified).toBe(verified);
expect(device.getIdentityKey()).toBe(keys[`curve25519:${deviceId}`]);
expect(device.getFingerprint()).toBe(keys[`ed25519:${deviceId}`]);
expect(device.displayName).toBe(displayName);
});

it("should add empty signatures", () => {
const deviceInfo = DeviceInfo.fromStorage({ keys, algorithms, verified }, deviceId);
const device = deviceInfoToDevice(deviceInfo, userId);

expect(device.signatures.size).toBe(0);
});
});
});
68 changes: 68 additions & 0 deletions spec/unit/rust-crypto/device-converter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
Copyright 2023 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 { DeviceKeys, DeviceVerification } from "../../../src";
import { downloadDeviceToJsDevice } from "../../../src/rust-crypto/device-converter";

describe("device-converter", () => {
const userId = "@alice:example.com";
const deviceId = "xcvf";

// All parameters for QueryDevice initialization
const keys = {
[`ed25519:${deviceId}`]: "key1",
[`curve25519:${deviceId}`]: "key2",
};
const algorithms = ["algo1", "algo2"];
const signatures = { [userId]: { [deviceId]: "sign1" } };
const displayName = "display name";
const unsigned = {
device_display_name: displayName,
};

describe("downloadDeviceToJsDevice", () => {
it("should convert a QueryDevice to a Device", () => {
const queryDevice: DeviceKeys[keyof DeviceKeys] = {
keys,
algorithms,
device_id: deviceId,
user_id: userId,
signatures,
unsigned,
};
const device = downloadDeviceToJsDevice(queryDevice);

expect(device.deviceId).toBe(deviceId);
expect(device.userId).toBe(userId);
expect(device.verified).toBe(DeviceVerification.Unverified);
expect(device.getIdentityKey()).toBe(keys[`curve25519:${deviceId}`]);
expect(device.getFingerprint()).toBe(keys[`ed25519:${deviceId}`]);
expect(device.displayName).toBe(displayName);
});

it("should add empty signatures", () => {
const queryDevice: DeviceKeys[keyof DeviceKeys] = {
keys,
algorithms,
device_id: deviceId,
user_id: userId,
};
const device = downloadDeviceToJsDevice(queryDevice);

expect(device.signatures.size).toBe(0);
});
});
});
16 changes: 16 additions & 0 deletions src/crypto-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ limitations under the License.

import type { IMegolmSessionData } from "./@types/crypto";
import { Room } from "./models/room";
import { DeviceMap } from "./models/device";

/**
* Public interface to the cryptography parts of the js-sdk
Expand Down Expand Up @@ -73,6 +74,21 @@ export interface CryptoApi {
*/
exportRoomKeys(): Promise<IMegolmSessionData[]>;

/**
* Get the device information for the given list of users.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*
* First of all, the devices are retrieved from the tracked users in the local client
* If the users are not tracked and the `downloadUncached` parameter is set at `true`,
* A `/keys/query` request is made to the local user homeserver to retrieve these devices
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*
* @param userIds - The users to fetch.
* @param downloadUncached - If true, download the device list for users whose device list we are not
* currently tracking. Defaults to false, in which case such users will not appear at all in the result map.
*
* @returns A map `{@link DeviceMap}`.
*/
getUserDeviceInfo(userIds: string[], downloadUncached?: boolean): Promise<DeviceMap>;

/**
* Set whether to trust other user's signatures of their devices.
*
Expand Down
45 changes: 45 additions & 0 deletions src/crypto/device-converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
Copyright 2023 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 { Device } from "../models/device";
import { DeviceInfo } from "./deviceinfo";

/**
* Convert a {@link DeviceInfo} to a {@link Device}.
* @param deviceInfo - deviceInfo to convert
* @param userId - id of the hat owns the device.
florianduros marked this conversation as resolved.
Show resolved Hide resolved
*/
export function deviceInfoToDevice(deviceInfo: DeviceInfo, userId: string): Device {
const keys = new Map<string, string>(Object.entries(deviceInfo.keys));
const displayName = deviceInfo.getDisplayName() || undefined;

const signatures = new Map<string, Map<string, string>>();
if (deviceInfo.signatures) {
for (const userId in deviceInfo.signatures) {
signatures.set(userId, new Map(Object.entries(deviceInfo.signatures[userId])));
}
}

return new Device({
deviceId: deviceInfo.deviceId,
userId: userId,
keys,
algorithms: deviceInfo.algorithms,
verified: deviceInfo.verified,
signatures,
displayName,
});
}
Loading