diff --git a/spec/integ/crypto/megolm-backup.spec.ts b/spec/integ/crypto/megolm-backup.spec.ts index 09d125b9e9c..decca3c2fdb 100644 --- a/spec/integ/crypto/megolm-backup.spec.ts +++ b/spec/integ/crypto/megolm-backup.spec.ts @@ -18,7 +18,7 @@ import fetchMock from "fetch-mock-jest"; import "fake-indexeddb/auto"; import { IKeyBackupSession } from "../../../src/crypto/keybackup"; -import { createClient, ICreateClientOpts, IEvent, MatrixClient } from "../../../src"; +import { createClient, CryptoEvent, ICreateClientOpts, IEvent, MatrixClient } from "../../../src"; import { SyncResponder } from "../../test-utils/SyncResponder"; import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver"; import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder"; @@ -151,13 +151,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe // start after saving the private key await aliceClient.startClient(); - // Persuade alice to fetch the device list. Completing the initial sync will make the device list download - // outdated device lists (of which our own user will be one). - syncResponder.sendOrQueueSyncResponse({}); - await jest.advanceTimersByTimeAsync(10); // DeviceList has a sleep(5) which we need to make happen - // tell Alice to trust the dummy device that signed the backup, and re-check the backup. // XXX: should we automatically re-check after a device becomes verified? + await waitForDeviceList(); await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID); await aliceClient.checkKeyBackup(); @@ -170,4 +166,72 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe await awaitDecryption(event, { waitOnDecryptionFailure: true }); expect(event.getContent()).toEqual("testytest"); }); + + oldBackendOnly("getActiveSessionBackupVersion() should give correct result", async function () { + // 404 means that there is no active backup + fetchMock.get("express:/_matrix/client/v3/room_keys/version", 404); + + aliceClient = await initTestClient(); + const aliceCrypto = aliceClient.getCrypto()!; + await aliceClient.startClient(); + + // tell Alice to trust the dummy device that signed the backup + await waitForDeviceList(); + await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID); + await aliceClient.checkKeyBackup(); + + // At this point there is no backup + let backupStatus: string | null; + backupStatus = await aliceCrypto.getActiveSessionBackupVersion(); + expect(backupStatus).toBeNull(); + + // Serve a backup with no trusted signature + const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA)); + delete unsignedBackup.auth_data.signatures; + fetchMock.get("express:/_matrix/client/v3/room_keys/version", unsignedBackup, { + overwriteRoutes: true, + }); + + const checked = await aliceClient.checkKeyBackup(); + expect(checked?.backupInfo?.version).toStrictEqual(unsignedBackup.version); + expect(checked?.trustInfo?.usable).toBeFalsy(); + + backupStatus = await aliceCrypto.getActiveSessionBackupVersion(); + expect(backupStatus).toBeNull(); + + // Add a valid signature to the backup + fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, { + overwriteRoutes: true, + }); + + // check that signaling is working + const backupPromise = new Promise((resolve, reject) => { + aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => { + if (enabled) { + resolve(); + } + }); + }); + + const validCheck = await aliceClient.checkKeyBackup(); + expect(validCheck?.trustInfo?.usable).toStrictEqual(true); + + await backupPromise; + + backupStatus = await aliceCrypto.getActiveSessionBackupVersion(); + expect(backupStatus).toStrictEqual(testData.SIGNED_BACKUP_DATA.version); + }); + + /** make sure that the client knows about the dummy device */ + async function waitForDeviceList(): Promise { + // Completing the initial sync will make the device list download outdated device lists (of which our own + // user will be one). + syncResponder.sendOrQueueSyncResponse({}); + // DeviceList has a sleep(5) which we need to make happen + await jest.advanceTimersByTimeAsync(10); + + // The client should now know about the dummy device + const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]); + expect(devices.get(TEST_USER_ID)!.keys()).toContain(TEST_DEVICE_ID); + } }); diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index 0173af4108a..608aed32336 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -569,6 +569,13 @@ describe("RustCrypto", () => { expect(new TextDecoder().decode(fetched!)).toEqual(key); }); }); + + describe("getActiveSessionBackupVersion", () => { + it("returns null", async () => { + const rustCrypto = await makeTestRustCrypto(); + expect(await rustCrypto.getActiveSessionBackupVersion()).toBeNull(); + }); + }); }); /** build a basic RustCrypto instance for testing diff --git a/src/client.ts b/src/client.ts index 376144bff38..5b1b1e925c1 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3294,6 +3294,12 @@ export class MatrixClient extends TypedEventEmitter; + + /** + * Get the current status of key backup. + * + * @returns If automatic key backups are enabled, the `version` of the active backup. Otherwise, `null`. + */ + getActiveSessionBackupVersion(): Promise; } /** diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 8ebc1bb0763..5071bd5c501 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -1280,6 +1280,18 @@ export class Crypto extends TypedEventEmitter { + if (this.backupManager.getKeyBackupEnabled()) { + return this.backupManager.version ?? null; + } + return null; + } + /** * Checks that a given cross-signing private key matches a given public key. * This can be used by the getCrossSigningKey callback to verify that the diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts new file mode 100644 index 00000000000..b14bc81c35d --- /dev/null +++ b/src/rust-crypto/backup.ts @@ -0,0 +1,25 @@ +/* +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. +*/ + +export class RustBackupManager { + /** + * Get the backup version we are currently backing up to, if any + */ + public async getActiveBackupVersion(): Promise { + // TODO stub + return null; + } +} diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index f2731e49717..cfd3f2e6061 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -56,6 +56,7 @@ import { RustVerificationRequest, verificationMethodIdentifierToMethod } from ". import { EventType } from "../@types/event"; import { CryptoEvent } from "../crypto"; import { TypedEventEmitter } from "../models/typed-event-emitter"; +import { RustBackupManager } from "./backup"; const ALL_VERIFICATION_METHODS = ["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"]; @@ -82,6 +83,8 @@ export class RustCrypto extends TypedEventEmitter { @@ -780,6 +784,15 @@ export class RustCrypto extends TypedEventEmitter { + return await this.backupManager.getActiveBackupVersion(); + } + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // SyncCryptoCallbacks implementation