Files
auto-login/crypto-store.js
T

185 lines
5.4 KiB
JavaScript
Raw Normal View History

2026-09-14 13:45:17 +08:00
/**
2026-09-14 17:51:40 +08:00
* 加密库:主口令 + AES-GCM 整库加密。
* local 存 vaultMeta / encryptedVault;解锁后明文放 session,不进 local;
* 派生密钥仅由 background 在内存中持有。
2026-09-14 13:45:17 +08:00
*/
(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;
}
2026-09-14 17:51:40 +08:00
2026-09-14 13:45:17 +08:00
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"]
);
}
2026-09-14 17:51:40 +08:00
2026-09-14 13:45:17 +08:00
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) };
}
2026-09-14 17:51:40 +08:00
/** 解密为 JSON 对象,口令错误或数据损坏时抛错 */
2026-09-14 13:45:17 +08:00
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];
}
2026-09-14 17:51:40 +08:00
2026-09-14 13:45:17 +08:00
async function hasVault() {
const meta = await getLocal(VAULT_META_KEY);
const cipher = await getLocal(ENCRYPTED_VAULT_KEY);
return !!(meta && cipher);
}
2026-09-14 17:51:40 +08:00
2026-09-14 13:45:17 +08:00
async function isUnlocked() {
const data = await getStorage("session").get(SESSION_ENV_KEY);
return !!data[SESSION_ENV_KEY];
}
2026-09-14 17:51:40 +08:00
/** 首次设置口令:生成盐、加密初始环境写入 local,并把明文写入 session,返回派生密钥 */
2026-09-14 13:45:17 +08:00
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;
}
2026-09-14 17:51:40 +08:00
/** 解锁:解密 local 密文写入 session,口令错误抛错,返回明文与密钥 */
2026-09-14 13:45:17 +08:00
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);
}
2026-09-14 17:51:40 +08:00
/** 用给定密钥加密 environments 并写回 local */
2026-09-14 13:45:17 +08:00
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,
};
});