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

185 lines
5.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 整库加密。
* local 存 vaultMeta / encryptedVault;解锁后明文放 session,不进 local;
* 派生密钥仅由 background 在内存中持有。
*/
(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;
}
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"]
);
}
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);
}
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,口令错误抛错,返回明文与密钥 */
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 };
}
async function lock() {
await getStorage("session").remove([SESSION_ENV_KEY, SESSION_ACTIVE_KEY]);
}
async function getVaultMeta() {
return getLocal(VAULT_META_KEY);
}
async function getEncryptedVault() {
return getLocal(ENCRYPTED_VAULT_KEY);
}
/** 用给定密钥加密 environments 并写回 local */
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,
};
});