Files
auto-login/crypto-store.js
T
2026-09-14 13:45:17 +08:00

205 lines
6.4 KiB
JavaScript
Raw 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.
/**
* 加密存储模块:主口令 + AES-GCM 加密整库。
*
* 存储布局:
* chrome.storage.local:
* - vaultMeta: { salt(base64), iterations, version } 口令派生参数
* - encryptedVault: { iv(base64), ciphertext(base64) } AES-GCM 密文
* chrome.storage.session(仅解锁后存在,内存中、不落盘):
* - environments: 解密后的环境数组
* - activeEnvId: 当前激活环境 id
*
* 主口令不存储。派生的 CryptoKey 仅在解锁后存放于 background SW 内存中,
* 用于每次 session 数据变化时重新加密写回 local。
*
* 同时在浏览器(Service Worker / 扩展页面)和 Node 测试环境下可用。
*/
(function (root, factory) {
if (typeof module === "object" && module.exports) {
module.exports = factory({
getCrypto: () => globalThis.crypto,
getStorage: (area) => {
const c = typeof chrome !== "undefined" ? chrome : null;
return c?.storage?.[area] || null;
},
});
} else {
const api = factory({
getCrypto: () => crypto,
getStorage: (area) => chrome.storage[area],
});
root.CryptoStore = api;
}
})(typeof globalThis !== "undefined" ? globalThis : this, function ({ getCrypto, getStorage }) {
const VAULT_META_KEY = "vaultMeta";
const ENCRYPTED_VAULT_KEY = "encryptedVault";
const SESSION_ENV_KEY = "environments";
const SESSION_ACTIVE_KEY = "activeEnvId";
const PBKDF2_ITERATIONS = 200000;
const VAULT_VERSION = 1;
function b64encode(buf) {
const bytes = new Uint8Array(buf);
let s = "";
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
return btoa(s);
}
function b64decode(str) {
const s = atob(str);
const bytes = new Uint8Array(s.length);
for (let i = 0; i < s.length; i++) bytes[i] = s.charCodeAt(i);
return bytes;
}
function randomBytes(n) {
const arr = new Uint8Array(n);
getCrypto().getRandomValues(arr);
return arr;
}
/** 从口令派生 AES-GCM 密钥 */
async function deriveKey(password, salt, iterations) {
const enc = new TextEncoder();
const keyMaterial = await getCrypto().subtle.importKey(
"raw", enc.encode(password), "PBKDF2", false, ["deriveKey"]
);
return getCrypto().subtle.deriveKey(
{ name: "PBKDF2", salt, iterations, hash: "SHA-256" },
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
}
/** 加密任意 JSON 对象,返回 { iv, ciphertext } base64 */
async function encryptJson(obj, key) {
const enc = new TextEncoder();
const iv = randomBytes(12);
const ciphertext = await getCrypto().subtle.encrypt(
{ name: "AES-GCM", iv }, key, enc.encode(JSON.stringify(obj))
);
return { iv: b64encode(iv), ciphertext: b64encode(ciphertext) };
}
/** 解密为 JSON 对象,口令错误/数据损坏抛错 */
async function decryptJson({ iv, ciphertext }, key) {
const dec = new TextDecoder();
const plain = await getCrypto().subtle.decrypt(
{ name: "AES-GCM", iv: b64decode(iv) }, key, b64decode(ciphertext)
);
return JSON.parse(dec.decode(plain));
}
async function getLocal(key) {
return (await getStorage("local").get(key))[key];
}
/** 是否已创建过口令 */
async function hasVault() {
const meta = await getLocal(VAULT_META_KEY);
const cipher = await getLocal(ENCRYPTED_VAULT_KEY);
return !!(meta && cipher);
}
/** 是否处于解锁状态(session 中有明文环境数据) */
async function isUnlocked() {
const data = await getStorage("session").get(SESSION_ENV_KEY);
return !!data[SESSION_ENV_KEY];
}
/**
* 首次创建口令:生成盐,用口令加密初始环境,写入 local。
* 同时把环境写入 session,视为已解锁。返回派生密钥(供调用方内存持有)。
*/
async function setupVault(password, environments) {
if (!password) throw new Error("口令不能为空");
const salt = randomBytes(16);
const key = await deriveKey(password, salt, PBKDF2_ITERATIONS);
const cipher = await encryptJson(environments || [], key);
const meta = {
version: VAULT_VERSION,
iterations: PBKDF2_ITERATIONS,
salt: b64encode(salt),
};
await getStorage("local").set({
[VAULT_META_KEY]: meta,
[ENCRYPTED_VAULT_KEY]: cipher,
});
const envs = environments || [];
await getStorage("session").set({
[SESSION_ENV_KEY]: envs,
[SESSION_ACTIVE_KEY]: envs[0]?.id || null,
});
return key;
}
/**
* 解锁:用口令解密 local 密文,写入 session。
* 返回 { environments, key },口令错误抛错。
*/
async function unlock(password) {
const meta = await getLocal(VAULT_META_KEY);
const cipher = await getLocal(ENCRYPTED_VAULT_KEY);
if (!meta || !cipher) throw new Error("尚未设置口令");
const key = await deriveKey(password, b64decode(meta.salt), meta.iterations);
let environments;
try {
environments = await decryptJson(cipher, key);
} catch {
throw new Error("口令错误");
}
const active = (await getStorage("session").get(SESSION_ACTIVE_KEY))[SESSION_ACTIVE_KEY]
|| environments[0]?.id || null;
await getStorage("session").set({
[SESSION_ENV_KEY]: environments,
[SESSION_ACTIVE_KEY]: active,
});
return { environments, key };
}
/** 锁定:清除 session 中的明文 */
async function lock() {
await getStorage("session").remove([SESSION_ENV_KEY, SESSION_ACTIVE_KEY]);
}
/** 读取 vaultMeta(供 background 派生密钥用) */
async function getVaultMeta() {
return getLocal(VAULT_META_KEY);
}
/** 读取加密 vault 密文 */
async function getEncryptedVault() {
return getLocal(ENCRYPTED_VAULT_KEY);
}
/** 用给定密钥加密 environments 并写回 local(background 内存密钥调用) */
async function persistEncrypted(environments, key) {
const cipher = await encryptJson(environments, key);
await getStorage("local").set({ [ENCRYPTED_VAULT_KEY]: cipher });
}
return {
VAULT_META_KEY,
ENCRYPTED_VAULT_KEY,
SESSION_ENV_KEY,
SESSION_ACTIVE_KEY,
PBKDF2_ITERATIONS,
hasVault,
isUnlocked,
setupVault,
unlock,
lock,
deriveKey,
encryptJson,
decryptJson,
getVaultMeta,
getEncryptedVault,
persistEncrypted,
b64encode,
b64decode,
randomBytes,
};
});