From afa7684e4bf4e4a431ccd099af888f3999b439dc Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 22 Jun 2026 01:31:27 -0400 Subject: [PATCH] fix(matrix): require durable authenticated recovery keys --- extensions/matrix/src/matrix/sdk.test.ts | 48 +++++++++++++++++++ extensions/matrix/src/matrix/sdk.ts | 6 ++- .../src/matrix/sdk/recovery-key-store.test.ts | 9 ++++ .../src/matrix/sdk/recovery-key-store.ts | 4 -- 4 files changed, 62 insertions(+), 5 deletions(-) diff --git a/extensions/matrix/src/matrix/sdk.test.ts b/extensions/matrix/src/matrix/sdk.test.ts index 5b5f1f14350..d73906b33b5 100644 --- a/extensions/matrix/src/matrix/sdk.test.ts +++ b/extensions/matrix/src/matrix/sdk.test.ts @@ -1645,6 +1645,54 @@ describe("MatrixClient crypto bootstrapping", () => { expect(bootstrapSpy).toHaveBeenCalledTimes(2); }); + it("rejects recovery keys when secret-storage metadata cannot authenticate them", async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "matrix-sdk-test-")); + const recoveryKeyPath = path.join(tmpDir, "recovery-key.json"); + fs.writeFileSync( + recoveryKeyPath, + JSON.stringify({ + version: 1, + createdAt: new Date().toISOString(), + keyId: "SSSSKEY", + privateKeyBase64: Buffer.from([1, 2, 3, 4]).toString("base64"), + }), + "utf8", + ); + const checkKey = vi.fn(async () => true); + Object.assign(matrixJsClient, { + secretStorage: { + getDefaultKeyId: vi.fn(async () => "SSSSKEY"), + getKey: vi.fn(async () => [ + "SSSSKEY", + { + algorithm: "m.secret_storage.v1.aes-hmac-sha2", + iv: "authenticated-iv", + }, + ]), + checkKey, + }, + }); + const client = new MatrixClient("https://matrix.example.org", "token", { + encryption: true, + recoveryKeyPath, + }); + await ( + client as unknown as { + ensureCryptoSupportInitialized: () => Promise; + } + ).ensureCryptoSupportInitialized(); + const bootstrapper = ( + client as unknown as { + cryptoBootstrapper: { + deps: { canUnlockSecretStorage: () => Promise }; + }; + } + ).cryptoBootstrapper; + + await expect(bootstrapper.deps.canUnlockSecretStorage()).resolves.toBe(false); + expect(checkKey).not.toHaveBeenCalled(); + }); + it("provides secret storage callbacks and resolves stored recovery key", async () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "matrix-sdk-test-")); const recoveryKeyPath = path.join(tmpDir, "recovery-key.json"); diff --git a/extensions/matrix/src/matrix/sdk.ts b/extensions/matrix/src/matrix/sdk.ts index deb60c3aeee..5029327bc1c 100644 --- a/extensions/matrix/src/matrix/sdk.ts +++ b/extensions/matrix/src/matrix/sdk.ts @@ -494,7 +494,11 @@ export class MatrixClient { if (!keyTuple || !key) { return false; } - return await this.client.secretStorage.checkKey(key, keyTuple[1]); + const keyInfo = keyTuple[1]; + if (!keyInfo.iv?.trim() || !keyInfo.mac?.trim()) { + return false; + } + return await this.client.secretStorage.checkKey(key, keyInfo); }, getDeviceId: () => this.client.getDeviceId(), verificationManager: this.verificationManager, diff --git a/extensions/matrix/src/matrix/sdk/recovery-key-store.test.ts b/extensions/matrix/src/matrix/sdk/recovery-key-store.test.ts index 4cbdf25b207..9b539aec2af 100644 --- a/extensions/matrix/src/matrix/sdk/recovery-key-store.test.ts +++ b/extensions/matrix/src/matrix/sdk/recovery-key-store.test.ts @@ -240,6 +240,15 @@ describe("MatrixRecoveryKeyStore", () => { expect(saved.privateKeyBase64).toBe(Buffer.from([9, 8, 7]).toString("base64")); }); + it("does not authorize destructive reset from an ephemeral cached key", () => { + const store = new MatrixRecoveryKeyStore(); + const callbacks = store.buildCryptoCallbacks(); + + callbacks.cacheSecretStorageKey?.("KEY123", { name: "openclaw" }, new Uint8Array([9, 8, 7])); + + expect(store.getSecretStorageKeyCandidate("KEY123")).toBeNull(); + }); + it("creates and persists a recovery key when secret storage is missing", async () => { const { store, createRecoveryKeyFromPassphrase, bootstrapSecretStorage } = await runSecretStorageBootstrapScenario({ diff --git a/extensions/matrix/src/matrix/sdk/recovery-key-store.ts b/extensions/matrix/src/matrix/sdk/recovery-key-store.ts index af7e40f4ea2..28b769fc9e5 100644 --- a/extensions/matrix/src/matrix/sdk/recovery-key-store.ts +++ b/extensions/matrix/src/matrix/sdk/recovery-key-store.ts @@ -144,10 +144,6 @@ export class MatrixRecoveryKeyStore { if (staged) { return staged[1]; } - const cached = this.secretStorageKeyCache.get(normalizedKeyId); - if (cached) { - return new Uint8Array(cached.key); - } const stored = this.loadStoredRecoveryKey(); if (!stored?.privateKeyBase64) { return null;