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,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|