diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index 43c40123b4b..75b3decc6c7 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -94,6 +94,7 @@ describe("initRustCrypto", () => { getSecretsFromInbox: jest.fn().mockResolvedValue([]), deleteSecretsFromInbox: jest.fn(), registerReceiveSecretCallback: jest.fn(), + registerDevicesUpdatedCallback: jest.fn(), outgoingRequests: jest.fn(), isBackupEnabled: jest.fn().mockResolvedValue(false), verifyBackup: jest.fn().mockResolvedValue({ trusted: jest.fn().mockReturnValue(false) }), @@ -1132,6 +1133,33 @@ describe("RustCrypto", () => { rustCrypto.stop(); }); + it("should emit events on device changes", async () => { + jest.useFakeTimers({ doNotFake: ["queueMicrotask"] }); + + fetchMock.post("path:/_matrix/client/v3/keys/upload", { one_time_key_counts: {} }); + fetchMock.post("path:/_matrix/client/v3/keys/query", { + device_keys: { + [testData.TEST_USER_ID]: { + [testData.TEST_DEVICE_ID]: testData.SIGNED_TEST_DEVICE_DATA, + }, + }, + }); + + const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), testData.TEST_USER_ID); + const willUpdateCallback = jest.fn(); + rustCrypto.on(CryptoEvent.WillUpdateDevices, willUpdateCallback); + const devicesUpdatedCallback = jest.fn(); + rustCrypto.on(CryptoEvent.DevicesUpdated, devicesUpdatedCallback); + + rustCrypto.onSyncCompleted({}); + + // wait for the devices to be updated + await rustCrypto.getUserDeviceInfo([testData.TEST_USER_ID]); + expect(willUpdateCallback).toHaveBeenCalledWith([testData.TEST_USER_ID], false); + expect(devicesUpdatedCallback).toHaveBeenCalledWith([testData.TEST_USER_ID], false); + rustCrypto.stop(); + }); + describe("requestDeviceVerification", () => { it("throws an error if the device is unknown", async () => { const rustCrypto = await makeTestRustCrypto(); diff --git a/src/client.ts b/src/client.ts index 23bf2af75fd..b08598913cb 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2360,6 +2360,9 @@ export class MatrixClient extends TypedEventEmitter rustCrypto.onUserIdentityUpdated(userId), ); + await olmMachine.registerDevicesUpdatedCallback((userIds: string[]) => rustCrypto.onDevicesUpdated(userIds)); // Check if there are any key backup secrets pending processing. There may be multiple secrets to process if several devices have gossiped them. // The `registerReceiveSecretCallback` function will only be triggered for new secrets. If the client is restarted before processing them, the secrets will need to be manually handled. diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 27277f57c9e..30c27201f9d 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -1441,7 +1441,7 @@ export class RustCrypto extends TypedEventEmitter { + this.emit(CryptoEvent.WillUpdateDevices, userIds, false); + this.emit(CryptoEvent.DevicesUpdated, userIds, false); + } + /** * Handles secret received from the rust secret inbox. * @@ -1828,6 +1844,9 @@ function rustEncryptionInfoToJsEncryptionInfo( type RustCryptoEvents = | CryptoEvent.VerificationRequestReceived | CryptoEvent.UserTrustStatusChanged + | CryptoEvent.KeysChanged + | CryptoEvent.WillUpdateDevices + | CryptoEvent.DevicesUpdated | RustBackupCryptoEvents; type RustCryptoEventMap = { @@ -1842,4 +1861,31 @@ type RustCryptoEventMap = { [CryptoEvent.UserTrustStatusChanged]: (userId: string, userTrustLevel: UserVerificationStatus) => void; [CryptoEvent.KeyBackupDecryptionKeyCached]: (version: string) => void; + /** + * Fires when the user's cross-signing keys have changed or cross-signing + * has been enabled/disabled. The client can use getStoredCrossSigningForUser + * with the user ID of the logged in user to check if cross-signing is + * enabled on the account. If enabled, it can test whether the current key + * is trusted using with checkUserTrust with the user ID of the logged + * in user. The checkOwnCrossSigningTrust function may be used to reconcile + * the trust in the account key. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * @experimental + */ + [CryptoEvent.KeysChanged]: (data: {}) => void; + /** + * Fires whenever the stored devices for a user will be updated + * @param users - A list of user IDs that will be updated + * @param initialFetch - If true, the store is empty (apart + * from our own device) and is being seeded. + */ + [CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void; + /** + * Fires whenever the stored devices for a user have changed + * @param users - A list of user IDs that were updated + * @param initialFetch - If true, the store was empty (apart + * from our own device) and has been seeded. + */ + [CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void; } & RustBackupCryptoEventMap;