This commit is contained in:
2026-09-17 22:56:02 +08:00
parent 9ccc9253d3
commit def731fc5a
11 changed files with 0 additions and 2773 deletions
-244
View File
@@ -1,244 +0,0 @@
"use strict";
importScripts("crypto-store.js");
let masterKey = null; // 解锁后持有,锁定或 SW 重启后清空
let autoLockTimer = null;
const AUTO_LOCK_MS = 10 * 60 * 1000; // 10 分钟无操作自动锁定
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(() => { });
}
});
// 旧版明文数据迁移:把 loginConfigs 转为 environments 明文结构
async function migrateLegacyVault() {
const data = await chrome.storage.local.get(["environments", "loginConfigs", "vaultMeta"]);
if (data.vaultMeta)
return;
if (Array.isArray(data.environments) && data.environments.length > 0)
return;
if (Array.isArray(data.loginConfigs) && data.loginConfigs.length > 0) {
const envs = [{ id: "env_legacy", name: "默认", configs: data.loginConfigs }];
await chrome.storage.local.set({ environments: envs });
await chrome.storage.local.remove("loginConfigs");
console.log("[AutoLogin] 旧版 loginConfigs 已迁移为明文 environments");
}
}
// 无 vault 明文模式:确保 session 有 environments(从 local 加载或创建预设)
async function ensurePlainEnvironments() {
if (await CryptoStore.hasVault())
return; // 加密模式不处理
const data = await chrome.storage.local.get("environments");
let envs = data.environments;
if (!Array.isArray(envs) || envs.length === 0) {
const PRESET = ["个人", "测试", "生产"];
envs = PRESET.map((name) => ({
id: "env_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8),
name,
configs: [],
}));
await chrome.storage.local.set({ environments: envs });
}
const session = await chrome.storage.session.get("environments");
if (!session.environments) {
await chrome.storage.session.set({
environments: envs,
activeEnvId: envs[0]?.id || null,
});
}
}
// SW 启动即初始化明文环境到 session,供 content script 自动登录读取
ensurePlainEnvironments().catch((err) => console.warn("[AutoLogin] 启动初始化失败:", err));
autoRestoreUnlock().catch((err) => console.warn("[AutoLogin] 自动恢复解锁失败:", err));
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();
await chrome.storage.session.remove("__autoPwd");
}
// SW 重启后自动恢复解锁:从 session 取已存密码解锁 vault
async function autoRestoreUnlock() {
try {
if (!(await CryptoStore.hasVault()))
return;
if (masterKey)
return;
const { __autoPwd } = await chrome.storage.session.get("__autoPwd");
if (!__autoPwd)
return;
const { key } = await CryptoStore.unlock(__autoPwd);
masterKey = key;
resetAutoLockTimer();
console.log("[AutoLogin] 自动恢复解锁成功");
}
catch (err) {
await chrome.storage.session.remove("__autoPwd").catch(() => { });
}
}
// session 中 environments 变化时用内存密钥重新加密写回 local
chrome.storage.onChanged.addListener(async (changes, areaName) => {
if (areaName !== "session")
return;
if (!changes.environments)
return;
const environments = changes.environments.newValue;
if (!environments)
return; // 锁定时被清除,无需持久化
try {
if (masterKey) {
await CryptoStore.persistEncrypted(environments, masterKey);
}
else {
// 无 vault 明文模式:明文回写 local
await chrome.storage.local.set({ environments });
}
}
catch (err) {
console.warn("[AutoLogin] 持久化失败:", err);
}
});
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
(async () => {
try {
switch (message.action) {
case "getVaultStatus": {
const hasVault = await CryptoStore.hasVault();
if (!hasVault) {
await ensurePlainEnvironments();
const local = await chrome.storage.local.get("environments");
const sess = await chrome.storage.session.get("environments");
const cfgCount = (arr) => (arr || []).reduce((s, e) => s + (e.configs?.length || 0), 0);
sendResponse({
hasVault: false, unlocked: true, pendingImport: false,
localEnvs: (local.environments || []).length, localCfgs: cfgCount(local.environments),
sessEnvs: (sess.environments || []).length, sessCfgs: cfgCount(sess.environments),
});
break;
}
if (!masterKey)
await autoRestoreUnlock();
sendResponse({ hasVault, unlocked: !!masterKey, pendingImport: false });
break;
}
case "loadAllConfigs": {
if (!(await CryptoStore.hasVault()))
await ensurePlainEnvironments();
const data = await chrome.storage.session.get("environments");
const environments = data.environments || [];
const all = [];
environments.forEach((env) => {
(env.configs || []).forEach((cfg) => all.push({ ...cfg, envId: env.id, envName: env.name }));
});
sendResponse({ configs: all });
break;
}
case "getDomainAttempts": {
const key = `${message.envId}::${message.domain}`;
const data = await chrome.storage.local.get("loginAttempts");
const attempts = data.loginAttempts || {};
sendResponse({ attempts: attempts[key] || { count: 0, lastAttempt: 0 } });
break;
}
case "incrementDomainAttempts": {
const key = `${message.envId}::${message.domain}`;
const data = await chrome.storage.local.get("loginAttempts");
const attempts = data.loginAttempts || {};
const cur = attempts[key] || { count: 0, lastAttempt: 0 };
cur.count += 1;
cur.lastAttempt = Date.now();
attempts[key] = cur;
await chrome.storage.local.set({ loginAttempts: attempts });
sendResponse({ attempts: cur });
break;
}
case "resetDomainAttempts": {
const key = `${message.envId}::${message.domain}`;
const data = await chrome.storage.local.get("loginAttempts");
const attempts = data.loginAttempts || {};
if (attempts[key]) {
delete attempts[key];
await chrome.storage.local.set({ loginAttempts: attempts });
}
sendResponse({ success: true });
break;
}
case "setupVault": {
// 设置口令:若已有明文 environments 则加密它,否则创建预设
const localData = await chrome.storage.local.get("environments");
let initialEnvs;
if (Array.isArray(localData.environments) && localData.environments.length > 0) {
initialEnvs = localData.environments;
}
else {
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.session.set({ __autoPwd: message.password });
await chrome.storage.local.remove("environments"); // 已加密,明文不再需要
resetAutoLockTimer();
sendResponse({ success: true });
break;
}
case "unlock": {
try {
const { key } = await CryptoStore.unlock(message.password);
masterKey = key;
await chrome.storage.session.set({ __autoPwd: message.password });
resetAutoLockTimer();
sendResponse({ success: true });
}
catch (err) {
sendResponse({ success: false, error: err.message });
}
break;
}
case "lock": {
await doLock();
sendResponse({ success: true });
break;
}
case "getActivity": {
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; // 异步响应
});
-548
View File
@@ -1,548 +0,0 @@
"use strict";
const t = (key, ...args) => chrome.i18n.getMessage(key, args) || key;
const _sendMessage = chrome.runtime.sendMessage;
// 2FA / 验证码检测:检测到则不自动提交,仅填充
function detectCaptcha() {
// reCAPTCHA
if (document.querySelector(".g-recaptcha, [data-sitekey], iframe[src*='recaptcha'], iframe[src*='google.com/recaptcha']")) {
return { type: "reCAPTCHA", reason: t("captchaTitleRe") };
}
// hCaptcha
if (document.querySelector(".h-captcha, iframe[src*='hcaptcha']")) {
return { type: "hCaptcha", reason: t("captchaTitleH") };
}
// 国内常见滑块/验证码容器
const sliderSelectors = [
"#slider", ".slider", ".nc_wrapper", ".nc_iconfont",
"#captcha", ".captcha", ".geetest", "#geetest",
"[id*='captcha']", "[class*='captcha']",
"canvas[id*='captcha']", "canvas[id*='verify']"
];
for (const sel of sliderSelectors) {
try {
const el = document.querySelector(sel);
if (el && el.offsetParent !== null) {
return { type: "slider/captcha", reason: t("captchaReasonSlider", sel) };
}
}
catch { /* selector 可能无效 */ }
}
// 明显的"验证码"文字输入框(带图形码)
const codeInputs = document.querySelectorAll("input[name*='captcha' i], input[name*='verify' i], input[name*='code' i], input[id*='captcha' i], input[id*='verify' i], input[placeholder*='验证码' i], input[placeholder*='验证' i]");
for (const input of Array.from(codeInputs)) {
if (input.offsetParent !== null && input.type !== "hidden") {
return { type: "captcha-input", reason: t("captchaReasonInput") };
}
}
return null;
}
function setNativeValue(el, value) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set || Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value")?.set;
if (nativeInputValueSetter) {
nativeInputValueSetter.call(el, value);
}
else {
el.value = value;
}
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
}
function deepQuerySelectorAll(selector, root) {
const result = [];
const stack = [root || document];
while (stack.length) {
const r = stack.pop();
result.push(...r.querySelectorAll(selector));
for (const el of r.querySelectorAll("*")) {
if (el.shadowRoot)
stack.push(el.shadowRoot);
}
}
return result;
}
function deepQuerySelector(selector, root) {
return deepQuerySelectorAll(selector, root)[0] || null;
}
function resolveElement(value, multiple) {
if (!value)
return null;
const trimmed = value.trim();
if (!trimmed.startsWith("<")) {
return multiple ? deepQuerySelectorAll(trimmed) : deepQuerySelector(trimmed);
}
const doc = new DOMParser().parseFromString(trimmed, "text/html");
const el = doc.body.firstElementChild;
if (!el)
return null;
const tag = el.tagName.toLowerCase();
const id = el.id;
const cls = Array.from(el.classList);
const type = el.getAttribute("type");
const nameAttr = el.getAttribute("name");
const candidates = [];
if (id)
candidates.push(`${tag}#${CSS.escape(id)}`);
if (cls.length)
candidates.push(`${tag}.${cls.map(CSS.escape).join(".")}`);
if (type)
candidates.push(`${tag}[type="${type}"]`);
if (nameAttr)
candidates.push(`${tag}[name="${CSS.escape(nameAttr)}"]`);
for (const c of cls)
candidates.push(`${tag}.${CSS.escape(c)}`);
candidates.push(tag);
for (const sel of candidates) {
if (multiple) {
const els = deepQuerySelectorAll(sel);
if (els.length)
return els;
}
else {
const found = deepQuerySelector(sel);
if (found)
return found;
}
}
return null;
}
function findUsernameField(config) {
if (config.usernameSelector) {
const el = resolveElement(config.usernameSelector);
if (el)
return el;
}
const passwordField = findPasswordField(config);
if (!passwordField)
return null;
const searchScope = passwordField.closest("form") || document;
const selectors = [
"input[type='text']",
"input[type='email']",
"input[type='tel']",
"input:not([type='password']):not([type='hidden']):not([type='submit']):not([type='button']):not([type='checkbox']):not([type='radio'])",
];
for (const sel of selectors) {
const inputs = deepQuerySelectorAll(sel, searchScope);
for (const input of inputs) {
if (input.offsetParent !== null || input.offsetWidth > 0) {
const name = (input.name || "").toLowerCase();
const id = (input.id || "").toLowerCase();
const placeholder = (input.placeholder || "").toLowerCase();
const autocomplete = (input.getAttribute("autocomplete") || "").toLowerCase();
if (name.includes("user") || name.includes("name") || name.includes("account") ||
name.includes("email") || name.includes("login") || name.includes("mail") ||
id.includes("user") || id.includes("name") || id.includes("account") ||
id.includes("email") || id.includes("login") ||
placeholder.includes("user") || placeholder.includes("name") ||
placeholder.includes("email") || placeholder.includes("account") ||
placeholder.includes("登录") || placeholder.includes("账号") ||
placeholder.includes("用户") || placeholder.includes("邮箱") ||
autocomplete.includes("username") || autocomplete.includes("email")) {
return input;
}
}
}
}
const allInputs = deepQuerySelectorAll("input[type='text'], input[type='email'], input:not([type='password']):not([type='hidden']):not([type='submit']):not([type='button']):not([type='checkbox']):not([type='radio'])", searchScope);
for (const input of allInputs) {
if (input.offsetParent !== null || input.offsetWidth > 0) {
return input;
}
}
return null;
}
function findPasswordField(config) {
if (config.passwordSelector) {
const el = resolveElement(config.passwordSelector);
if (el)
return el;
}
const passwordInputs = deepQuerySelectorAll("input[type='password']");
for (const input of passwordInputs) {
if (input.offsetParent !== null || input.offsetWidth > 0) {
return input;
}
}
if (passwordInputs.length > 0)
return passwordInputs[0];
const allInputs = deepQuerySelectorAll("input");
for (const input of allInputs) {
if (input.type === "password")
return input;
const sig = ((input.name || "") + " " + (input.id || "") + " " +
(input.placeholder || "") + " " + (input.getAttribute("autocomplete") || "")).toLowerCase();
if (sig.includes("password") || sig.includes("passwd") || sig.includes("pwd")) {
if (input.offsetParent !== null || input.offsetWidth > 0)
return input;
}
}
return null;
}
function findSubmitButton(config, passwordField) {
if (config.submitSelector) {
const els = resolveElement(config.submitSelector, true);
if (els && els.length === 1)
return els[0];
if (els && els.length > 1) {
for (const el of els) {
if (el.offsetParent === null && el.offsetWidth === 0)
continue;
const compact = (el.textContent || el.value || "").toLowerCase().replace(/\s/g, "");
if (compact.includes("登录") || compact.includes("login") || compact.includes("signin")) {
return el;
}
}
for (const el of els) {
if (el.offsetParent !== null || el.offsetWidth > 0)
return el;
}
}
}
const form = passwordField ? passwordField.closest("form") : null;
const searchScope = form || document;
const submitSelectors = [
"button[type='submit']",
"input[type='submit']",
"button[type='button']",
"input[type='button']",
];
for (const sel of submitSelectors) {
const btns = deepQuerySelectorAll(sel, searchScope);
for (const btn of btns) {
if (btn.offsetParent !== null || btn.offsetWidth > 0) {
const text = (btn.textContent || btn.value || "").toLowerCase().trim();
const compact = text.replace(/\s/g, "");
if (compact.includes("登录") || compact.includes("login") || compact.includes("login") ||
compact.includes("signin") || compact.includes("提交") || compact.includes("submit") ||
compact.includes("确定") || compact.includes("ok") || compact.includes("go")) {
return btn;
}
}
}
}
for (const sel of submitSelectors) {
const btn = deepQuerySelector(sel, searchScope);
if (btn && (btn.offsetParent !== null || btn.offsetWidth > 0)) {
return btn;
}
}
const allButtons = deepQuerySelectorAll("button, input[type='button'], a[role='button']", searchScope);
for (const btn of allButtons) {
if (btn.offsetParent !== null || btn.offsetWidth > 0) {
const text = (btn.textContent || btn.value || "").toLowerCase().trim();
const compact = text.replace(/\s/g, "");
if (compact.includes("登录") || compact.includes("login") || compact.includes("signin")) {
return btn;
}
}
}
return null;
}
function fillForm(config, canSubmit = true) {
const passwordField = findPasswordField(config);
if (!passwordField) {
console.log("[AutoLogin]", t("logNoPasswordField"));
return false;
}
const usernameField = findUsernameField(config);
if (usernameField) {
setNativeValue(usernameField, config.username);
console.log("[AutoLogin]", t("logFilledUsername"));
}
else {
console.log("[AutoLogin]", t("logNoUsernameField"));
}
setNativeValue(passwordField, config.password);
console.log("[AutoLogin]", t("logFilledPassword"));
// 检测到验证码则强制不自动提交,提示用户手动操作
const captcha = detectCaptcha();
let actualCanSubmit = canSubmit;
if (captcha && config.autoSubmit) {
actualCanSubmit = false;
console.warn("[AutoLogin]", t("logCaptchaSkip", captcha.reason));
showCaptchaToast(captcha.reason);
}
if (config.autoSubmit && actualCanSubmit) {
const submitBtn = findSubmitButton(config, passwordField);
if (submitBtn) {
// delayMs 给需要 JS 初始化的页面留出时间
const delay = Math.max(300, Number(config.delayMs) || 300);
setTimeout(() => {
console.log("[AutoLogin]", t("logAutoClick", String(delay)));
submitBtn.click();
}, delay);
return true;
}
if (passwordField.form) {
const delay = Math.max(300, Number(config.delayMs) || 300);
console.log("[AutoLogin]", t("logTryFormSubmit"));
setTimeout(() => {
passwordField.form.requestSubmit
? passwordField.form.requestSubmit()
: passwordField.form.submit();
}, delay);
return true;
}
console.log("[AutoLogin]", t("logNoSubmitAndForm"));
}
else if (config.autoSubmit && !actualCanSubmit && captcha) {
console.warn("[AutoLogin]", t("logCaptchaFilled"));
}
else if (config.autoSubmit && !canSubmit) {
console.warn("[AutoLogin]", t("logAttemptExceeded"));
}
else {
console.log("[AutoLogin]", t("logNoAutoSubmit"));
}
return true;
}
// 验证码提示浮窗,8 秒后自动消失
let captchaToastEl = null;
function showCaptchaToast(reason) {
if (captchaToastEl && captchaToastEl.parentNode) {
captchaToastEl.parentNode.removeChild(captchaToastEl);
}
const el = document.createElement("div");
el.style.cssText = `
position: fixed;
top: 16px;
right: 16px;
z-index: 2147483647;
background: #1a1a2e;
border: 1px solid #e94560;
border-radius: 8px;
padding: 10px 14px;
color: #e0e0e0;
font-size: 13px;
font-family: "Segoe UI","Microsoft YaHei",sans-serif;
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
max-width: 300px;
`;
el.innerHTML = `<div style="font-weight:600;color:#e94560;margin-bottom:4px;">${t("captchaToastTitle")}</div>` +
`<div style="font-size:11px;color:#a0a0b0;">${t("captchaToastBody", reason)}</div>`;
document.body.appendChild(el);
captchaToastEl = el;
setTimeout(() => {
if (el && el.parentNode)
el.parentNode.removeChild(el);
captchaToastEl = null;
}, 8000);
}
const MAX_LOGIN_ATTEMPTS = 3;
const ATTEMPT_COOLDOWN = 5 * 60 * 1000;
async function getDomainAttempts(envId, domain) {
const resp = await _sendMessage({ action: "getDomainAttempts", envId, domain }).catch(() => null);
return resp?.attempts || { count: 0, lastAttempt: 0 };
}
async function incrementDomainAttempts(envId, domain) {
const resp = await _sendMessage({ action: "incrementDomainAttempts", envId, domain }).catch(() => null);
return resp?.attempts || { count: 0, lastAttempt: 0 };
}
async function resetDomainAttempts(envId, domain) {
await _sendMessage({ action: "resetDomainAttempts", envId, domain }).catch(() => { });
}
async function loadAllConfigs() {
const resp = await _sendMessage({ action: "loadAllConfigs" }).catch(() => null);
return resp?.configs || [];
}
async function autoFillOnLoad() {
let allConfigs = await loadAllConfigs();
if (allConfigs.length === 0) {
// session 可能为空(明文模式 SW 重启后未初始化),触发 background 加载后重试
try {
await _sendMessage({ action: "getVaultStatus" });
allConfigs = await loadAllConfigs();
}
catch (e) {
console.warn("[AutoLogin] 初始化失败:", e.message || e);
return;
}
if (allConfigs.length === 0)
return;
}
// 每个配置有独立开关 enabled
const enabledConfigs = allConfigs.filter((c) => c.enabled !== false);
if (enabledConfigs.length === 0)
return;
const matched = enabledConfigs.filter((c) => isDomainMatch(window.location.href, c.domain));
if (matched.length === 0)
return;
if (matched.length === 1) {
await fillSingleConfig(matched[0]);
return;
}
// 多环境命中:弹浮窗让用户选择
console.log("[AutoLogin]", t("logMatchedMulti", String(matched.length)));
showEnvPicker(matched);
}
// SPA 表单可能晚于脚本加载,填不到时每 500ms 重试,30 秒超时
const SPA_FILL_TIMEOUT_MS = 30000;
const SPA_RETRY_INTERVAL_MS = 500;
async function fillSingleConfig(config) {
console.log("[AutoLogin]", t("logStartFillEnv", config.envName || t("defaultEnvName")));
// 超限后仅填充不自动提交
let canSubmit = config.autoSubmit;
if (config.autoSubmit) {
const attempts = await getDomainAttempts(config.envId, config.domain);
const now = Date.now();
if (now - attempts.lastAttempt > ATTEMPT_COOLDOWN) {
attempts.count = 0;
}
if (attempts.count >= MAX_LOGIN_ATTEMPTS) {
canSubmit = false;
console.warn("[AutoLogin] " + t("logAttemptExceededDetail", config.envName, config.domain, String(attempts.count), String(MAX_LOGIN_ATTEMPTS), String(Math.ceil(ATTEMPT_COOLDOWN / 60000))));
}
else {
await incrementDomainAttempts(config.envId, config.domain);
}
}
// SPA 重试填充,30 秒超时
const startTime = Date.now();
const tryFill = () => {
const elapsed = Date.now() - startTime;
if (elapsed > SPA_FILL_TIMEOUT_MS) {
const inputs = deepQuerySelectorAll("input");
const pwd = deepQuerySelectorAll("input[type='password']");
console.warn("[AutoLogin] 填充超时: input=" + inputs.length + " pwd=" + pwd.length + " form=" + document.querySelectorAll("form").length + " | " + inputs.slice(0, 6).map((i) => "type=" + (i.type || "?") + "/name=" + (i.name || i.id || "?")).join(", "));
return;
}
const filled = fillForm(config, canSubmit);
if (!filled) {
setTimeout(tryFill, SPA_RETRY_INTERVAL_MS);
}
};
tryFill();
}
// 多环境命中时在页面右上角插入选择浮窗
let envPickerEl = null;
function showEnvPicker(configs) {
if (envPickerEl && envPickerEl.parentNode) {
envPickerEl.parentNode.removeChild(envPickerEl);
envPickerEl = null;
}
const overlay = document.createElement("div");
overlay.id = "auto-login-env-picker";
overlay.style.cssText = `
position: fixed;
top: 16px;
right: 16px;
z-index: 2147483647;
background: #1a1a2e;
border: 1px solid #e94560;
border-radius: 10px;
padding: 14px 16px;
min-width: 240px;
max-width: 320px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
font-family: "Segoe UI","Microsoft YaHei",sans-serif;
color: #e0e0e0;
font-size: 13px;
`;
const title = document.createElement("div");
title.textContent = t("envPickerTitle");
title.style.cssText = "font-weight:600;color:#e94560;margin-bottom:10px;font-size:14px;";
overlay.appendChild(title);
const list = document.createElement("div");
list.style.cssText = "display:flex;flex-direction:column;gap:6px;";
configs.forEach((cfg) => {
const btn = document.createElement("button");
btn.style.cssText = `
text-align:left;
background:#16213e;
border:1px solid #0f3460;
border-radius:6px;
padding:8px 10px;
color:#e0e0e0;
cursor:pointer;
font-size:12px;
font-family:inherit;
transition:background 0.15s;
`;
const envLabel = escapeHtml(cfg.envName || t("defaultEnvName"));
const domainLabel = escapeHtml(cfg.alias || cfg.domain);
const userLabel = escapeHtml(cfg.username);
btn.innerHTML = `
<div style="font-weight:600;color:#e94560;">${envLabel}</div>
<div style="color:#888;font-size:11px;margin-top:2px;">${domainLabel} · ${userLabel}</div>
`;
btn.addEventListener("mouseenter", () => {
btn.style.background = "#0f3460";
});
btn.addEventListener("mouseleave", () => {
btn.style.background = "#16213e";
});
btn.addEventListener("click", async () => {
overlay.parentNode && overlay.parentNode.removeChild(overlay);
envPickerEl = null;
await fillSingleConfig(cfg);
});
list.appendChild(btn);
});
overlay.appendChild(list);
const closeBtn = document.createElement("button");
closeBtn.textContent = t("cancelBtn");
closeBtn.style.cssText = `
margin-top:10px;
width:100%;
padding:6px;
background:transparent;
border:1px solid #0f3460;
border-radius:6px;
color:#888;
cursor:pointer;
font-size:11px;
font-family:inherit;
`;
closeBtn.addEventListener("click", () => {
overlay.parentNode && overlay.parentNode.removeChild(overlay);
envPickerEl = null;
});
overlay.appendChild(closeBtn);
document.body.appendChild(overlay);
envPickerEl = overlay;
}
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.action === "manualFill" && message.config) {
resetDomainAttempts(message.config.envId, message.config.domain);
const filled = fillForm(message.config, true);
if (filled) {
sendResponse({ success: true });
}
else {
const inputs = deepQuerySelectorAll("input");
const pwdInputs = deepQuerySelectorAll("input[type='password']");
sendResponse({
success: false,
diag: {
total: inputs.length,
pwd: pwdInputs.length,
forms: document.querySelectorAll("form").length,
sample: inputs.slice(0, 10).map((i) => `type=${i.type || "?"}|name=${(i.name || i.id || i.placeholder || "?").slice(0, 20)}`),
},
});
}
}
else if (message.action === "manualFillMulti" && Array.isArray(message.configs)) {
showEnvPicker(message.configs);
sendResponse({ success: true });
}
return true;
});
autoFillOnLoad();
// URL 变化后若新页面不再匹配任何配置,重置之前匹配环境的失败计数
let lastUrl = window.location.href;
const observer = new MutationObserver(() => {
if (window.location.href !== lastUrl) {
const prevUrl = lastUrl;
lastUrl = window.location.href;
loadAllConfigs().then((allConfigs) => {
const stillMatched = allConfigs.some((c) => isDomainMatch(window.location.href, c.domain));
if (!stillMatched) {
const prevMatched = allConfigs.filter((c) => isDomainMatch(prevUrl, c.domain));
prevMatched.forEach((c) => resetDomainAttempts(c.envId, c.domain));
}
});
setTimeout(autoFillOnLoad, 1000);
}
});
observer.observe(document.body, { childList: true, subtree: true });
-47
View File
@@ -1,47 +0,0 @@
const enc = new TextEncoder();
const dec = new TextDecoder();
export function bufToBase64(buf) {
const bytes = new Uint8Array(buf);
let bin = "";
for (let i = 0; i < bytes.length; i++)
bin += String.fromCharCode(bytes[i]);
return btoa(bin);
}
export function base64ToBuf(b64) {
const bin = atob(b64);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++)
bytes[i] = bin.charCodeAt(i);
return bytes.buffer;
}
async function deriveKey(password, salt, iterations) {
const keyMaterial = await crypto.subtle.importKey("raw", enc.encode(password), { name: "PBKDF2" }, false, ["deriveKey"]);
return crypto.subtle.deriveKey({ name: "PBKDF2", salt, iterations, hash: "SHA-256" }, keyMaterial, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
}
export async function encryptConfigs(configs, password) {
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const iterations = 100000;
const key = await deriveKey(password, salt, iterations);
const plaintext = enc.encode(JSON.stringify(configs));
const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plaintext);
return {
algorithm: "AES-GCM",
iv: bufToBase64(iv),
ciphertext: bufToBase64(ciphertext),
kdf: {
algorithm: "PBKDF2",
iterations,
hash: "SHA-256",
salt: bufToBase64(salt)
}
};
}
export async function decryptConfigs(cipher, password) {
const salt = new Uint8Array(base64ToBuf(cipher.kdf.salt));
const iv = new Uint8Array(base64ToBuf(cipher.iv));
const iterations = cipher.kdf.iterations || 100000;
const key = await deriveKey(password, salt, iterations);
const plainBuf = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, base64ToBuf(cipher.ciphertext));
return JSON.parse(dec.decode(plainBuf));
}
-159
View File
@@ -1,159 +0,0 @@
"use strict";
/**
* 加密库:主口令 + 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,
};
});
-136
View File
@@ -1,136 +0,0 @@
"use strict";
/**
* 环境数据层:环境/配置的读写,数据存于 chrome.storage.session(解锁后明文)。
* 首次建口令由 crypto-store.setupVault 完成;加密回写由 background 监听完成。
*/
(function (root, factory) {
if (typeof module === "object" && module.exports) {
module.exports = factory({
getStorage: () => (typeof chrome !== "undefined" ? chrome.storage.session : null),
getLocalStorage: () => (typeof chrome !== "undefined" ? chrome.storage.local : null),
});
}
else {
const api = factory({
getStorage: () => chrome.storage.session,
getLocalStorage: () => chrome.storage.local,
});
root.EnvStore = api;
}
})(typeof globalThis !== "undefined" ? globalThis : this, function ({ getStorage, getLocalStorage }) {
const ENV_KEY = "environments";
const ACTIVE_ENV_KEY = "activeEnvId";
function genId() {
return "env_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
}
function genCfgId() {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
}
function storage() {
const s = getStorage();
if (!s)
throw new Error("chrome.storage.session 不可用");
return s;
}
async function loadEnvironments() {
const data = await storage().get([ENV_KEY, ACTIVE_ENV_KEY]);
const environments = data[ENV_KEY] || [];
const activeEnvId = data[ACTIVE_ENV_KEY] || environments[0]?.id || null;
return { environments, activeEnvId };
}
async function saveEnvironments(environments, activeEnvId) {
const patch = { [ENV_KEY]: environments };
if (activeEnvId !== undefined)
patch[ACTIVE_ENV_KEY] = activeEnvId;
await storage().set(patch);
}
async function getActiveEnv() {
const { environments, activeEnvId } = await loadEnvironments();
return environments.find((e) => e.id === activeEnvId) || environments[0] || null;
}
async function setActiveEnv(envId) {
await storage().set({ [ACTIVE_ENV_KEY]: envId });
}
const MAX_ENVIRONMENTS = 5;
async function createEnvironment(name) {
const { environments } = await loadEnvironments();
if (environments.length >= MAX_ENVIRONMENTS) {
throw new Error(`最多 ${MAX_ENVIRONMENTS} 个环境`);
}
const env = { id: genId(), name, configs: [] };
environments.push(env);
await saveEnvironments(environments, env.id);
return env;
}
async function renameEnvironment(envId, newName) {
const { environments } = await loadEnvironments();
const env = environments.find((e) => e.id === envId);
if (env) {
env.name = newName;
await saveEnvironments(environments);
}
return env;
}
async function deleteEnvironment(envId) {
const { environments, activeEnvId } = await loadEnvironments();
if (environments.length <= 1) {
throw new Error("至少保留一个环境");
}
const idx = environments.findIndex((e) => e.id === envId);
if (idx === -1)
return;
environments.splice(idx, 1);
const newActive = activeEnvId === envId ? environments[0].id : activeEnvId;
await saveEnvironments(environments, newActive ?? undefined);
// 失败计数存于 local(非敏感),删除环境时一并清理
const local = getLocalStorage();
if (local) {
const attemptsData = await local.get("loginAttempts");
const attempts = attemptsData.loginAttempts || {};
Object.keys(attempts).forEach((k) => {
if (k.startsWith(envId + "::"))
delete attempts[k];
});
await local.set({ loginAttempts: attempts });
}
}
async function loadConfigs() {
const env = await getActiveEnv();
return env?.configs || [];
}
async function saveConfigs(configs) {
const { environments, activeEnvId } = await loadEnvironments();
const env = environments.find((e) => e.id === activeEnvId);
if (!env)
throw new Error("未找到当前环境");
env.configs = configs;
await saveEnvironments(environments);
}
async function loadAllConfigs() {
const { environments } = await loadEnvironments();
const all = [];
environments.forEach((env) => {
(env.configs || []).forEach((cfg) => {
all.push({ ...cfg, envId: env.id, envName: env.name });
});
});
return all;
}
return {
ENV_KEY,
ACTIVE_ENV_KEY,
MAX_ENVIRONMENTS,
loadEnvironments,
saveEnvironments,
getActiveEnv,
setActiveEnv,
createEnvironment,
renameEnvironment,
deleteEnvironment,
loadConfigs,
saveConfigs,
loadAllConfigs,
genId,
genCfgId,
};
});
-41
View File
@@ -1,41 +0,0 @@
export function t(key, ...args) {
const msg = chrome.i18n.getMessage(key, args);
return msg || key;
}
export function applyI18n(root = document) {
root.querySelectorAll("[data-i18n]").forEach((el) => {
const key = el.dataset.i18n || "";
if (el.children.length === 0) {
const msg = chrome.i18n.getMessage(key);
if (msg)
el.textContent = msg;
}
else {
const msg = chrome.i18n.getMessage(key);
if (msg) {
const firstText = el.firstChild;
if (firstText && firstText.nodeType === Node.TEXT_NODE) {
firstText.nodeValue = msg;
}
}
}
});
root.querySelectorAll("[data-i18n-placeholder]").forEach((el) => {
const key = el.dataset.i18nPlaceholder || "";
const msg = chrome.i18n.getMessage(key);
if (msg)
el.placeholder = msg;
});
root.querySelectorAll("[data-i18n-title]").forEach((el) => {
const key = el.dataset.i18nTitle || "";
const msg = chrome.i18n.getMessage(key);
if (msg)
el.title = msg;
});
root.querySelectorAll("[data-i18n-aria]").forEach((el) => {
const key = el.dataset.i18nAria || "";
const msg = chrome.i18n.getMessage(key);
if (msg)
el.setAttribute("aria-label", msg);
});
}
-27
View File
@@ -1,27 +0,0 @@
import { t } from "./i18n.js";
export function sendMessage(message, retries = 3) {
return new Promise((resolve, reject) => {
let attempt = 0;
const trySend = () => {
attempt++;
if (!chrome.runtime?.id) {
location.reload();
reject(new Error(t("toastContextReload")));
return;
}
chrome.runtime.sendMessage(message, (resp) => {
const err = chrome.runtime.lastError;
if (err) {
if (attempt < retries && /Receiving end does not exist/i.test(err.message ?? "")) {
setTimeout(trySend, 200 * attempt);
return;
}
reject(new Error(err.message ?? ""));
return;
}
resolve(resp);
});
};
trySend();
});
}
-1400
View File
File diff suppressed because it is too large Load Diff
-67
View File
@@ -1,67 +0,0 @@
import { t } from "./i18n.js";
const ATTACK_RATE = 1e7;
export function estimateStrength(pwd) {
if (!pwd)
return { entropy: 0, seconds: 0, level: "empty", label: t("strengthEmpty") };
let pool = 0;
if (/[a-z]/.test(pwd))
pool += 26;
if (/[A-Z]/.test(pwd))
pool += 26;
if (/[0-9]/.test(pwd))
pool += 10;
if (/[^a-zA-Z0-9]/.test(pwd))
pool += 32;
if (pool === 0)
pool = 1;
const entropy = pwd.length * Math.log2(pool);
const combinations = Math.pow(2, entropy);
const seconds = combinations / 2 / ATTACK_RATE;
let level;
let label;
if (seconds < 1) {
level = "very-weak";
label = t("strengthVeryWeak");
}
else if (seconds < 3600) {
level = "weak";
label = t("strengthWeak");
}
else if (seconds < 86400 * 30) {
level = "medium";
label = t("strengthMedium");
}
else if (seconds < 86400 * 365 * 1000) {
level = "strong";
label = t("strengthStrong");
}
else {
level = "very-strong";
label = t("strengthVeryStrong");
}
return { entropy, seconds, level, label };
}
export function formatTime(seconds) {
if (seconds < 1)
return t("crackInstant");
if (seconds < 60)
return t("crackSeconds", String(Math.round(seconds)));
if (seconds < 3600)
return t("crackMinutes", String(Math.round(seconds / 60)));
if (seconds < 86400)
return t("crackHours", String(Math.round(seconds / 3600)));
if (seconds < 86400 * 30)
return t("crackDays", String(Math.round(seconds / 86400)));
if (seconds < 86400 * 365)
return t("crackMonths", String(Math.round(seconds / 86400 / 30)));
const years = seconds / 86400 / 365;
if (years < 1000)
return t("crackYears", String(Math.round(years)));
if (years < 1e6)
return t("crackThousandYears", String(Math.round(years / 1000)));
if (years < 1e9)
return t("crackMillionYears", String(Math.round(years / 1e6)));
if (years < 1e12)
return t("crackBillionYears", String(Math.round(years / 1e9)));
return t("crackTrillionYears", (years / 1e12).toExponential(2));
}
-24
View File
@@ -1,24 +0,0 @@
export function openConfigUrl(domain) {
if (!domain)
return;
const url = /^https?:\/\//i.test(domain) ? domain : "http://" + domain;
window.open(url, "_blank", "noopener");
}
export async function fetchTitleByDomain(domain) {
try {
const base = /^https?:\/\//i.test(domain) ? domain : "*://" + domain;
const patterns = [base];
if (!base.endsWith("*"))
patterns.push(base + "/*");
const tabs = await chrome.tabs.query({ url: patterns });
for (const tab of tabs) {
if (tab.title)
return tab.title;
}
}
catch (e) { }
return null;
}
export function closeAllMenus() {
document.querySelectorAll(".config-menu.open").forEach((m) => m.classList.remove("open"));
}
-80
View File
@@ -1,80 +0,0 @@
"use strict";
// URL 匹配 + HTML 转义,浏览器与 Node 通用
(function (root, factory) {
if (typeof module === "object" && module.exports) {
module.exports = factory();
}
else {
const api = factory();
root.isDomainMatch = api.isDomainMatch;
root.escapeHtml = api.escapeHtml;
}
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
/**
* 判断 URL 是否匹配规则。
* 纯 hostname:example.com / *.example.com
* hostname+path:example.com/login/*,路径部分支持 * 通配
*/
function isDomainMatch(url, pattern) {
if (!url || !pattern)
return false;
try {
const u = new URL(url);
const rawPattern = String(pattern).trim().replace(/^https?:\/\//i, "");
if (rawPattern.includes("/")) {
const slashIdx = rawPattern.indexOf("/");
const hostPart = rawPattern.slice(0, slashIdx);
const pathPart = rawPattern.slice(slashIdx + 1) || "";
const hostForMatch = hostPart.includes(":") ? u.host : u.hostname;
if (!matchHost(hostForMatch, hostPart))
return false;
return matchPath(u.pathname, pathPart);
}
const hostForMatch = rawPattern.includes(":") ? u.host : u.hostname;
return matchHost(hostForMatch, rawPattern);
}
catch {
return false;
}
}
function matchHost(hostname, hostPart) {
if (hostPart.startsWith("*.")) {
const suffix = hostPart.slice(2);
return hostname === suffix || hostname.endsWith("." + suffix);
}
if (hostPart.includes("*")) {
const regexStr = hostPart.replace(/\./g, "\\.").replace(/\*/g, ".*");
return new RegExp("^" + regexStr + "$").test(hostname);
}
return hostname === hostPart;
}
function matchPath(pathname, pathPart) {
const pagePath = pathname.startsWith("/") ? pathname.slice(1) : pathname;
if (!pathPart)
return true;
if (pathPart === "*")
return true;
if (pathPart.endsWith("/*")) {
const prefix = pathPart.slice(0, -2);
return pagePath === prefix || pagePath.startsWith(prefix + "/");
}
if (pathPart.includes("*")) {
const regexStr = pathPart.replace(/\//g, "\\/").replace(/\*/g, ".*");
return new RegExp("^" + regexStr + "$").test(pagePath);
}
return pagePath === pathPart;
}
/** HTML 转义,防止配置中的特殊字符破坏页面 */
function escapeHtml(str) {
if (str == null)
return "";
return String(str)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
return { isDomainMatch, escapeHtml };
});