174 lines
5.8 KiB
JavaScript
174 lines
5.8 KiB
JavaScript
// background.js —— Service Worker,负责:
|
||
// 1. Side Panel 打开行为
|
||
// 2. 口令锁状态管理(解锁后内存持有 AES 密钥)
|
||
// 3. session 数据变化时自动加密回写 local
|
||
// 4. 闲置自动锁定
|
||
// 5. 旧版明文库迁移到加密库
|
||
|
||
importScripts("crypto-store.js");
|
||
|
||
// 内存中的派生密钥,解锁后才有值,锁定/SW 重启后清空
|
||
let masterKey = null;
|
||
// 闲置自动锁定计时器
|
||
let autoLockTimer = null;
|
||
const AUTO_LOCK_MS = 10 * 60 * 1000; // 10 分钟无操作自动锁定
|
||
|
||
// 全局启用 Side Panel
|
||
chrome.runtime.onInstalled.addListener(() => {
|
||
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true })
|
||
.catch((err) => console.warn("[AutoLogin] 设置 sidePanel 行为失败:", err));
|
||
// 安装/升级时迁移旧版明文库
|
||
migrateLegacyVault();
|
||
console.log("[AutoLogin] 扩展已安装");
|
||
});
|
||
|
||
chrome.action.onClicked.addListener((tab) => {
|
||
if (tab?.id !== undefined) {
|
||
chrome.sidePanel.open({ tabId: tab.id }).catch(() => {});
|
||
}
|
||
});
|
||
|
||
/**
|
||
* 旧版迁移:如果 local 中存在明文 environments 但没有 vaultMeta,
|
||
* 则提示用户需要先设置口令(无法在无口令下加密)。
|
||
* 这里把明文数据暂存,等用户设置口令时由 setupVault 导入。
|
||
*/
|
||
async function migrateLegacyVault() {
|
||
const data = await chrome.storage.local.get(["environments", "loginConfigs", "vaultMeta"]);
|
||
const hasVault = !!data.vaultMeta;
|
||
if (hasVault) return;
|
||
|
||
// 有旧版明文数据:迁移为待导入状态
|
||
const legacyEnvs = data.environments;
|
||
const legacyConfigs = data.loginConfigs;
|
||
if (legacyEnvs || legacyConfigs) {
|
||
let pending = null;
|
||
if (Array.isArray(legacyEnvs) && legacyEnvs.length > 0) {
|
||
pending = { type: "environments", data: legacyEnvs };
|
||
} else if (Array.isArray(legacyConfigs) && legacyConfigs.length > 0) {
|
||
pending = {
|
||
type: "configs",
|
||
data: [{ id: "env_legacy", name: "默认", configs: legacyConfigs }],
|
||
};
|
||
}
|
||
if (pending) {
|
||
await chrome.storage.local.set({ pendingImport: pending });
|
||
// 清掉明文,避免继续以明文存在
|
||
await chrome.storage.local.remove(["environments", "loginConfigs"]);
|
||
console.log("[AutoLogin] 旧版明文数据已标记为待导入,等待设置口令后加密");
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 重置闲置自动锁定计时器 */
|
||
function resetAutoLockTimer() {
|
||
if (autoLockTimer) clearTimeout(autoLockTimer);
|
||
if (masterKey) {
|
||
autoLockTimer = setTimeout(() => {
|
||
console.log("[AutoLogin] 闲置超时,自动锁定");
|
||
doLock();
|
||
}, AUTO_LOCK_MS);
|
||
}
|
||
}
|
||
|
||
async function doLock() {
|
||
masterKey = null;
|
||
if (autoLockTimer) { clearTimeout(autoLockTimer); autoLockTimer = null; }
|
||
await CryptoStore.lock();
|
||
}
|
||
|
||
/**
|
||
* 监听 session 中 environments 变化:用内存密钥重新加密写回 local。
|
||
* 这是"加密透明化"的核心——EnvStore 只管 session,加密由这里自动完成。
|
||
*/
|
||
chrome.storage.onChanged.addListener(async (changes, areaName) => {
|
||
if (areaName !== "session") return;
|
||
if (!changes.environments || !masterKey) return;
|
||
try {
|
||
const environments = changes.environments.newValue;
|
||
if (!environments) return; // 锁定时被清除,无需加密
|
||
await CryptoStore.persistEncrypted(environments, masterKey);
|
||
} catch (err) {
|
||
console.warn("[AutoLogin] 加密回写失败:", err);
|
||
}
|
||
});
|
||
|
||
// 消息处理:来自 popup / content script
|
||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||
(async () => {
|
||
try {
|
||
switch (message.action) {
|
||
case "getVaultStatus": {
|
||
const hasVault = await CryptoStore.hasVault();
|
||
const unlocked = hasVault ? !!masterKey : false;
|
||
const pending = (await chrome.storage.local.get("pendingImport")).pendingImport || null;
|
||
sendResponse({ hasVault, unlocked, pendingImport: !!pending });
|
||
break;
|
||
}
|
||
|
||
case "setupVault": {
|
||
// 首次设置口令。如果有待导入的旧数据,一并加密。
|
||
const pendingData = await chrome.storage.local.get("pendingImport");
|
||
let initialEnvs = [];
|
||
if (pendingData.pendingImport) {
|
||
initialEnvs = pendingData.pendingImport.data;
|
||
} else {
|
||
// 预置 3 个环境
|
||
const PRESET = ["个人", "测试", "生产"];
|
||
initialEnvs = PRESET.map((name) => ({
|
||
id: "env_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8),
|
||
name,
|
||
configs: [],
|
||
}));
|
||
}
|
||
masterKey = await CryptoStore.setupVault(message.password, initialEnvs);
|
||
await chrome.storage.local.remove("pendingImport");
|
||
resetAutoLockTimer();
|
||
sendResponse({ success: true });
|
||
break;
|
||
}
|
||
|
||
case "unlock": {
|
||
try {
|
||
const { key } = await CryptoStore.unlock(message.password);
|
||
masterKey = key;
|
||
resetAutoLockTimer();
|
||
sendResponse({ success: true });
|
||
} catch (err) {
|
||
sendResponse({ success: false, error: err.message });
|
||
}
|
||
break;
|
||
}
|
||
|
||
case "lock": {
|
||
await doLock();
|
||
sendResponse({ success: true });
|
||
break;
|
||
}
|
||
|
||
case "getActivity": {
|
||
// 供 popup 显示闲置倒计时
|
||
sendResponse({
|
||
unlocked: !!masterKey,
|
||
autoLockMs: AUTO_LOCK_MS,
|
||
});
|
||
break;
|
||
}
|
||
|
||
case "ping": {
|
||
// 任何消息都算用户活动,重置自动锁定计时
|
||
resetAutoLockTimer();
|
||
sendResponse({ pong: true });
|
||
break;
|
||
}
|
||
|
||
default:
|
||
sendResponse({ error: "unknown action" });
|
||
}
|
||
} catch (err) {
|
||
sendResponse({ success: false, error: err.message });
|
||
}
|
||
})();
|
||
return true; // 异步响应
|
||
});
|