// tests/crypto-store.test.js // 测试 crypto-store.js:主口令派生、AES-GCM 加解密、设置/解锁/锁定 // 运行:node --test tests/crypto-store.test.js const { describe, it, beforeEach } = require("node:test"); const assert = require("node:assert/strict"); function createMemoryStorage() { const local = {}; const session = {}; const makeArea = (store) => ({ async get(keys) { const result = {}; if (Array.isArray(keys)) keys.forEach((k) => { if (k in store) result[k] = store[k]; }); else if (typeof keys === "string") { if (keys in store) result[keys] = store[keys]; } else Object.assign(result, store); return result; }, async set(items) { Object.assign(store, items); }, async remove(keys) { if (Array.isArray(keys)) keys.forEach((k) => delete store[k]); else delete store[keys]; }, }); return { local: makeArea(local), session: makeArea(session), localStore: local, sessionStore: session }; } function loadCryptoStore(storage) { global.chrome = { storage }; delete require.cache[require.resolve("../dist/crypto-store.js")]; return require("../dist/crypto-store.js"); } describe("crypto-store - 基础加解密", () => { it("encryptJson + decryptJson 往返一致", async () => { const storage = createMemoryStorage(); const C = loadCryptoStore(storage); const salt = C.randomBytes(16); const key = await C.deriveKey("mypassword", salt, C.PBKDF2_ITERATIONS); const data = [{ id: "e1", name: "个人", configs: [{ domain: "a.com" }] }]; const cipher = await C.encryptJson(data, key); const decrypted = await C.decryptJson(cipher, key); assert.deepEqual(decrypted, data); }); it("不同口令派生的密钥无法解密对方数据", async () => { const storage = createMemoryStorage(); const C = loadCryptoStore(storage); const salt = C.randomBytes(16); const key1 = await C.deriveKey("password1", salt, C.PBKDF2_ITERATIONS); const key2 = await C.deriveKey("password2", salt, C.PBKDF2_ITERATIONS); const cipher = await C.encryptJson({ secret: "hello" }, key1); await assert.rejects(() => C.decryptJson(cipher, key2)); }); it("b64encode / b64decode 往返一致", () => { const C = loadCryptoStore(createMemoryStorage()); const buf = new Uint8Array([0, 1, 2, 253, 254, 255]); const encoded = C.b64encode(buf); const decoded = C.b64decode(encoded); assert.deepEqual(Array.from(decoded), Array.from(buf)); }); }); describe("crypto-store - 设置/解锁/锁定", () => { let C, storage; beforeEach(() => { storage = createMemoryStorage(); C = loadCryptoStore(storage); }); it("hasVault 初始为 false", async () => { assert.equal(await C.hasVault(), false); assert.equal(await C.isUnlocked(), false); }); it("setupVault 加密并解锁", async () => { const envs = [{ id: "e1", name: "个人", configs: [] }]; const key = await C.setupVault("mypass123", envs); assert.ok(key); assert.equal(await C.hasVault(), true); assert.equal(await C.isUnlocked(), true); // local 中是密文,不是明文 assert.ok(storage.localStore.vaultMeta); assert.ok(storage.localStore.encryptedVault); assert.equal(storage.localStore.environments, undefined); // session 中是明文 assert.ok(storage.sessionStore.environments); }); it("setupVault 口令不能为空", async () => { await assert.rejects(() => C.setupVault("", []), /口令不能为空/); }); it("unlock 正确口令可解密", async () => { await C.setupVault("correct horse", [{ id: "e1", name: "x", configs: [] }]); // 先锁定 await C.lock(); assert.equal(await C.isUnlocked(), false); const { environments, key } = await C.unlock("correct horse"); assert.ok(key); assert.equal(environments.length, 1); assert.equal(environments[0].name, "x"); assert.equal(await C.isUnlocked(), true); }); it("unlock 错误口令抛错", async () => { await C.setupVault("correct", []); await C.lock(); await assert.rejects(() => C.unlock("wrong"), /口令错误/); // 错误口令后仍未解锁 assert.equal(await C.isUnlocked(), false); }); it("lock 清除 session 明文但保留 local 密文", async () => { await C.setupVault("pw", [{ id: "e1", name: "x", configs: [] }]); assert.ok(storage.sessionStore.environments); await C.lock(); assert.equal(storage.sessionStore.environments, undefined); // 密文还在 assert.ok(storage.localStore.encryptedVault); }); it("unlock 未设置口令时抛错", async () => { await assert.rejects(() => C.unlock("anything"), /尚未设置口令/); }); it("lock 后可再次 unlock", async () => { await C.setupVault("pw", [{ id: "e1", name: "x", configs: [] }]); await C.lock(); await C.unlock("pw"); assert.equal(await C.isUnlocked(), true); const envs = (await C.unlock("pw")).environments; assert.equal(envs[0].name, "x"); }); }); describe("crypto-store - unlockWithKey(密钥句柄恢复)", () => { it("用 setupVault 返回的密钥可重新解锁,无需口令", async () => { const storage = createMemoryStorage(); const C = loadCryptoStore(storage); const key = await C.setupVault("pw", [{ id: "e1", name: "个人", configs: [{ domain: "a.com" }] }]); await C.lock(); assert.equal(await C.isUnlocked(), false); // 模拟 SW 重启后从 IndexedDB 取回句柄 const environments = await C.unlockWithKey(key); assert.equal(environments.length, 1); assert.equal(environments[0].name, "个人"); assert.equal(await C.isUnlocked(), true); assert.ok(storage.sessionStore.environments); // 全程没有明文口令落进任何存储 assert.equal(storage.sessionStore.__autoPwd, undefined); }); it("密钥与 vault 不匹配时抛错", async () => { const storage = createMemoryStorage(); const C = loadCryptoStore(storage); await C.setupVault("pw", [{ id: "e1", name: "x", configs: [] }]); await C.lock(); const otherKey = await C.deriveKey("别的口令", C.randomBytes(16), C.PBKDF2_ITERATIONS); await assert.rejects(() => C.unlockWithKey(otherKey)); assert.equal(await C.isUnlocked(), false); }); it("未设置口令时 unlockWithKey 抛错", async () => { const C = loadCryptoStore(createMemoryStorage()); const key = await C.deriveKey("pw", C.randomBytes(16), C.PBKDF2_ITERATIONS); await assert.rejects(() => C.unlockWithKey(key), /尚未设置口令/); }); it("重设口令后旧句柄失效(背景会据此丢弃句柄)", async () => { const storage = createMemoryStorage(); const C = loadCryptoStore(storage); const oldKey = await C.setupVault("old-pw", [{ id: "e1", name: "x", configs: [] }]); await C.setupVault("new-pw", [{ id: "e1", name: "x", configs: [] }]); await C.lock(); await assert.rejects(() => C.unlockWithKey(oldKey)); }); }); describe("crypto-store - persistEncrypted", () => { it("用密钥重新加密 environments 写回 local", async () => { const storage = createMemoryStorage(); const C = loadCryptoStore(storage); const key = await C.setupVault("pw", [{ id: "e1", name: "x", configs: [] }]); // 修改环境 const newEnvs = [{ id: "e1", name: "x", configs: [{ domain: "a.com" }] }]; await C.persistEncrypted(newEnvs, key); // 锁定后重新解锁,验证持久化生效 await C.lock(); const { environments } = await C.unlock("pw"); assert.equal(environments[0].configs.length, 1); assert.equal(environments[0].configs[0].domain, "a.com"); }); });