From 61375ef38a3ab411834cd872484201b4ab2716ac Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 22 Jan 2025 11:53:50 +0100 Subject: [PATCH] Add `CryptoApi.resetEncryption` (#4614) * feat(crypto api): Add `CryptoApi#resetEncryption` * docs(crypto api): Review changes * test(crypto api): Cleaner way to handle key backup removal --- spec/unit/rust-crypto/rust-crypto.spec.ts | 70 +++++++++++++++++++++++ src/crypto-api/index.ts | 13 +++++ src/crypto/index.ts | 7 +++ src/rust-crypto/rust-crypto.ts | 25 ++++++++ 4 files changed, 115 insertions(+) diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index 1b56dfccbc..632a39df98 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.ts"; const TEST_USER = "@alice:example.com"; const TEST_DEVICE_ID = "TEST_DEVICE"; @@ -1879,6 +1881,74 @@ describe("RustCrypto", () => { ); }); }); + + describe("resetEncryption", () => { + let secretStorage: ServerSideSecretStorage; + beforeEach(() => { + secretStorage = { + setDefaultKeyId: jest.fn(), + hasKey: jest.fn().mockResolvedValue(false), + getKey: jest.fn().mockResolvedValue(null), + } as unknown as ServerSideSecretStorage; + + fetchMock.post("path:/_matrix/client/v3/keys/upload", { one_time_key_counts: {} }); + fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {}); + }); + + it("reset should reset 4S, backup and cross-signing", async () => { + // We don't have a key backup + fetchMock.get("path:/_matrix/client/v3/room_keys/version", {}); + + const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), undefined, undefined, secretStorage); + + const authUploadDeviceSigningKeys = jest.fn(); + await rustCrypto.resetEncryption(authUploadDeviceSigningKeys); + + // The default key id should be deleted + expect(secretStorage.setDefaultKeyId).toHaveBeenCalledWith(null); + expect(await rustCrypto.getActiveSessionBackupVersion()).toBeNull(); + // The new cross signing keys should be uploaded + expect(authUploadDeviceSigningKeys).toHaveBeenCalledWith(expect.any(Function)); + }); + + it("key backup should be re-enabled after reset", async () => { + // When we will delete the key backup + let backupIsDeleted = false; + fetchMock.delete("path:/_matrix/client/v3/room_keys/version/1", () => { + backupIsDeleted = true; + return {}; + }); + // If the backup is deleted, we will return an empty object + fetchMock.get("path:/_matrix/client/v3/room_keys/version", () => { + return backupIsDeleted ? {} : testData.SIGNED_BACKUP_DATA; + }); + + // We consider the key backup as trusted + 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(); + + // A new key backup should be created after the reset + let newKeyBackupInfo!: KeyBackupInfo; + fetchMock.post("path:/_matrix/client/v3/room_keys/version", (res, options) => { + newKeyBackupInfo = JSON.parse(options.body as string); + return { version: "2" }; + }); + + const authUploadDeviceSigningKeys = jest.fn(); + await rustCrypto.resetEncryption(authUploadDeviceSigningKeys); + + // A new key backup should be created + expect(newKeyBackupInfo.auth_data).toBeTruthy(); + // The new cross signing keys should be uploaded + 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..786345eb83 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 user by going through the following steps: + * - Disable backing up room keys and delete any existing backups. + * - 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 f66402f560..596ef97aad 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; + + // Disable backup, and delete all the backups from the server + 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