You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-25 05:23:13 +03:00
Element-R: wire up device lists (#3272)
* Add `getUserDeviceInfo` to `CryptoBackend` and old crypto impl * Add `getUserDeviceInfo` WIP impl to `rust-crypto` * Add tests for `downloadUncached` * WIP test * Fix typo and use `downloadDeviceToJsDevice` * Add `getUserDeviceInfo` to `client.ts` * Use new `Device` class instead of `IDevice` * Add tests for `device-convertor` * Add method description for `isInRustUserIds` in `rust-crypto.ts` * Misc * Fix typo * Fix `rustDeviceToJsDevice` * Fix comments and new one * Review of `device.ts` * Remove `getUserDeviceInfo` from `client.ts` * Review of `getUserDeviceInfo` in `rust-crypto.ts` * Fix typo in `index.ts` * Review `device-converter.ts` * Add documentation to `getUserDeviceInfo` in `crypto-api.ts` * Last changes in comments
This commit is contained in:
@@ -48,6 +48,8 @@ import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
|||||||
import { E2EKeyReceiver, IE2EKeyReceiver } from "../test-utils/E2EKeyReceiver";
|
import { E2EKeyReceiver, IE2EKeyReceiver } from "../test-utils/E2EKeyReceiver";
|
||||||
import { ISyncResponder, SyncResponder } from "../test-utils/SyncResponder";
|
import { ISyncResponder, SyncResponder } from "../test-utils/SyncResponder";
|
||||||
import { escapeRegExp } from "../../src/utils";
|
import { escapeRegExp } from "../../src/utils";
|
||||||
|
import { downloadDeviceToJsDevice } from "../../src/rust-crypto/device-converter";
|
||||||
|
import { flushPromises } from "../test-utils/flushPromises";
|
||||||
|
|
||||||
const ROOM_ID = "!room:id";
|
const ROOM_ID = "!room:id";
|
||||||
|
|
||||||
@@ -1997,4 +1999,178 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
expect(res.fallbackKeysCount).toBeGreaterThan(0);
|
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
spec/unit/crypto/device-converter.spec.ts
Normal file
58
spec/unit/crypto/device-converter.spec.ts
Normal file
@@ -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
spec/unit/rust-crypto/device-converter.spec.ts
Normal file
68
spec/unit/rust-crypto/device-converter.spec.ts
Normal file
@@ -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,6 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import type { IMegolmSessionData } from "./@types/crypto";
|
import type { IMegolmSessionData } from "./@types/crypto";
|
||||||
import { Room } from "./models/room";
|
import { Room } from "./models/room";
|
||||||
|
import { DeviceMap } from "./models/device";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public interface to the cryptography parts of the js-sdk
|
* Public interface to the cryptography parts of the js-sdk
|
||||||
@@ -73,6 +74,23 @@ export interface CryptoApi {
|
|||||||
*/
|
*/
|
||||||
exportRoomKeys(): Promise<IMegolmSessionData[]>;
|
exportRoomKeys(): Promise<IMegolmSessionData[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the device information for the given list of users.
|
||||||
|
*
|
||||||
|
* For any users whose device lists are cached (due to sharing an encrypted room with the user), the
|
||||||
|
* cached device data is returned.
|
||||||
|
*
|
||||||
|
* If there are uncached users, and the `downloadUncached` parameter is set to `true`,
|
||||||
|
* a `/keys/query` request is made to the server to retrieve these devices.
|
||||||
|
*
|
||||||
|
* @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.
|
* Set whether to trust other user's signatures of their devices.
|
||||||
*
|
*
|
||||||
|
|||||||
45
src/crypto/device-converter.ts
Normal file
45
src/crypto/device-converter.ts
Normal file
@@ -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 user that owns the device.
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ISignatures } from "../@types/signed";
|
import { ISignatures } from "../@types/signed";
|
||||||
|
import { DeviceVerification } from "../models/device";
|
||||||
|
|
||||||
export interface IDevice {
|
export interface IDevice {
|
||||||
keys: Record<string, string>;
|
keys: Record<string, string>;
|
||||||
@@ -25,12 +26,6 @@ export interface IDevice {
|
|||||||
signatures?: ISignatures;
|
signatures?: ISignatures;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DeviceVerification {
|
|
||||||
Blocked = -1,
|
|
||||||
Unverified = 0,
|
|
||||||
Verified = 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Information about a user's device
|
* Information about a user's device
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ import {
|
|||||||
} from "../secret-storage";
|
} from "../secret-storage";
|
||||||
import { ISecretRequest } from "./SecretSharing";
|
import { ISecretRequest } from "./SecretSharing";
|
||||||
import { DeviceVerificationStatus } from "../crypto-api";
|
import { DeviceVerificationStatus } from "../crypto-api";
|
||||||
|
import { Device, DeviceMap } from "../models/device";
|
||||||
|
import { deviceInfoToDevice } from "./device-converter";
|
||||||
|
|
||||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||||
|
|
||||||
@@ -2063,6 +2065,54 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
return this.deviceList.getStoredDevicesForUser(userId);
|
return this.deviceList.getStoredDevicesForUser(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the device information for the given list of users.
|
||||||
|
*
|
||||||
|
* @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}`.
|
||||||
|
*/
|
||||||
|
public async getUserDeviceInfo(userIds: string[], downloadUncached = false): Promise<DeviceMap> {
|
||||||
|
const deviceMapByUserId = new Map<string, Map<string, Device>>();
|
||||||
|
// Keep the users without device to download theirs keys
|
||||||
|
const usersWithoutDeviceInfo: string[] = [];
|
||||||
|
|
||||||
|
for (const userId of userIds) {
|
||||||
|
const deviceInfos = await this.getStoredDevicesForUser(userId);
|
||||||
|
// If there are device infos for a userId, we transform it into a map
|
||||||
|
// Else, the keys will be downloaded after
|
||||||
|
if (deviceInfos) {
|
||||||
|
const deviceMap = new Map(
|
||||||
|
// Convert DeviceInfo to Device
|
||||||
|
deviceInfos.map((deviceInfo) => [deviceInfo.deviceId, deviceInfoToDevice(deviceInfo, userId)]),
|
||||||
|
);
|
||||||
|
deviceMapByUserId.set(userId, deviceMap);
|
||||||
|
} else {
|
||||||
|
usersWithoutDeviceInfo.push(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download device info for users without device infos
|
||||||
|
if (downloadUncached && usersWithoutDeviceInfo.length > 0) {
|
||||||
|
const newDeviceInfoMap = await this.downloadKeys(usersWithoutDeviceInfo);
|
||||||
|
|
||||||
|
newDeviceInfoMap.forEach((deviceInfoMap, userId) => {
|
||||||
|
const deviceMap = new Map<string, Device>();
|
||||||
|
// Convert DeviceInfo to Device
|
||||||
|
deviceInfoMap.forEach((deviceInfo, deviceId) =>
|
||||||
|
deviceMap.set(deviceId, deviceInfoToDevice(deviceInfo, userId)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Put the new device infos into the returned map
|
||||||
|
deviceMapByUserId.set(userId, deviceMap);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceMapByUserId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the stored keys for a single device
|
* Get the stored keys for a single device
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export * from "./models/poll";
|
|||||||
export * from "./models/room-member";
|
export * from "./models/room-member";
|
||||||
export * from "./models/room-state";
|
export * from "./models/room-state";
|
||||||
export * from "./models/user";
|
export * from "./models/user";
|
||||||
|
export * from "./models/device";
|
||||||
export * from "./scheduler";
|
export * from "./scheduler";
|
||||||
export * from "./filter";
|
export * from "./filter";
|
||||||
export * from "./timeline-window";
|
export * from "./timeline-window";
|
||||||
|
|||||||
81
src/models/device.ts
Normal file
81
src/models/device.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** State of the verification of the device. */
|
||||||
|
export enum DeviceVerification {
|
||||||
|
Blocked = -1,
|
||||||
|
Unverified = 0,
|
||||||
|
Verified = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A map from user ID to device ID to Device */
|
||||||
|
export type DeviceMap = Map<string, Map<string, Device>>;
|
||||||
|
|
||||||
|
type DeviceParameters = Pick<Device, "deviceId" | "userId" | "algorithms" | "keys"> & Partial<Device>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information on a user's device, as returned by {@link CryptoApi.getUserDeviceInfo}.
|
||||||
|
*/
|
||||||
|
export class Device {
|
||||||
|
/** id of the device */
|
||||||
|
public readonly deviceId: string;
|
||||||
|
|
||||||
|
/** id of the user that owns the device */
|
||||||
|
public readonly userId: string;
|
||||||
|
|
||||||
|
/** list of algorithms supported by this device */
|
||||||
|
public readonly algorithms: string[];
|
||||||
|
|
||||||
|
/** a map from `<key type>:<id> -> <base64-encoded key>` */
|
||||||
|
public readonly keys: Map<string, string>;
|
||||||
|
|
||||||
|
/** whether the device has been verified/blocked by the user */
|
||||||
|
public readonly verified: DeviceVerification;
|
||||||
|
|
||||||
|
/** a map `<userId, map<algorithm:device_id, signature>>` */
|
||||||
|
public readonly signatures: Map<string, Map<string, string>>;
|
||||||
|
|
||||||
|
/** display name of the device */
|
||||||
|
public readonly displayName?: string;
|
||||||
|
|
||||||
|
public constructor(opts: DeviceParameters) {
|
||||||
|
this.deviceId = opts.deviceId;
|
||||||
|
this.userId = opts.userId;
|
||||||
|
this.algorithms = opts.algorithms;
|
||||||
|
this.keys = opts.keys;
|
||||||
|
this.verified = opts.verified || DeviceVerification.Unverified;
|
||||||
|
this.signatures = opts.signatures || new Map();
|
||||||
|
this.displayName = opts.displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the fingerprint for this device (ie, the Ed25519 key)
|
||||||
|
*
|
||||||
|
* @returns base64-encoded fingerprint of this device
|
||||||
|
*/
|
||||||
|
public getFingerprint(): string | undefined {
|
||||||
|
return this.keys.get(`ed25519:${this.deviceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the identity key for this device (ie, the Curve25519 key)
|
||||||
|
*
|
||||||
|
* @returns base64-encoded identity key of this device
|
||||||
|
*/
|
||||||
|
public getIdentityKey(): string | undefined {
|
||||||
|
return this.keys.get(`curve25519:${this.deviceId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
121
src/rust-crypto/device-converter.ts
Normal file
121
src/rust-crypto/device-converter.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
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 * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||||
|
|
||||||
|
import { Device, DeviceVerification } from "../models/device";
|
||||||
|
import { DeviceKeys } from "../client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a {@link RustSdkCryptoJs.Device} to a {@link Device}
|
||||||
|
* @param device - Rust Sdk device
|
||||||
|
* @param userId - owner of the device
|
||||||
|
*/
|
||||||
|
export function rustDeviceToJsDevice(device: RustSdkCryptoJs.Device, userId: RustSdkCryptoJs.UserId): Device {
|
||||||
|
// Copy rust device keys to Device.keys
|
||||||
|
const keys = new Map<string, string>();
|
||||||
|
for (const [keyId, key] of device.keys.entries()) {
|
||||||
|
keys.set(keyId.toString(), key.toBase64());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute verified from device state
|
||||||
|
let verified: DeviceVerification = DeviceVerification.Unverified;
|
||||||
|
if (device.isBlacklisted()) {
|
||||||
|
verified = DeviceVerification.Blocked;
|
||||||
|
} else if (device.isVerified()) {
|
||||||
|
verified = DeviceVerification.Verified;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert rust signatures to Device.signatures
|
||||||
|
const signatures = new Map<string, Map<string, string>>();
|
||||||
|
const mayBeSignatureMap: Map<string, RustSdkCryptoJs.MaybeSignature> | undefined = device.signatures.get(userId);
|
||||||
|
if (mayBeSignatureMap) {
|
||||||
|
const convertedSignatures = new Map<string, string>();
|
||||||
|
// Convert maybeSignatures map to a Map<string, string>
|
||||||
|
for (const [key, value] of mayBeSignatureMap.entries()) {
|
||||||
|
if (value.isValid() && value.signature) {
|
||||||
|
convertedSignatures.set(key, value.signature.toBase64());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signatures.set(userId.toString(), convertedSignatures);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert rust algorithms to algorithms
|
||||||
|
const rustAlgorithms: RustSdkCryptoJs.EncryptionAlgorithm[] = device.algorithms;
|
||||||
|
// Use set to ensure that algorithms are not duplicated
|
||||||
|
const algorithms = new Set<string>();
|
||||||
|
rustAlgorithms.forEach((algorithm) => {
|
||||||
|
switch (algorithm) {
|
||||||
|
case RustSdkCryptoJs.EncryptionAlgorithm.MegolmV1AesSha2:
|
||||||
|
algorithms.add("m.megolm.v1.aes-sha2");
|
||||||
|
break;
|
||||||
|
case RustSdkCryptoJs.EncryptionAlgorithm.OlmV1Curve25519AesSha2:
|
||||||
|
default:
|
||||||
|
algorithms.add("m.olm.v1.curve25519-aes-sha2");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Device({
|
||||||
|
deviceId: device.deviceId.toString(),
|
||||||
|
userId: userId.toString(),
|
||||||
|
keys,
|
||||||
|
algorithms: Array.from(algorithms),
|
||||||
|
verified,
|
||||||
|
signatures,
|
||||||
|
displayName: device.displayName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert {@link DeviceKeys} from `/keys/query` request to a `Map<string, Device>`
|
||||||
|
* @param deviceKeys - Device keys object to convert
|
||||||
|
*/
|
||||||
|
export function deviceKeysToDeviceMap(deviceKeys: DeviceKeys): Map<string, Device> {
|
||||||
|
return new Map(
|
||||||
|
Object.entries(deviceKeys).map(([deviceId, device]) => [deviceId, downloadDeviceToJsDevice(device)]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Device from `/keys/query` request
|
||||||
|
type QueryDevice = DeviceKeys[keyof DeviceKeys];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert `/keys/query` {@link QueryDevice} device to {@link Device}
|
||||||
|
* @param device - Device from `/keys/query` request
|
||||||
|
*/
|
||||||
|
export function downloadDeviceToJsDevice(device: QueryDevice): Device {
|
||||||
|
const keys = new Map(Object.entries(device.keys));
|
||||||
|
const displayName = device.unsigned?.device_display_name;
|
||||||
|
|
||||||
|
const signatures = new Map<string, Map<string, string>>();
|
||||||
|
if (device.signatures) {
|
||||||
|
for (const userId in device.signatures) {
|
||||||
|
signatures.set(userId, new Map(Object.entries(device.signatures[userId])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Device({
|
||||||
|
deviceId: device.device_id,
|
||||||
|
userId: device.user_id,
|
||||||
|
keys,
|
||||||
|
algorithms: device.algorithms,
|
||||||
|
verified: DeviceVerification.Unverified,
|
||||||
|
signatures,
|
||||||
|
displayName,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -24,13 +24,16 @@ import { Room } from "../models/room";
|
|||||||
import { RoomMember } from "../models/room-member";
|
import { RoomMember } from "../models/room-member";
|
||||||
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
|
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { IHttpOpts, MatrixHttpApi } from "../http-api";
|
import { IHttpOpts, MatrixHttpApi, Method } from "../http-api";
|
||||||
import { UserTrustLevel } from "../crypto/CrossSigning";
|
import { UserTrustLevel } from "../crypto/CrossSigning";
|
||||||
import { RoomEncryptor } from "./RoomEncryptor";
|
import { RoomEncryptor } from "./RoomEncryptor";
|
||||||
import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
|
import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
|
||||||
import { KeyClaimManager } from "./KeyClaimManager";
|
import { KeyClaimManager } from "./KeyClaimManager";
|
||||||
import { MapWithDefault } from "../utils";
|
import { MapWithDefault } from "../utils";
|
||||||
import { DeviceVerificationStatus } from "../crypto-api";
|
import { DeviceVerificationStatus } from "../crypto-api";
|
||||||
|
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter";
|
||||||
|
import { IDownloadKeyResult, IQueryKeysRequest } from "../client";
|
||||||
|
import { Device, DeviceMap } from "../models/device";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
|
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
|
||||||
@@ -54,7 +57,7 @@ export class RustCrypto implements CryptoBackend {
|
|||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly olmMachine: RustSdkCryptoJs.OlmMachine,
|
private readonly olmMachine: RustSdkCryptoJs.OlmMachine,
|
||||||
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||||
_userId: string,
|
_userId: string,
|
||||||
_deviceId: string,
|
_deviceId: string,
|
||||||
) {
|
) {
|
||||||
@@ -162,6 +165,79 @@ export class RustCrypto implements CryptoBackend {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the device information for the given list of users.
|
||||||
|
*
|
||||||
|
* @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}`.
|
||||||
|
*/
|
||||||
|
public async getUserDeviceInfo(userIds: string[], downloadUncached = false): Promise<DeviceMap> {
|
||||||
|
const deviceMapByUserId = new Map<string, Map<string, Device>>();
|
||||||
|
const rustTrackedUsers: Set<RustSdkCryptoJs.UserId> = await this.olmMachine.trackedUsers();
|
||||||
|
|
||||||
|
// Convert RustSdkCryptoJs.UserId to a `Set<string>`
|
||||||
|
const trackedUsers = new Set<string>();
|
||||||
|
rustTrackedUsers.forEach((rustUserId) => trackedUsers.add(rustUserId.toString()));
|
||||||
|
|
||||||
|
// Keep untracked user to download their keys after
|
||||||
|
const untrackedUsers: Set<string> = new Set();
|
||||||
|
|
||||||
|
for (const userId of userIds) {
|
||||||
|
// if this is a tracked user, we can just fetch the device list from the rust-sdk
|
||||||
|
// (NB: this is probably ok even if we race with a leave event such that we stop tracking the user's
|
||||||
|
// devices: the rust-sdk will return the last-known device list, which will be good enough.)
|
||||||
|
if (trackedUsers.has(userId)) {
|
||||||
|
deviceMapByUserId.set(userId, await this.getUserDevices(userId));
|
||||||
|
} else {
|
||||||
|
untrackedUsers.add(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// for any users whose device lists we are not tracking, fall back to downloading the device list
|
||||||
|
// over HTTP.
|
||||||
|
if (downloadUncached && untrackedUsers.size >= 1) {
|
||||||
|
const queryResult = await this.downloadDeviceList(untrackedUsers);
|
||||||
|
Object.entries(queryResult.device_keys).forEach(([userId, deviceKeys]) =>
|
||||||
|
deviceMapByUserId.set(userId, deviceKeysToDeviceMap(deviceKeys)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceMapByUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the device list for the given user from the olm machine
|
||||||
|
* @param userId - Rust SDK UserId
|
||||||
|
*/
|
||||||
|
private async getUserDevices(userId: string): Promise<Map<string, Device>> {
|
||||||
|
const rustUserId = new RustSdkCryptoJs.UserId(userId);
|
||||||
|
const devices: RustSdkCryptoJs.UserDevices = await this.olmMachine.getUserDevices(rustUserId);
|
||||||
|
return new Map(
|
||||||
|
devices
|
||||||
|
.devices()
|
||||||
|
.map((device: RustSdkCryptoJs.Device) => [
|
||||||
|
device.deviceId.toString(),
|
||||||
|
rustDeviceToJsDevice(device, rustUserId),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the given user keys by calling `/keys/query` request
|
||||||
|
* @param untrackedUsers - download keys of these users
|
||||||
|
*/
|
||||||
|
private async downloadDeviceList(untrackedUsers: Set<string>): Promise<IDownloadKeyResult> {
|
||||||
|
const queryBody: IQueryKeysRequest = { device_keys: {} };
|
||||||
|
untrackedUsers.forEach((user) => (queryBody.device_keys[user] = []));
|
||||||
|
|
||||||
|
return await this.http.authedRequest(Method.Post, "/_matrix/client/v3/keys/query", undefined, queryBody, {
|
||||||
|
prefix: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of {@link CryptoApi#getTrustCrossSignedDevices}.
|
* Implementation of {@link CryptoApi#getTrustCrossSignedDevices}.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user