Files
PassKeeper/extension/background.js
T

202 lines
7.7 KiB
JavaScript

/**
* extension/background.js — Manifest V3 service worker.
*
* Responsibilities:
* - Update the action badge (number of matching vault items) for the active tab.
* - Bridge SAVE_CREDENTIALS messages from content script → chrome.storage.local.
* - Re-update badges when the vault cache changes.
* - Lock the vault automatically after IDLE_LOCK_SECONDS of system inactivity.
*/
// ── Idle lock ─────────────────────────────────────────────────────────────────
// Default: 10 minutes. User can change via the Account view in the popup.
const DEFAULT_IDLE_LOCK_SECONDS = 600;
const IDLE_TIMEOUT_KEY = 'idle_lock_seconds';
/** Apply the idle detection interval, reading the user's saved preference. */
async function applyIdleInterval() {
const stored = await chrome.storage.local.get(IDLE_TIMEOUT_KEY);
const seconds = stored[IDLE_TIMEOUT_KEY] ?? DEFAULT_IDLE_LOCK_SECONDS;
if (seconds === 0) {
// "Never" — unregister by setting to Chrome's maximum (the API requires a value).
chrome.idle.setDetectionInterval(3600);
} else {
chrome.idle.setDetectionInterval(Math.max(15, seconds));
}
console.log('[PassKeeper] Idle lock interval:', seconds === 0 ? 'never' : seconds + 's');
}
// Apply on every SW startup (SW can be killed and restarted at any time).
applyIdleInterval();
chrome.idle.onStateChanged.addListener(async (newState) => {
if (newState === 'idle' || newState === 'locked') {
// Check if the user set "Never" before locking.
const stored = await chrome.storage.local.get(IDLE_TIMEOUT_KEY);
if ((stored[IDLE_TIMEOUT_KEY] ?? DEFAULT_IDLE_LOCK_SECONDS) === 0) return;
console.log('[PassKeeper] System', newState, '— locking vault.');
await chrome.storage.session.clear();
await chrome.storage.local.remove('vault_items_cs');
const tabs = await chrome.tabs.query({});
tabs.forEach(tab => {
if (tab.id) chrome.action.setBadgeText({ text: '', tabId: tab.id });
});
}
});
// ── Badge helpers ────────────────────────────────────────────────────────────
async function updateBadgeForTab(tabId, url) {
try {
const { vault_items } = await chrome.storage.session.get('vault_items');
if (!vault_items?.length) {
chrome.action.setBadgeText({ text: '', tabId });
return;
}
let hostname;
try { hostname = new URL(url).hostname.replace(/^www\./, ''); }
catch { chrome.action.setBadgeText({ text: '', tabId }); return; }
const matches = vault_items.filter(item => {
if (item.item_type !== 'password' || !item.plain?.url) return false;
try {
const h = new URL(item.plain.url).hostname.replace(/^www\./, '');
return h === hostname || h.endsWith(`.${hostname}`) || hostname.endsWith(`.${h}`);
} catch { return false; }
});
if (matches.length > 0) {
chrome.action.setBadgeText({ text: String(matches.length), tabId });
chrome.action.setBadgeBackgroundColor({ color: '#1a73e8', tabId });
} else {
chrome.action.setBadgeText({ text: '', tabId });
}
} catch { /* tab may have closed */ }
}
async function refreshAllBadges() {
const tabs = await chrome.tabs.query({});
for (const tab of tabs) {
if (tab.url?.startsWith('http')) updateBadgeForTab(tab.id, tab.url);
}
}
// ── Tab events ───────────────────────────────────────────────────────────────
chrome.tabs.onActivated.addListener(async ({ tabId }) => {
try {
const tab = await chrome.tabs.get(tabId);
if (tab.url?.startsWith('http')) updateBadgeForTab(tabId, tab.url);
} catch { }
});
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && tab.url?.startsWith('http')) {
updateBadgeForTab(tabId, tab.url);
}
});
// ── Message handler ──────────────────────────────────────────────────────────
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
// Popup updated the idle lock timeout → re-apply immediately.
if (msg.type === 'SET_IDLE_TIMEOUT') {
applyIdleInterval();
sendResponse({ ok: true });
return false;
}
// Content script requests opening the vault tab (from suggestion dropdown).
if (msg.type === 'OPEN_VAULT') {
chrome.tabs.create({ url: 'https://pwkeeper.ngodanguyen.tech/vault' });
sendResponse({ ok: true });
return false;
}
// Content script requests navigating the popup to the generator view.
if (msg.type === 'OPEN_GENERATOR') {
chrome.storage.session.set({ popup_nav: 'generator' });
chrome.action.openPopup().catch(() => {
// openPopup() requires user gesture in some Chrome versions — fallback is a no-op.
});
sendResponse({ ok: true });
return false;
}
// No-op ping from popup — keeps the service worker alive while the browser
// is open so chrome.storage.session is not wiped between popup openings.
if (msg.type === 'KEEPALIVE') {
sendResponse({ ok: true });
return false;
}
// Popup signals vault cache was refreshed → re-check badges AND forward
// the decrypted items directly to all content scripts (avoids storage read).
if (msg.type === 'VAULT_UPDATED') {
refreshAllBadges();
const payload = { type: 'VAULT_UPDATED', vault_items: msg.vault_items || [] };
chrome.tabs.query({}, function (tabs) {
tabs.forEach(function (tab) {
if (tab.url && (tab.url.startsWith('http://') || tab.url.startsWith('https://'))) {
chrome.tabs.sendMessage(tab.id, payload).catch(function () { });
}
});
});
sendResponse({ ok: true });
return false;
}
// Content script detected credentials on form submit → store for save-prompt.
if (msg.type === 'SAVE_CREDENTIALS') {
// Store in local storage so the prompt survives service worker restarts
// and is guaranteed to be present when the user next opens the popup.
chrome.storage.local.set({ pending_save: msg.data });
sendResponse({ ok: true });
return false;
}
// Bridge: web app logged in → store tokens in extension storage.
if (msg.type === 'WEB_SESSION_SYNC') {
chrome.storage.session.set({
access_token: msg.access_token,
enc_key_salt: msg.enc_key_salt || '',
});
// Also persist enc_key_salt locally so the unlock-only view survives browser restarts
chrome.storage.local.set({
refresh_token: msg.refresh_token,
...(msg.enc_key_salt ? { enc_key_salt: msg.enc_key_salt } : {}),
});
sendResponse({ ok: true });
return false;
}
// Bridge: web app logged out → clear extension session.
if (msg.type === 'WEB_SESSION_CLEAR') {
chrome.storage.session.remove(['access_token', 'vault_key_jwk', 'vault_items', 'enc_key_salt']);
chrome.storage.local.remove('refresh_token');
sendResponse({ ok: true });
return false;
}
// Popup logged in via extension → inject session into open vault tabs.
if (msg.type === 'EXT_SESSION_SYNC') {
chrome.tabs.query({ url: 'https://pwkeeper.ngodanguyen.tech/*' }, tabs => {
tabs.forEach(tab => {
chrome.tabs.sendMessage(tab.id, {
type: 'INJECT_SESSION',
access_token: msg.access_token,
refresh_token: msg.refresh_token,
enc_key_salt: msg.enc_key_salt,
}).catch(() => { });
});
});
sendResponse({ ok: true });
return false;
}
return false;
});