优化
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
// 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(name) {
|
||||
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; }),
|
||||
get: (key) => makeRequest(() => data.get(key)),
|
||||
delete: (key) => makeRequest(() => { data.delete(key); return undefined; }),
|
||||
});
|
||||
return tx;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
open(name) {
|
||||
const req = {
|
||||
result: undefined, error: null,
|
||||
onsuccess: null, onerror: null, onupgradeneeded: null, onblocked: null,
|
||||
};
|
||||
queueMicrotask(() => {
|
||||
let db = databases.get(name);
|
||||
const isNew = !db;
|
||||
if (isNew) {
|
||||
db = makeDb(name);
|
||||
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")
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user