diff --git a/spec/integ/crypto/verification.spec.ts b/spec/integ/crypto/verification.spec.ts index 668641aea36..b92d5a51fc3 100644 --- a/spec/integ/crypto/verification.spec.ts +++ b/spec/integ/crypto/verification.spec.ts @@ -62,6 +62,7 @@ import { BOB_TEST_USER_ID, CURVE25519_KEY_BACKUP_DATA, MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64, + MEGOLM_SESSION_DATA, SIGNED_CROSS_SIGNING_KEYS_DATA, SIGNED_TEST_DEVICE_DATA, TEST_DEVICE_ID, @@ -81,6 +82,7 @@ import { ToDeviceEvent, } from "./olm-utils"; import { KeyBackupInfo } from "../../../src/crypto-api"; + import { encodeBase64 } from "../../../src/base64"; // The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations @@ -1259,19 +1261,41 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st const requestId = await requestPromises.get("m.megolm_backup.v1"); - await sendBackupGossipAndExpectVersion(requestId!, BACKUP_DECRYPTION_KEY_BASE64, matchingBackupInfo); + const keyBackupIsCached = new Promise((resolve) => { + aliceClient.on(CryptoEvent.KeyBackupPrivateKeyCached, () => { + resolve(); + }); + }); - // We are lacking a way to signal that the secret has been received, so we wait a bit.. - jest.useRealTimers(); - await new Promise((resolve) => { - setTimeout(resolve, 500); + const keyBackupStatusEnabled = new Promise((resolve) => { + aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => { + if (enabled) { + resolve(); + } + }); + }); + + const autoImportFinished = new Promise((resolve) => { + aliceClient.on(CryptoEvent.KeyBackupAutoImportFinished, (total) => { + if (total > 0) { + resolve(); + } + }); }); - jest.useFakeTimers(); + + await sendBackupGossipAndExpectVersion(requestId!, BACKUP_DECRYPTION_KEY_BASE64, matchingBackupInfo); + + await keyBackupStatusEnabled; + await keyBackupIsCached; // the backup secret should be cached const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey(); expect(cachedKey).toBeTruthy(); expect(encodeBase64(cachedKey!)).toEqual(BACKUP_DECRYPTION_KEY_BASE64); + + // An auto import should have been triggered + await autoImportFinished; + }); newBackendOnly("Should not accept the backup decryption key gossip if private key do not match", async () => { @@ -1281,14 +1305,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st const requestId = await requestPromises.get("m.megolm_backup.v1"); + const keyBackupStatusUpdate = new Promise((resolve) => { + aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => { + resolve(); + }); + }); + await sendBackupGossipAndExpectVersion(requestId!, BACKUP_DECRYPTION_KEY_BASE64, nonMatchingBackupInfo); + await keyBackupStatusUpdate; // We are lacking a way to signal that the secret has been received, so we wait a bit.. - jest.useRealTimers(); - await new Promise((resolve) => { - setTimeout(resolve, 500); - }); - jest.useFakeTimers(); + await jest.runAllTimersAsync(); // the backup secret should not be cached const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey(); @@ -1305,14 +1332,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st const infoCopy = Object.assign({}, matchingBackupInfo); delete infoCopy.auth_data.signatures; + const keyBackupStatusUpdate = new Promise((resolve) => { + aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => { + resolve(enabled); + }); + }); + await sendBackupGossipAndExpectVersion(requestId!, BACKUP_DECRYPTION_KEY_BASE64, infoCopy); - // We are lacking a way to signal that the secret has been received, so we wait a bit.. - jest.useRealTimers(); - await new Promise((resolve) => { - setTimeout(resolve, 500); - }); - jest.useFakeTimers(); + const backupStatus = await keyBackupStatusUpdate; + expect(backupStatus).toBe(false); + await jest.runAllTimersAsync(); // the backup secret should not be cached const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey(); @@ -1326,18 +1356,23 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st const requestId = await requestPromises.get("m.megolm_backup.v1"); + + const keyBackupStatusUpdate = new Promise((resolve) => { + aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => { + resolve(enabled); + }); + }); + + await sendBackupGossipAndExpectVersion( requestId!, BACKUP_DECRYPTION_KEY_BASE64, unknownAlgorithmBackupInfo, ); - // We are lacking a way to signal that the secret has been received, so we wait a bit.. - jest.useRealTimers(); - await new Promise((resolve) => { - setTimeout(resolve, 500); - }); - jest.useFakeTimers(); + + await keyBackupStatusUpdate; + await jest.runAllTimersAsync(); // the backup secret should not be cached const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey(); @@ -1351,14 +1386,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st const requestId = await requestPromises.get("m.megolm_backup.v1"); + const keyBackupStatusUpdate = new Promise((resolve) => { + aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => { + resolve(enabled); + }); + }); + await sendBackupGossipAndExpectVersion(requestId!, "InvalidSecret", matchingBackupInfo); - // We are lacking a way to signal that the secret has been received, so we wait a bit.. - jest.useRealTimers(); - await new Promise((resolve) => { - setTimeout(resolve, 500); - }); - jest.useFakeTimers(); + const backupStatus = await keyBackupStatusUpdate; + expect(backupStatus).toBe(true); + jest.runAllTimersAsync(); // the backup secret should not be cached const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey(); @@ -1400,7 +1438,18 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st ); }); - fetchMock.get("express:/_matrix/client/v3/room_keys/keys", CURVE25519_KEY_BACKUP_DATA); + const fullBackup = { + rooms: { + "!ROOM:ID": { + sessions: { + [MEGOLM_SESSION_DATA.session_id]: CURVE25519_KEY_BACKUP_DATA, + }, + }, + }, + }; + + fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup); + // The dummy device sends the secret returnToDeviceMessageFromSync(toDeviceEvent); @@ -1539,6 +1588,7 @@ function mockSecretRequestAndGetPromises(): Map> { const messages = JSON.parse(opts.body as string).messages[TEST_USER_ID]; // rust crypto broadcasts to all devices, old crypto to a specific device, take the first one const content = Object.values(messages)[0] as any; + if (content.action == "request") { const name = content.name; const requestId = content.request_id; diff --git a/src/client.ts b/src/client.ts index 2d151bdf527..38c01dd4448 100644 --- a/src/client.ts +++ b/src/client.ts @@ -961,6 +961,9 @@ type CryptoEvents = | CryptoEvent.KeyBackupStatus | CryptoEvent.KeyBackupFailed | CryptoEvent.KeyBackupSessionsRemaining + | CryptoEvent.KeyBackupPrivateKeyCached + | CryptoEvent.KeyBackupAutoImportStarted + | CryptoEvent.KeyBackupAutoImportFinished | CryptoEvent.RoomKeyRequest | CryptoEvent.RoomKeyRequestCancellation | CryptoEvent.VerificationRequest @@ -2269,6 +2272,7 @@ export class MatrixClient extends TypedEventEmitter { + // do not await this, as it can be long. + // XXX We need new APIs to do this properly. To avoid having several restore going on at the same time, + // there should be proper progress reporting, and a way to cancel a restore. + this.emit(CryptoEvent.KeyBackupAutoImportStarted); + this.restoreKeyBackupWithCache(undefined, undefined, info).then((result) => { + this.emit(CryptoEvent.KeyBackupAutoImportFinished, result.imported); + logger.info("Backup restored."); + }); + }); + // re-emit the events emitted by the crypto impl this.reEmitter.reEmit(rustCrypto, [ CryptoEvent.VerificationRequestReceived, @@ -2357,6 +2372,7 @@ export class MatrixClient extends TypedEventEmitter void; [CryptoEvent.UserCrossSigningUpdated]: (userId: string) => void; + [CryptoEvent.KeyBackupPrivateKeyCached]: (info: KeyBackupInfo) => void; + [CryptoEvent.KeyBackupAutoImportStarted]: () => void; + [CryptoEvent.KeyBackupAutoImportFinished]: (totalKeys: number) => void; }; export class Crypto extends TypedEventEmitter implements CryptoBackend { diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index 784e5b59995..e78e51f4ad0 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -156,6 +156,9 @@ export class RustBackupManager extends TypedEventEmitter void; [CryptoEvent.KeyBackupSessionsRemaining]: (remaining: number) => void; [CryptoEvent.KeyBackupFailed]: (errCode: string) => void; + /** + * Fired when the backup decryption key is received via secret sharing and stored in cache. + */ + [CryptoEvent.KeyBackupPrivateKeyCached]: (info: KeyBackupInfo) => void; }; diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 34ceec27208..9675a741222 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -152,6 +152,7 @@ export class RustCrypto extends TypedEventEmitter void; + + /** + * Fires when the backup decryption key is received via secret sharing. + */ + [CryptoEvent.KeyBackupPrivateKeyCached]: (info: KeyBackupInfo) => void; } & RustBackupCryptoEventMap;