fix(matrix): require durable authenticated recovery keys

This commit is contained in:
Peter Steinberger
2026-06-22 01:31:27 -04:00
committed by Vincent Koc
parent 592d458943
commit afa7684e4b
4 changed files with 62 additions and 5 deletions

View File

@@ -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<void>;
}
).ensureCryptoSupportInitialized();
const bootstrapper = (
client as unknown as {
cryptoBootstrapper: {
deps: { canUnlockSecretStorage: () => Promise<boolean> };
};
}
).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");

View File

@@ -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,

View File

@@ -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({

View File

@@ -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;