Files
auto-login/tests/key-store.test.js
陈银军 1b0c3c012c feat: 按域名授权、iframe 多帧填充、自动提交限流修正,并补齐工程规范与文档
安全与权限:
- 站点访问权限改为 optional_host_permissions,保存配置时逐域名授权;移除 host_permissions 与 tabs,安装提示不再出现全站数据访问与浏览记录
- 主口令不再写入 storage.session,改用 IndexedDB 中不可导出的 CryptoKey 句柄恢复解锁,锁定即丢弃
- content script 注入范围只注册已授权域名,并随权限变化即时收敛

自动登录:
- 支持 iframe 内的登录表单:脚本注入所有帧,逐帧按自身地址匹配配置
- 手动填充改为逐帧探测,定向发送到真正含密码框的帧
- 限流修正:只在真正触发提交后计数;判定登录成功后立即清零;达到上限时页面给出可见提示;重置判断只看启用中的配置
- SPA 重试改为 DOM 变更门控 + 退避,并单独监听已发现的 Shadow Root

工程化与文档:
- 引入 ESLint(扁平配置)与 Prettier,CI 增加 lint 与 format:check
- 信息类日志改为 debugLog(默认静默,chrome.storage.local.debugLog 开关)
- 测试 123 → 128 用例(新增权限、iframe、限流相关用例)
- README / PRIVACY / CHANGELOG 同步
2026-09-18 00:05:20 +08:00

170 lines
5.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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() {
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();
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);
});
});