diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index 1b56dfccbc..2f609cc7c6 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -61,6 +61,7 @@ import { EventShieldReason, ImportRoomKeysOpts, KeyBackupCheck, + KeyBackupInfo, VerificationRequest, } from "../../../src/crypto-api"; import * as testData from "../../test-utils/test-data"; @@ -72,6 +73,7 @@ import { Curve25519AuthData } from "../../../src/crypto-api/keybackup"; import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts"; import { CryptoStore, SecretStorePrivateKeys } from "../../../src/crypto/store/base"; import { CryptoEvent } from "../../../src/crypto-api/index.ts"; +import { RustBackupManager } from "../../../src/rust-crypto/backup"; const TEST_USER = "@alice:example.com"; const TEST_DEVICE_ID = "TEST_DEVICE"; @@ -1879,6 +1881,71 @@ describe("RustCrypto", () => { ); }); }); + + describe("resetEncryption", () => { + fetchMock.post("path:/_matrix/client/v3/keys/upload", { one_time_key_counts: {} }); + fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {}); + + let secretStorage: ServerSideSecretStorage; + beforeEach(() => { + secretStorage = { + setDefaultKeyId: jest.fn(), + hasKey: jest.fn().mockResolvedValue(false), + getKey: jest.fn().mockResolvedValue(null), + } as unknown as ServerSideSecretStorage; + }); + + it("key backup should stay disabled after reset", async () => { + fetchMock.get("path:/_matrix/client/v3/room_keys/version", { errcode: "M_NOT_FOUND" }); + + const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), undefined, undefined, secretStorage); + + const authUploadDeviceSigningKeys = jest.fn(); + await rustCrypto.resetEncryption(authUploadDeviceSigningKeys); + + expect(secretStorage.setDefaultKeyId).toHaveBeenCalledWith(null); + expect(await rustCrypto.getActiveSessionBackupVersion()).toBeNull(); + expect(authUploadDeviceSigningKeys).toHaveBeenCalledWith(expect.any(Function)); + }); + + it("key backup should be re-enabled after reset", async () => { + fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA); + // When we will delete the key backup + fetchMock.delete("path:/_matrix/client/v3/room_keys/version/1", {}); + + jest.spyOn(RustBackupManager.prototype, "isKeyBackupTrusted").mockResolvedValue({ + trusted: true, + matchesDecryptionKey: true, + }); + + const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), undefined, undefined, secretStorage); + // We have a key backup + expect(await rustCrypto.getActiveSessionBackupVersion()).not.toBeNull(); + + let nbCalls = 0; + fetchMock.get( + "path:/_matrix/client/v3/room_keys/version", + () => { + // First call is when we check if the key backup is enabled + // Second call is to get the next backup after we deleted the first key backup + return ++nbCalls === 1 ? testData.SIGNED_BACKUP_DATA : {}; + }, + { overwriteRoutes: true }, + ); + + let content!: KeyBackupInfo; + fetchMock.post("path:/_matrix/client/v3/room_keys/version", (res, options) => { + content = JSON.parse(options.body as string); + return { version: "2" }; + }); + + const authUploadDeviceSigningKeys = jest.fn(); + await rustCrypto.resetEncryption(authUploadDeviceSigningKeys); + + expect(content.auth_data).toBeTruthy(); + expect(authUploadDeviceSigningKeys).toHaveBeenCalledWith(expect.any(Function)); + }); + }); }); /** Build a MatrixHttpApi instance */ diff --git a/src/crypto-api/index.ts b/src/crypto-api/index.ts index 4a78069677..81ed30c595 100644 --- a/src/crypto-api/index.ts +++ b/src/crypto-api/index.ts @@ -396,6 +396,19 @@ export interface CryptoApi { payload: ToDevicePayload, ): Promise; + /** + * Reset the encryption of the account by going through the following steps: + * - Disable backing up room keys and delete any existing backup. + * - Remove the default secret storage key from the account data (ie: the recovery key). + * - Reset the cross-signing keys. + * - Re-enable backing up room keys if enabled before. + * + * @param authUploadDeviceSigningKeys - Callback to authenticate the upload of device signing keys. + * Used when resetting the cross signing keys. + * See {@link BootstrapCrossSigningOpts#authUploadDeviceSigningKeys}. + */ + resetEncryption(authUploadDeviceSigningKeys: UIAuthCallback): Promise; + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Device/User verification diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 3af8d86436..8981da5639 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -4340,6 +4340,13 @@ export class Crypto extends TypedEventEmitter { throw new Error("Not implemented"); } + + /** + * Stub function -- resetEncryption is not implemented here, so throw error + */ + public resetEncryption(): Promise { + throw new Error("Not implemented"); + } } /** diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 02fe6270a1..0f4f165247 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -88,6 +88,7 @@ import { PerSessionKeyBackupDownloader } from "./PerSessionKeyBackupDownloader.t import { DehydratedDeviceManager } from "./DehydratedDeviceManager.ts"; import { VerificationMethod } from "../types.ts"; import { keyFromAuthData } from "../common-crypto/key-passphrase.ts"; +import { UIAuthCallback } from "../interactive-auth.ts"; const ALL_VERIFICATION_METHODS = [ VerificationMethod.Sas, @@ -1472,6 +1473,30 @@ export class RustCrypto extends TypedEventEmitter): Promise { + const backupEnabled = (await this.backupManager.getActiveBackupVersion()) !== null; + + // Delete all the backup + await this.backupManager.deleteAllKeyBackupVersions(); + + // Disable the recovery key and the secret storage + await this.secretStorage.setDefaultKeyId(null); + + // Reset the cross-signing keys + await this.crossSigningIdentity.bootstrapCrossSigning({ + setupNewCrossSigning: true, + authUploadDeviceSigningKeys, + }); + + // If key backup was enabled, we create a new backup + if (backupEnabled) { + await this.resetKeyBackup(); + } + } + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // SyncCryptoCallbacks implementation