mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 15:45:01 +00:00
Matrix-js: sync with main plugin-loading standards
This commit is contained in:
202
extensions/matrix-js/src/matrix/sdk/recovery-key-store.test.ts
Normal file
202
extensions/matrix-js/src/matrix/sdk/recovery-key-store.test.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { encodeRecoveryKey } from "matrix-js-sdk/lib/crypto-api/recovery-key.js";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { MatrixRecoveryKeyStore } from "./recovery-key-store.js";
|
||||
import type { MatrixCryptoBootstrapApi } from "./types.js";
|
||||
|
||||
function createTempRecoveryKeyPath(): string {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "matrix-recovery-key-store-"));
|
||||
return path.join(dir, "recovery-key.json");
|
||||
}
|
||||
|
||||
describe("MatrixRecoveryKeyStore", () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("loads a stored recovery key for requested secret-storage keys", async () => {
|
||||
const recoveryKeyPath = createTempRecoveryKeyPath();
|
||||
fs.writeFileSync(
|
||||
recoveryKeyPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
createdAt: new Date().toISOString(),
|
||||
keyId: "SSSS",
|
||||
privateKeyBase64: Buffer.from([1, 2, 3, 4]).toString("base64"),
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const store = new MatrixRecoveryKeyStore(recoveryKeyPath);
|
||||
const callbacks = store.buildCryptoCallbacks();
|
||||
const resolved = await callbacks.getSecretStorageKey?.(
|
||||
{ keys: { SSSS: { name: "test" } } },
|
||||
"m.cross_signing.master",
|
||||
);
|
||||
|
||||
expect(resolved?.[0]).toBe("SSSS");
|
||||
expect(Array.from(resolved?.[1] ?? [])).toEqual([1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
it("persists cached secret-storage keys with secure file permissions", () => {
|
||||
const recoveryKeyPath = createTempRecoveryKeyPath();
|
||||
const store = new MatrixRecoveryKeyStore(recoveryKeyPath);
|
||||
const callbacks = store.buildCryptoCallbacks();
|
||||
|
||||
callbacks.cacheSecretStorageKey?.(
|
||||
"KEY123",
|
||||
{
|
||||
name: "openclaw",
|
||||
},
|
||||
new Uint8Array([9, 8, 7]),
|
||||
);
|
||||
|
||||
const saved = JSON.parse(fs.readFileSync(recoveryKeyPath, "utf8")) as {
|
||||
keyId?: string;
|
||||
privateKeyBase64?: string;
|
||||
};
|
||||
expect(saved.keyId).toBe("KEY123");
|
||||
expect(saved.privateKeyBase64).toBe(Buffer.from([9, 8, 7]).toString("base64"));
|
||||
|
||||
const mode = fs.statSync(recoveryKeyPath).mode & 0o777;
|
||||
expect(mode).toBe(0o600);
|
||||
});
|
||||
|
||||
it("creates and persists a recovery key when secret storage is missing", async () => {
|
||||
const recoveryKeyPath = createTempRecoveryKeyPath();
|
||||
const store = new MatrixRecoveryKeyStore(recoveryKeyPath);
|
||||
const generated = {
|
||||
keyId: "GENERATED",
|
||||
keyInfo: { name: "generated" },
|
||||
privateKey: new Uint8Array([5, 6, 7, 8]),
|
||||
encodedPrivateKey: "encoded-generated-key",
|
||||
};
|
||||
const createRecoveryKeyFromPassphrase = vi.fn(async () => generated);
|
||||
const bootstrapSecretStorage = vi.fn(
|
||||
async (opts?: { createSecretStorageKey?: () => Promise<unknown> }) => {
|
||||
await opts?.createSecretStorageKey?.();
|
||||
},
|
||||
);
|
||||
const crypto = {
|
||||
on: vi.fn(),
|
||||
bootstrapCrossSigning: vi.fn(async () => {}),
|
||||
bootstrapSecretStorage,
|
||||
createRecoveryKeyFromPassphrase,
|
||||
getSecretStorageStatus: vi.fn(async () => ({ ready: false, defaultKeyId: null })),
|
||||
requestOwnUserVerification: vi.fn(async () => null),
|
||||
} as unknown as MatrixCryptoBootstrapApi;
|
||||
|
||||
await store.bootstrapSecretStorageWithRecoveryKey(crypto);
|
||||
|
||||
expect(createRecoveryKeyFromPassphrase).toHaveBeenCalledTimes(1);
|
||||
expect(bootstrapSecretStorage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
setupNewSecretStorage: true,
|
||||
}),
|
||||
);
|
||||
expect(store.getRecoveryKeySummary()).toMatchObject({
|
||||
keyId: "GENERATED",
|
||||
encodedPrivateKey: "encoded-generated-key",
|
||||
});
|
||||
});
|
||||
|
||||
it("rebinds stored recovery key to server default key id when it changes", async () => {
|
||||
const recoveryKeyPath = createTempRecoveryKeyPath();
|
||||
fs.writeFileSync(
|
||||
recoveryKeyPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
createdAt: new Date().toISOString(),
|
||||
keyId: "OLD",
|
||||
privateKeyBase64: Buffer.from([1, 2, 3, 4]).toString("base64"),
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
const store = new MatrixRecoveryKeyStore(recoveryKeyPath);
|
||||
|
||||
const bootstrapSecretStorage = vi.fn(async () => {});
|
||||
const createRecoveryKeyFromPassphrase = vi.fn(async () => {
|
||||
throw new Error("should not be called");
|
||||
});
|
||||
const crypto = {
|
||||
on: vi.fn(),
|
||||
bootstrapCrossSigning: vi.fn(async () => {}),
|
||||
bootstrapSecretStorage,
|
||||
createRecoveryKeyFromPassphrase,
|
||||
getSecretStorageStatus: vi.fn(async () => ({ ready: true, defaultKeyId: "NEW" })),
|
||||
requestOwnUserVerification: vi.fn(async () => null),
|
||||
} as unknown as MatrixCryptoBootstrapApi;
|
||||
|
||||
await store.bootstrapSecretStorageWithRecoveryKey(crypto);
|
||||
|
||||
expect(createRecoveryKeyFromPassphrase).not.toHaveBeenCalled();
|
||||
expect(store.getRecoveryKeySummary()).toMatchObject({
|
||||
keyId: "NEW",
|
||||
});
|
||||
});
|
||||
|
||||
it("recreates secret storage when default key exists but is not usable locally", async () => {
|
||||
const recoveryKeyPath = createTempRecoveryKeyPath();
|
||||
const store = new MatrixRecoveryKeyStore(recoveryKeyPath);
|
||||
const generated = {
|
||||
keyId: "RECOVERED",
|
||||
keyInfo: { name: "recovered" },
|
||||
privateKey: new Uint8Array([1, 1, 2, 3]),
|
||||
encodedPrivateKey: "encoded-recovered-key",
|
||||
};
|
||||
const createRecoveryKeyFromPassphrase = vi.fn(async () => generated);
|
||||
const bootstrapSecretStorage = vi.fn(
|
||||
async (opts?: { createSecretStorageKey?: () => Promise<unknown> }) => {
|
||||
await opts?.createSecretStorageKey?.();
|
||||
},
|
||||
);
|
||||
const crypto = {
|
||||
on: vi.fn(),
|
||||
bootstrapCrossSigning: vi.fn(async () => {}),
|
||||
bootstrapSecretStorage,
|
||||
createRecoveryKeyFromPassphrase,
|
||||
getSecretStorageStatus: vi.fn(async () => ({ ready: false, defaultKeyId: "LEGACY" })),
|
||||
requestOwnUserVerification: vi.fn(async () => null),
|
||||
} as unknown as MatrixCryptoBootstrapApi;
|
||||
|
||||
await store.bootstrapSecretStorageWithRecoveryKey(crypto);
|
||||
|
||||
expect(createRecoveryKeyFromPassphrase).toHaveBeenCalledTimes(1);
|
||||
expect(bootstrapSecretStorage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
setupNewSecretStorage: true,
|
||||
}),
|
||||
);
|
||||
expect(store.getRecoveryKeySummary()).toMatchObject({
|
||||
keyId: "RECOVERED",
|
||||
encodedPrivateKey: "encoded-recovered-key",
|
||||
});
|
||||
});
|
||||
|
||||
it("stores an encoded recovery key and decodes its private key material", () => {
|
||||
const recoveryKeyPath = createTempRecoveryKeyPath();
|
||||
const store = new MatrixRecoveryKeyStore(recoveryKeyPath);
|
||||
const encoded = encodeRecoveryKey(new Uint8Array(Array.from({ length: 32 }, (_, i) => i + 1)));
|
||||
expect(encoded).toBeTypeOf("string");
|
||||
|
||||
const summary = store.storeEncodedRecoveryKey({
|
||||
encodedPrivateKey: encoded as string,
|
||||
keyId: "SSSSKEY",
|
||||
});
|
||||
|
||||
expect(summary.keyId).toBe("SSSSKEY");
|
||||
expect(summary.encodedPrivateKey).toBe(encoded);
|
||||
const persisted = JSON.parse(fs.readFileSync(recoveryKeyPath, "utf8")) as {
|
||||
privateKeyBase64?: string;
|
||||
keyId?: string;
|
||||
};
|
||||
expect(persisted.keyId).toBe("SSSSKEY");
|
||||
expect(
|
||||
Buffer.from(persisted.privateKeyBase64 ?? "", "base64").equals(
|
||||
Buffer.from(Array.from({ length: 32 }, (_, i) => i + 1)),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user