You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-31 15:24:23 +03:00
ElementR: Update CryptoApi.userHasCrossSigningKeys
(#3646)
* WIP `CryptoApi.getStoredCrossSigningForUser` * Fix QRCode * Add docs and rename * Add tests for `RustCrossSigningInfo.ts` * Do `/keys/query` instead of using `UserIdentity` * Review changes * Get rid of `CrossSigningInfo` * Merge `hasCrossSigningKeysForUser` into `userHasCrossSigningKeys` * Apply suggestions from code review * More review comments --------- Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Co-authored-by: Richard van der Hoff <richard@matrix.org>
This commit is contained in:
@ -25,7 +25,16 @@ import Olm from "@matrix-org/olm";
|
|||||||
import type { IDeviceKeys } from "../../../src/@types/crypto";
|
import type { IDeviceKeys } from "../../../src/@types/crypto";
|
||||||
import * as testUtils from "../../test-utils/test-utils";
|
import * as testUtils from "../../test-utils/test-utils";
|
||||||
import { CRYPTO_BACKENDS, getSyncResponse, InitCrypto, syncPromise } from "../../test-utils/test-utils";
|
import { CRYPTO_BACKENDS, getSyncResponse, InitCrypto, syncPromise } from "../../test-utils/test-utils";
|
||||||
import { TEST_ROOM_ID, TEST_ROOM_ID as ROOM_ID, TEST_USER_ID } from "../../test-utils/test-data";
|
import {
|
||||||
|
BOB_SIGNED_CROSS_SIGNING_KEYS_DATA,
|
||||||
|
BOB_SIGNED_TEST_DEVICE_DATA,
|
||||||
|
BOB_TEST_USER_ID,
|
||||||
|
SIGNED_CROSS_SIGNING_KEYS_DATA,
|
||||||
|
SIGNED_TEST_DEVICE_DATA,
|
||||||
|
TEST_ROOM_ID,
|
||||||
|
TEST_ROOM_ID as ROOM_ID,
|
||||||
|
TEST_USER_ID,
|
||||||
|
} from "../../test-utils/test-data";
|
||||||
import { TestClient } from "../../TestClient";
|
import { TestClient } from "../../TestClient";
|
||||||
import { logger } from "../../../src/logger";
|
import { logger } from "../../../src/logger";
|
||||||
import {
|
import {
|
||||||
@ -36,6 +45,7 @@ import {
|
|||||||
IDownloadKeyResult,
|
IDownloadKeyResult,
|
||||||
IEvent,
|
IEvent,
|
||||||
IndexedDBCryptoStore,
|
IndexedDBCryptoStore,
|
||||||
|
IRoomEvent,
|
||||||
IStartClientOpts,
|
IStartClientOpts,
|
||||||
MatrixClient,
|
MatrixClient,
|
||||||
MatrixEvent,
|
MatrixEvent,
|
||||||
@ -44,7 +54,6 @@ import {
|
|||||||
Room,
|
Room,
|
||||||
RoomMember,
|
RoomMember,
|
||||||
RoomStateEvent,
|
RoomStateEvent,
|
||||||
IRoomEvent,
|
|
||||||
} from "../../../src/matrix";
|
} from "../../../src/matrix";
|
||||||
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||||
import { E2EKeyReceiver, IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
import { E2EKeyReceiver, IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||||
@ -2566,4 +2575,46 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Check if the cross signing keys are available for a user", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// anything that we don't have a specific matcher for silently returns a 404
|
||||||
|
fetchMock.catch(404);
|
||||||
|
|
||||||
|
keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
|
||||||
|
keyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||||
|
keyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
||||||
|
keyResponder.addKeyReceiver(BOB_TEST_USER_ID, keyReceiver);
|
||||||
|
keyResponder.addCrossSigningData(BOB_SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||||
|
keyResponder.addDeviceKeys(BOB_SIGNED_TEST_DEVICE_DATA);
|
||||||
|
|
||||||
|
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||||
|
await startClientAndAwaitFirstSync();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Cross signing keys are available for an untracked user with cross signing keys on the homeserver", async () => {
|
||||||
|
// Needed for old crypto, download and cache locally the cross signing keys of Bob
|
||||||
|
await aliceClient.getCrypto()?.getUserDeviceInfo([BOB_TEST_USER_ID], true);
|
||||||
|
|
||||||
|
const hasCrossSigningKeysForUser = await aliceClient
|
||||||
|
.getCrypto()!
|
||||||
|
.userHasCrossSigningKeys(BOB_TEST_USER_ID, true);
|
||||||
|
expect(hasCrossSigningKeysForUser).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Cross signing keys are available for a tracked user", async () => {
|
||||||
|
// Process Alice keys, old crypto has a sleep(5ms) during the process
|
||||||
|
await jest.advanceTimersByTimeAsync(5);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
// Alice is the local user and should be tracked !
|
||||||
|
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(TEST_USER_ID);
|
||||||
|
expect(hasCrossSigningKeysForUser).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Cross signing keys are not available for an unknown user", async () => {
|
||||||
|
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys("@unknown:xyz");
|
||||||
|
expect(hasCrossSigningKeysForUser).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -460,6 +460,23 @@ describe("RustCrypto", () => {
|
|||||||
|
|
||||||
await expect(rustCrypto.userHasCrossSigningKeys()).resolves.toBe(true);
|
await expect(rustCrypto.userHasCrossSigningKeys()).resolves.toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns true if the user is untracked, downloadUncached is set at true and the cross-signing keys are available", async () => {
|
||||||
|
fetchMock.post("path:/_matrix/client/v3/keys/query", {
|
||||||
|
device_keys: {
|
||||||
|
[testData.BOB_TEST_USER_ID]: {
|
||||||
|
[testData.BOB_TEST_DEVICE_ID]: testData.BOB_SIGNED_TEST_DEVICE_DATA,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...testData.BOB_SIGNED_CROSS_SIGNING_KEYS_DATA,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(rustCrypto.userHasCrossSigningKeys(testData.BOB_TEST_USER_ID, true)).resolves.toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false if the user is unknown", async () => {
|
||||||
|
await expect(rustCrypto.userHasCrossSigningKeys(testData.BOB_TEST_USER_ID)).resolves.toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("createRecoveryKeyFromPassphrase", () => {
|
describe("createRecoveryKeyFromPassphrase", () => {
|
||||||
|
@ -2628,6 +2628,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
* @param userId - the user ID to get the cross-signing info for.
|
* @param userId - the user ID to get the cross-signing info for.
|
||||||
*
|
*
|
||||||
* @returns the cross signing information for the user.
|
* @returns the cross signing information for the user.
|
||||||
|
* @deprecated Prefer {@link CryptoApi#userHasCrossSigningKeys}
|
||||||
*/
|
*/
|
||||||
public getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null {
|
public getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null {
|
||||||
if (!this.cryptoBackend) {
|
if (!this.cryptoBackend) {
|
||||||
|
@ -87,6 +87,7 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
|
|||||||
* @param userId - the user ID to get the cross-signing info for.
|
* @param userId - the user ID to get the cross-signing info for.
|
||||||
*
|
*
|
||||||
* @returns the cross signing information for the user.
|
* @returns the cross signing information for the user.
|
||||||
|
* @deprecated Prefer {@link CryptoApi#userHasCrossSigningKeys}
|
||||||
*/
|
*/
|
||||||
getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null;
|
getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null;
|
||||||
|
|
||||||
|
@ -38,16 +38,6 @@ export interface CryptoApi {
|
|||||||
*/
|
*/
|
||||||
globalBlacklistUnverifiedDevices: boolean;
|
globalBlacklistUnverifiedDevices: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the user has previously published cross-signing keys
|
|
||||||
*
|
|
||||||
* This means downloading the devicelist for the user and checking if the list includes
|
|
||||||
* the cross-signing pseudo-device.
|
|
||||||
*
|
|
||||||
* @returns true if the user has previously published cross-signing keys
|
|
||||||
*/
|
|
||||||
userHasCrossSigningKeys(): Promise<boolean>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform any background tasks that can be done before a message is ready to
|
* Perform any background tasks that can be done before a message is ready to
|
||||||
* send, in order to speed up sending of the message.
|
* send, in order to speed up sending of the message.
|
||||||
@ -88,6 +78,22 @@ export interface CryptoApi {
|
|||||||
*/
|
*/
|
||||||
importRoomKeys(keys: IMegolmSessionData[], opts?: ImportRoomKeysOpts): Promise<void>;
|
importRoomKeys(keys: IMegolmSessionData[], opts?: ImportRoomKeysOpts): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given user has published cross-signing keys.
|
||||||
|
*
|
||||||
|
* - If the user is tracked, a `/keys/query` request is made to update locally the cross signing keys.
|
||||||
|
* - If the user is not tracked locally and downloadUncached is set to true,
|
||||||
|
* a `/keys/query` request is made to the server to retrieve the cross signing keys.
|
||||||
|
* - Otherwise, return false
|
||||||
|
*
|
||||||
|
* @param userId - the user ID to check. Defaults to the local user.
|
||||||
|
* @param downloadUncached - If true, download the device list for users whose device list we are not
|
||||||
|
* currently tracking. Defaults to false, in which case `false` will be returned for such users.
|
||||||
|
*
|
||||||
|
* @returns true if the cross signing keys are available.
|
||||||
|
*/
|
||||||
|
userHasCrossSigningKeys(userId?: string, downloadUncached?: boolean): Promise<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the device information for the given list of users.
|
* Get the device information for the given list of users.
|
||||||
*
|
*
|
||||||
|
@ -700,9 +700,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
*
|
*
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
public async userHasCrossSigningKeys(): Promise<boolean> {
|
public async userHasCrossSigningKeys(userId = this.userId): Promise<boolean> {
|
||||||
await this.downloadKeys([this.userId]);
|
await this.downloadKeys([userId]);
|
||||||
return this.deviceList.getStoredCrossSigningForUser(this.userId) !== null;
|
return this.deviceList.getStoredCrossSigningForUser(userId) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -244,20 +244,6 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
|||||||
|
|
||||||
public globalBlacklistUnverifiedDevices = false;
|
public globalBlacklistUnverifiedDevices = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of {@link CryptoApi.userHasCrossSigningKeys}.
|
|
||||||
*/
|
|
||||||
public async userHasCrossSigningKeys(): Promise<boolean> {
|
|
||||||
const userId = new RustSdkCryptoJs.UserId(this.userId);
|
|
||||||
/* make sure we have an *up-to-date* idea of the user's cross-signing keys. This is important, because if we
|
|
||||||
* return "false" here, we will end up generating new cross-signing keys and replacing the existing ones.
|
|
||||||
*/
|
|
||||||
const request = this.olmMachine.queryKeysForUsers([userId]);
|
|
||||||
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
|
|
||||||
const userIdentity = await this.olmMachine.getIdentity(userId);
|
|
||||||
return userIdentity !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
public prepareToEncrypt(room: Room): void {
|
public prepareToEncrypt(room: Room): void {
|
||||||
const encryptor = this.roomEncryptors[room.roomId];
|
const encryptor = this.roomEncryptors[room.roomId];
|
||||||
|
|
||||||
@ -289,6 +275,47 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link CryptoApi.userHasCrossSigningKeys}.
|
||||||
|
*/
|
||||||
|
public async userHasCrossSigningKeys(userId = this.userId, downloadUncached = false): Promise<boolean> {
|
||||||
|
// TODO: could probably do with a more efficient way of doing this than returning the whole set and searching
|
||||||
|
const rustTrackedUsers: Set<RustSdkCryptoJs.UserId> = await this.olmMachine.trackedUsers();
|
||||||
|
let rustTrackedUser: RustSdkCryptoJs.UserId | undefined;
|
||||||
|
for (const u of rustTrackedUsers) {
|
||||||
|
if (userId === u.toString()) {
|
||||||
|
rustTrackedUser = u;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rustTrackedUser !== undefined) {
|
||||||
|
if (userId === this.userId) {
|
||||||
|
/* make sure we have an *up-to-date* idea of the user's cross-signing keys. This is important, because if we
|
||||||
|
* return "false" here, we will end up generating new cross-signing keys and replacing the existing ones.
|
||||||
|
*/
|
||||||
|
const request = this.olmMachine.queryKeysForUsers([rustTrackedUser]);
|
||||||
|
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
|
||||||
|
}
|
||||||
|
const userIdentity = await this.olmMachine.getIdentity(rustTrackedUser);
|
||||||
|
return userIdentity !== undefined;
|
||||||
|
} else if (downloadUncached) {
|
||||||
|
// Download the cross signing keys and check if the master key is available
|
||||||
|
const keyResult = await this.downloadDeviceList(new Set([userId]));
|
||||||
|
const keys = keyResult.master_keys?.[userId];
|
||||||
|
|
||||||
|
// No master key
|
||||||
|
if (!keys) return false;
|
||||||
|
|
||||||
|
// `keys` is an object with { [`ed25519:${pubKey}`]: pubKey }
|
||||||
|
// We assume only a single key, and we want the bare form without type
|
||||||
|
// prefix, so we select the values.
|
||||||
|
return Boolean(Object.values(keys.keys)[0]);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the device information for the given list of users.
|
* Get the device information for the given list of users.
|
||||||
*
|
*
|
||||||
|
Reference in New Issue
Block a user