Files
auto-login/tests/key-store.test.js
T

170 lines
5.0 KiB
JavaScript
Raw Normal View History

2026-09-17 23:00:47 +08:00
// tests/key-store.test.js
// 测试 key-store.js:把不可导出的 CryptoKey 句柄存取到 IndexedDB
// 运行:node --test tests/key-store.test.js
//
// 说明:用极简 IndexedDB shim 覆盖模块的异步流程与 API 语义;
// 真实的 structured clone(CryptoKey 句柄持久化)语义由浏览器保证,Node 侧无法完整模拟。
const { describe, it, beforeEach } = require("node:test");
const assert = require("node:assert/strict");
function createFakeIndexedDB() {
const databases = new Map();
function makeRequest(getResult) {
const req = { result: undefined, error: null, onsuccess: null, onerror: null };
queueMicrotask(() => {
try {
req.result = getResult();
if (req.onsuccess) req.onsuccess();
} catch (err) {
req.error = err;
if (req.onerror) req.onerror();
}
});
return req;
}
function makeDb() {
2026-09-17 23:00:47 +08:00
const stores = new Map();
return {
objectStoreNames: { contains: (n) => stores.has(n) },
createObjectStore: (n) => {
stores.set(n, new Map());
return {};
},
transaction(storeName) {
const data = stores.get(storeName);
const tx = { error: null, onabort: null };
tx.objectStore = () => ({
put: (value, key) =>
makeRequest(() => {
data.set(key, value);
return key;
}),
2026-09-17 23:00:47 +08:00
get: (key) => makeRequest(() => data.get(key)),
delete: (key) =>
makeRequest(() => {
data.delete(key);
return undefined;
}),
2026-09-17 23:00:47 +08:00
});
return tx;
},
};
}
return {
open(name) {
const req = {
result: undefined,
error: null,
onsuccess: null,
onerror: null,
onupgradeneeded: null,
onblocked: null,
2026-09-17 23:00:47 +08:00
};
queueMicrotask(() => {
let db = databases.get(name);
const isNew = !db;
if (isNew) {
db = makeDb();
2026-09-17 23:00:47 +08:00
databases.set(name, db);
}
req.result = db;
if (isNew && req.onupgradeneeded) req.onupgradeneeded();
if (req.onsuccess) req.onsuccess();
});
return req;
},
};
}
function loadKeyStore() {
delete require.cache[require.resolve("../dist/key-store.js")];
return require("../dist/key-store.js");
}
async function makeAesKey() {
// extractable = false:这正是 vault 派生密钥的形态
return crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
}
describe("key-store - 密钥句柄存取", () => {
let K;
beforeEach(() => {
global.indexedDB = createFakeIndexedDB();
K = loadKeyStore();
});
it("初始没有句柄时 loadKey 返回 null", async () => {
assert.equal(await K.loadKey(), null);
});
it("saveKey 后可以取回同一个密钥对象", async () => {
const key = await makeAesKey();
await K.saveKey(key);
const loaded = await K.loadKey();
assert.ok(loaded);
assert.equal(loaded.algorithm.name, "AES-GCM");
});
it("取回的密钥仍不可导出(句柄而非原始字节)", async () => {
const key = await makeAesKey();
await K.saveKey(key);
const loaded = await K.loadKey();
assert.equal(loaded.extractable, false);
await assert.rejects(() => crypto.subtle.exportKey("raw", loaded));
});
it("取回的密钥可以正常解密", async () => {
const key = await makeAesKey();
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
new TextEncoder().encode("secret"),
2026-09-17 23:00:47 +08:00
);
await K.saveKey(key);
const loaded = await K.loadKey();
const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, loaded, ciphertext);
assert.equal(new TextDecoder().decode(plain), "secret");
});
it("重复 saveKey 覆盖旧句柄", async () => {
const first = await makeAesKey();
await K.saveKey(first);
const iv = crypto.getRandomValues(new Uint8Array(12));
const payload = new TextEncoder().encode("hello");
const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, first, payload);
const second = await makeAesKey();
await K.saveKey(second);
const loaded = await K.loadKey();
// 取回的是第二个密钥,用旧密钥加密的数据解不开
await assert.rejects(() => crypto.subtle.decrypt({ name: "AES-GCM", iv }, loaded, ciphertext));
});
it("deleteKey 后取不到句柄(锁定即丢弃)", async () => {
await K.saveKey(await makeAesKey());
await K.deleteKey();
assert.equal(await K.loadKey(), null);
});
it("isAvailable 在支持 IndexedDB 时为 true", async () => {
assert.equal(await K.isAvailable(), true);
});
});
describe("key-store - 不支持 IndexedDB 时降级", () => {
it("loadKey 返回 null 而不是抛错", async () => {
delete global.indexedDB;
const K = loadKeyStore();
assert.equal(await K.loadKey(), null);
assert.equal(await K.isAvailable(), false);
});
});