185 lines
5.4 KiB
JavaScript
185 lines
5.4 KiB
JavaScript
/**
|
||
* 加密库:主口令 + 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,
|
||
};
|
||
});
|