2026-09-14 13:45:17 +08:00
|
|
|
// 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 };
|
2026-09-17 01:40:27 +08:00
|
|
|
delete require.cache[require.resolve("../dist/crypto-store.js")];
|
|
|
|
|
return require("../dist/crypto-store.js");
|
2026-09-14 13:45:17 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-17 23:00:47 +08:00
|
|
|
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));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-14 13:45:17 +08:00
|
|
|
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");
|
|
|
|
|
});
|
|
|
|
|
});
|