From df8628e71c742067d0b436a3e64df03f5ab39627 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Mon, 20 Apr 2026 16:45:09 -0400 Subject: [PATCH] 04/20/2026 enhance the extension --- extension/background.js | 142 +++++++++++++++++++--------------------- extension/manifest.json | 44 ++++++++----- 2 files changed, 96 insertions(+), 90 deletions(-) diff --git a/extension/background.js b/extension/background.js index 6c3a7f1..2cb0c2b 100644 --- a/extension/background.js +++ b/extension/background.js @@ -3,58 +3,69 @@ * * Responsibilities: * - Update the action badge (number of matching vault items) for the active tab. - * - Bridge SAVE_CREDENTIALS messages from content script → chrome.storage.session - * so the popup can pick them up as a save-prompt. + * - 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 ───────────────────────────────────────────────────────────────── + +// Lock after 10 minutes of system idle or when the screen is locked. +const IDLE_LOCK_SECONDS = 600; + +chrome.idle.setDetectionInterval(IDLE_LOCK_SECONDS); + +chrome.idle.onStateChanged.addListener(async (newState) => { + if (newState === 'idle' || newState === 'locked') { + console.log('[PassKeeper] System', newState, '— locking vault.'); + // Clear the session (vault key, access token, vault items) so the popup + // requires master password re-entry on next open. + await chrome.storage.session.clear(); + // Clear the content-script vault cache so suggestions stop showing. + await chrome.storage.local.remove('vault_items_cs'); + // Clear all badge text — vault is now locked. + 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"); + const { vault_items } = await chrome.storage.session.get('vault_items'); if (!vault_items?.length) { - chrome.action.setBadgeText({ text: "", tabId }); + chrome.action.setBadgeText({ text: '', tabId }); return; } let hostname; - try { - hostname = new URL(url).hostname.replace(/^www\./, ""); - } catch { - chrome.action.setBadgeText({ text: "", tabId }); - return; - } + 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; + 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; - } + 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 }); + chrome.action.setBadgeBackgroundColor({ color: '#1a73e8', tabId }); } else { - chrome.action.setBadgeText({ text: "", tabId }); + chrome.action.setBadgeText({ text: '', tabId }); } - } catch { - /* tab may have closed */ - } + } 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); + if (tab.url?.startsWith('http')) updateBadgeForTab(tab.id, tab.url); } } @@ -63,12 +74,12 @@ async function refreshAllBadges() { chrome.tabs.onActivated.addListener(async ({ tabId }) => { try { const tab = await chrome.tabs.get(tabId); - if (tab.url?.startsWith("http")) updateBadgeForTab(tabId, tab.url); - } catch {} + 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")) { + if (changeInfo.status === 'complete' && tab.url?.startsWith('http')) { updateBadgeForTab(tabId, tab.url); } }); @@ -76,16 +87,17 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { // ── Message handler ────────────────────────────────────────────────────────── chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + // Content script requests opening the vault tab (from suggestion dropdown). - if (msg.type === "OPEN_VAULT") { - chrome.tabs.create({ url: "https://pwkeeper.ngodanguyen.tech/vault" }); + 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" }); + 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. }); @@ -95,26 +107,20 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { // 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") { + 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") { + if (msg.type === 'VAULT_UPDATED') { refreshAllBadges(); - const payload = { - type: "VAULT_UPDATED", - vault_items: msg.vault_items || [], - }; + 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 () {}); + if (tab.url && (tab.url.startsWith('http://') || tab.url.startsWith('https://'))) { + chrome.tabs.sendMessage(tab.id, payload).catch(function () { }); } }); }); @@ -123,7 +129,7 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { } // Content script detected credentials on form submit → store for save-prompt. - if (msg.type === "SAVE_CREDENTIALS") { + 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 }); @@ -132,10 +138,10 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { } // Bridge: web app logged in → store tokens in extension storage. - if (msg.type === "WEB_SESSION_SYNC") { + if (msg.type === 'WEB_SESSION_SYNC') { chrome.storage.session.set({ access_token: msg.access_token, - enc_key_salt: msg.enc_key_salt || "", + 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({ @@ -147,38 +153,28 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { } // 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"); + 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(() => {}); - }); - }, - ); + 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; -}); +}); \ No newline at end of file diff --git a/extension/manifest.json b/extension/manifest.json index da58bc3..74e1e45 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -3,42 +3,52 @@ "name": "PassKeeper", "version": "1.0.0", "description": "Autofill and manage your PassKeeper vault", - - "permissions": ["storage", "activeTab", "tabs"], - "host_permissions": [ - "https://pwkeeper.ngodanguyen.tech/*" + "permissions": [ + "storage", + "activeTab", + "tabs", + "idle" + ], + "host_permissions": [ + "http://*/*", + "https://*/*" ], - "background": { "service_worker": "background.js" }, - "action": { "default_popup": "popup/popup.html", "default_title": "PassKeeper", "default_icon": { - "16": "icons/icon16.png", - "48": "icons/icon48.png", + "16": "icons/icon16.png", + "48": "icons/icon48.png", "128": "icons/icon128.png" } }, - "content_scripts": [ { - "matches": ["http://*/*", "https://*/*"], - "js": ["content/content.js"], + "matches": [ + "http://*/*", + "https://*/*" + ], + "js": [ + "content/content.js" + ], "run_at": "document_idle" }, { - "matches": ["https://pwkeeper.ngodanguyen.tech/*"], - "js": ["bridge/bridge.js"], + "matches": [ + "https://pwkeeper.ngodanguyen.tech/*" + ], + "js": [ + "bridge/bridge.js" + ], "run_at": "document_idle" } ], - "icons": { - "16": "icons/icon16.png", - "48": "icons/icon48.png", + "16": "icons/icon16.png", + "48": "icons/icon48.png", "128": "icons/icon128.png" } -} +} \ No newline at end of file