diff --git a/spec/integ/crypto/crypto.spec.ts b/spec/integ/crypto/crypto.spec.ts index 96d809aea9..386f3dd531 100644 --- a/spec/integ/crypto/crypto.spec.ts +++ b/spec/integ/crypto/crypto.spec.ts @@ -3121,6 +3121,32 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, const mskId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.Master)!; expect(signatures![aliceClient.getUserId()!][`ed25519:${mskId}`]).toBeDefined(); }); + + newBackendOnly("should upload existing megolm backup key to a new 4S store", async () => { + const backupKeyTo4SPromise = awaitMegolmBackupKeyUpload(); + + // we need these to set up the mocks but we don't actually care whether they + // resolve because we're not testing those things in this test. + awaitCrossSigningKeyUpload("master"); + awaitCrossSigningKeyUpload("user_signing"); + awaitCrossSigningKeyUpload("self_signing"); + awaitSecretStorageKeyStoredInAccountData(); + + mockSetupCrossSigningRequests(); + mockSetupMegolmBackupRequests("1"); + + await aliceClient.getCrypto()!.bootstrapCrossSigning({}); + await aliceClient.getCrypto()!.resetKeyBackup(); + + await aliceClient.getCrypto()!.bootstrapSecretStorage({ + setupNewSecretStorage: true, + createSecretStorageKey, + setupNewKeyBackup: false, + }); + + await backupKeyTo4SPromise; + expect(accountDataAccumulator.accountDataEvents.get("m.megolm_backup.v1")).toBeDefined(); + }); }); describe("Manage Key Backup", () => { diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index fc9b571acb..6510bcc854 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -705,6 +705,119 @@ describe("RustCrypto", () => { expect(resetKeyBackup.mock.calls).toHaveLength(2); }); + describe("upload existing key backup key to new 4S store", () => { + const secretStorageCallbacks = { + getSecretStorageKey: async (keys: any, name: string) => { + return [[...Object.keys(keys.keys)][0], new Uint8Array(32)]; + }, + } as SecretStorageCallbacks; + let secretStorage: ServerSideSecretStorageImpl; + + let backupAuthData: any; + let backupAlg: string; + + const fetchMock = { + authedRequest: jest.fn().mockImplementation((method, path, query, body) => { + if (path === "/room_keys/version") { + if (method === "POST") { + backupAuthData = body["auth_data"]; + backupAlg = body["algorithm"]; + return Promise.resolve({ version: "1", algorithm: backupAlg, auth_data: backupAuthData }); + } else if (method === "GET" && backupAuthData) { + return Promise.resolve({ version: "1", algorithm: backupAlg, auth_data: backupAuthData }); + } + } + return Promise.resolve({}); + }), + }; + + beforeEach(() => { + backupAuthData = undefined; + backupAlg = ""; + + secretStorage = new ServerSideSecretStorageImpl(new DummyAccountDataClient(), secretStorageCallbacks); + }); + + it("bootstrapSecretStorage saves megolm backup key if already cached", async () => { + const rustCrypto = await makeTestRustCrypto( + fetchMock as unknown as MatrixHttpApi, + testData.TEST_USER_ID, + undefined, + secretStorage, + ); + + async function createSecretStorageKey() { + return { + keyInfo: {} as AddSecretStorageKeyOpts, + privateKey: new Uint8Array(32), + }; + } + + await rustCrypto.resetKeyBackup(); + + const storeSpy = jest.spyOn(secretStorage, "store"); + + await rustCrypto.bootstrapSecretStorage({ + createSecretStorageKey, + setupNewSecretStorage: true, + setupNewKeyBackup: false, + }); + + expect(storeSpy).toHaveBeenCalledWith("m.megolm_backup.v1", expect.anything()); + }); + + it("bootstrapSecretStorage doesn't try to save megolm backup key not in cache", async () => { + const mockOlmMachine = { + isBackupEnabled: jest.fn().mockResolvedValue(false), + sign: jest.fn().mockResolvedValue({ + asJSON: jest.fn().mockReturnValue("{}"), + }), + saveBackupDecryptionKey: jest.fn(), + crossSigningStatus: jest.fn().mockResolvedValue({ + hasMaster: true, + hasSelfSigning: true, + hasUserSigning: true, + }), + exportCrossSigningKeys: jest.fn().mockResolvedValue({ + masterKey: "sosecret", + userSigningKey: "secrets", + self_signing_key: "ssshhh", + }), + getBackupKeys: jest.fn().mockResolvedValue({}), + verifyBackup: jest.fn().mockResolvedValue({ trusted: jest.fn().mockReturnValue(false) }), + } as unknown as OlmMachine; + + const rustCrypto = new RustCrypto( + logger, + mockOlmMachine, + fetchMock as unknown as MatrixHttpApi, + TEST_USER, + TEST_DEVICE_ID, + secretStorage, + {} as CryptoCallbacks, + ); + + async function createSecretStorageKey() { + return { + keyInfo: {} as AddSecretStorageKeyOpts, + privateKey: new Uint8Array(32), + }; + } + + await rustCrypto.resetKeyBackup(); + + const storeSpy = jest.spyOn(secretStorage, "store"); + + await rustCrypto.bootstrapSecretStorage({ + createSecretStorageKey, + setupNewSecretStorage: true, + setupNewKeyBackup: false, + }); + + expect(storeSpy).not.toHaveBeenCalledWith("m.megolm_backup.v1", expect.anything()); + }); + }); + it("isSecretStorageReady", async () => { const mockSecretStorage = { getDefaultKeyId: jest.fn().mockResolvedValue(null), diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 86853a9867..79308e8369 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -841,11 +841,46 @@ export class RustCrypto extends TypedEventEmitter { + const keyBackupInfo = await this.backupManager.getServerBackupInfo(); + if (!keyBackupInfo || !keyBackupInfo.version) { + logger.info("Not saving backup key to secret storage: no backup info"); + return; + } + + const backupKeys: RustSdkCryptoJs.BackupKeys = await this.olmMachine.getBackupKeys(); + if (!backupKeys.decryptionKey) { + logger.info("Not saving backup key to secret storage: no backup key"); + return; + } + + if (!decryptionKeyMatchesKeyBackupInfo(backupKeys.decryptionKey, keyBackupInfo)) { + logger.info("Not saving backup key to secret storage: decryption key does not match backup info"); + } + + const backupKeyFromStorage = await this.secretStorage.get("m.megolm_backup.v1"); + const backupKeyBase64 = backupKeys.decryptionKey.toBase64(); + + // The backup version that the key corresponds to isn't saved in 4S so if it's different, we must assume + // it's stale and overwrite. + if (backupKeyFromStorage !== backupKeyBase64) { + await this.secretStorage.store("m.megolm_backup.v1", backupKeyBase64); + } + } + /** * Add the secretStorage key to the secret storage * - The secret storage key must have the `keyInfo` field filled