05/18 Enhanced codes and functionalities 9 (extension)

This commit is contained in:
2026-05-18 21:13:43 -04:00
parent e8b386b2c8
commit 304881a6c2
3 changed files with 212 additions and 5 deletions
+45 -1
View File
@@ -66,6 +66,7 @@ async function resetIdleTimer() {
console.log('[PassKeeper] Idle lock triggered after', seconds, 's');
await chrome.storage.session.clear();
await chrome.storage.local.remove('vault_items_cs');
await chrome.storage.local.remove(HEALTH_STATUS_KEY);
const tabs = await chrome.tabs.query({});
tabs.forEach((tab) => {
if (tab.id) chrome.browserAction.setBadgeText({ text: '', tabId: tab.id });
@@ -77,6 +78,33 @@ resetIdleTimer();
// ── Badge helpers ─────────────────────────────────────────────────────────────
const HEALTH_STATUS_KEY = 'health_status';
async function applyHealthBadge() {
try {
const stored = await chrome.storage.local.get(HEALTH_STATUS_KEY);
const health = stored[HEALTH_STATUS_KEY] || { breached: 0, weak: 0, reused: 0 };
const hasIssues = (health.breached || 0) + (health.weak || 0) + (health.reused || 0) > 0;
const tabs = await chrome.tabs.query({});
for (const tab of tabs) {
if (!tab.url?.startsWith('http')) continue;
const current = await new Promise((res) =>
chrome.browserAction.getBadgeText({ tabId: tab.id }, res)
).catch(() => '');
if (current && current !== '' && current !== '⚠') continue;
if (hasIssues) {
const color = (health.breached || 0) > 0 ? '#c0392b' : '#d97706';
chrome.browserAction.setBadgeText({ text: '⚠', tabId: tab.id });
chrome.browserAction.setBadgeBackgroundColor({ color, tabId: tab.id });
} else {
chrome.browserAction.setBadgeText({ text: '', tabId: tab.id });
}
}
} catch (err) {
console.warn('[PassKeeper] applyHealthBadge error:', err);
}
}
async function updateBadgeForTab(tabId, url) {
try {
const { vault_items } = await chrome.storage.session.get('vault_items');
@@ -99,7 +127,16 @@ async function updateBadgeForTab(tabId, url) {
chrome.browserAction.setBadgeText({ text: String(matches.length), tabId });
chrome.browserAction.setBadgeBackgroundColor({ color: '#1a73e8', tabId });
} else {
chrome.browserAction.setBadgeText({ text: '', tabId });
const stored = await chrome.storage.local.get(HEALTH_STATUS_KEY);
const health = stored[HEALTH_STATUS_KEY] || {};
const hasIssues = (health.breached || 0) + (health.weak || 0) + (health.reused || 0) > 0;
if (hasIssues) {
const color = (health.breached || 0) > 0 ? '#c0392b' : '#d97706';
chrome.browserAction.setBadgeText({ text: '⚠', tabId });
chrome.browserAction.setBadgeBackgroundColor({ color, tabId });
} else {
chrome.browserAction.setBadgeText({ text: '', tabId });
}
}
} catch { /* tab may have closed */ }
}
@@ -167,8 +204,15 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
chrome.browserAction.setBadgeBackgroundColor({ color: '#c0392b' });
sendResponse({ ok: true }); return false;
}
if (msg.type === 'HEALTH_UPDATE') {
const { breached = 0, weak = 0, reused = 0 } = msg;
chrome.storage.local.set({ [HEALTH_STATUS_KEY]: { breached, weak, reused } });
applyHealthBadge();
sendResponse({ ok: true }); return false;
}
if (msg.type === 'CLEAR_SAVE_BADGE') {
chrome.browserAction.setBadgeText({ text: '' });
applyHealthBadge();
sendResponse({ ok: true }); return false;
}
if (msg.type === 'WEB_SESSION_SYNC') {
+58 -1
View File
@@ -64,6 +64,8 @@ chrome.idle.onStateChanged.addListener(async (newState) => {
console.log("[PassKeeper] System", newState, "— locking vault.");
// session.clear() also removes vault_items_cs (moved from local to session storage).
await chrome.storage.session.clear();
// Clear health status so stale warnings don't persist after lock.
await chrome.storage.local.remove(HEALTH_STATUS_KEY);
const tabs = await chrome.tabs.query({});
tabs.forEach((tab) => {
if (tab.id) chrome.action.setBadgeText({ text: "", tabId: tab.id });
@@ -73,6 +75,40 @@ chrome.idle.onStateChanged.addListener(async (newState) => {
// ── Badge helpers ────────────────────────────────────────────────────────────
const HEALTH_STATUS_KEY = 'health_status';
/**
* Read the stored health status and apply a global warning badge on tabs that
* currently have no match-count badge. Called after vault updates, after
* clearing the pending-save badge, and when the health status changes.
* Priority: pending-save "!" > per-tab match count (blue) > health warning (amber).
*/
async function applyHealthBadge() {
try {
const stored = await chrome.storage.local.get(HEALTH_STATUS_KEY);
const health = stored[HEALTH_STATUS_KEY] || { breached: 0, weak: 0, reused: 0 };
const hasIssues = (health.breached || 0) + (health.weak || 0) + (health.reused || 0) > 0;
const tabs = await chrome.tabs.query({});
for (const tab of tabs) {
if (!tab.url?.startsWith('http')) continue;
// Don't overwrite the per-tab match-count badge.
const current = await chrome.action.getBadgeText({ tabId: tab.id }).catch(() => '');
if (current && current !== '' && current !== '⚠') continue;
if (hasIssues) {
const label = health.breached > 0 ? '⚠' : '⚠';
const color = health.breached > 0 ? '#c0392b' : '#d97706';
chrome.action.setBadgeText({ text: label, tabId: tab.id });
chrome.action.setBadgeBackgroundColor({ color, tabId: tab.id });
} else {
chrome.action.setBadgeText({ text: '', tabId: tab.id });
}
}
} catch (err) {
console.warn('[PassKeeper] applyHealthBadge error:', err);
}
}
async function updateBadgeForTab(tabId, url) {
try {
const { vault_items } = await chrome.storage.session.get("vault_items");
@@ -107,7 +143,17 @@ async function updateBadgeForTab(tabId, url) {
chrome.action.setBadgeText({ text: String(matches.length), tabId });
chrome.action.setBadgeBackgroundColor({ color: "#1a73e8", tabId });
} else {
chrome.action.setBadgeText({ text: "", tabId });
// No match for this tab — show health warning badge if applicable.
const stored = await chrome.storage.local.get(HEALTH_STATUS_KEY);
const health = stored[HEALTH_STATUS_KEY] || {};
const hasIssues = (health.breached || 0) + (health.weak || 0) + (health.reused || 0) > 0;
if (hasIssues) {
const color = (health.breached || 0) > 0 ? "#c0392b" : "#d97706";
chrome.action.setBadgeText({ text: "⚠", tabId });
chrome.action.setBadgeBackgroundColor({ color, tabId });
} else {
chrome.action.setBadgeText({ text: "", tabId });
}
}
} catch {
/* tab may have closed */
@@ -213,9 +259,20 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
return false;
}
// Popup reports vault health check results → store and update badge.
if (msg.type === "HEALTH_UPDATE") {
const { breached = 0, weak = 0, reused = 0 } = msg;
chrome.storage.local.set({ [HEALTH_STATUS_KEY]: { breached, weak, reused } });
applyHealthBadge();
sendResponse({ ok: true });
return false;
}
// Clear the pending-save badge once the popup signals it has handled the prompt.
// Re-apply the health warning badge afterwards if applicable.
if (msg.type === "CLEAR_SAVE_BADGE") {
chrome.action.setBadgeText({ text: "" });
applyHealthBadge();
sendResponse({ ok: true });
return false;
}
+106
View File
@@ -617,6 +617,11 @@ async function fetchAndDecryptVault() {
vault_items: itemsForContentScript,
})
.catch(() => {});
// Run health checks in the background — sync metrics first, then HIBP.
// Results are sent to the background SW via HEALTH_UPDATE so the toolbar
// badge reflects the vault health even when the popup is closed.
_runPopupHealthCheck();
} catch (err) {
console.error("fetchAndDecryptVault:", err);
} finally {
@@ -624,6 +629,107 @@ async function fetchAndDecryptVault() {
}
}
// ── Vault health check ────────────────────────────────────────────────────────
/**
* Compute weak/reused counts synchronously, then run HIBP in parallel.
* Sends HEALTH_UPDATE to the background service worker so the toolbar
* badge is updated even after the popup closes.
* Called once per fetchAndDecryptVault() cycle.
*/
async function _runPopupHealthCheck() {
try {
const pwItems = _items.filter(
(i) => i.item_type === "password" && i.plain?.password,
);
if (!pwItems.length) {
chrome.runtime
.sendMessage({ type: "HEALTH_UPDATE", breached: 0, weak: 0, reused: 0 })
.catch(() => {});
return;
}
// Synchronous metrics.
const weak = pwItems.filter((i) => {
const p = i.plain.password;
if (p.length < 10) return true;
return (
[/[A-Z]/, /[a-z]/, /[0-9]/, /[^A-Za-z0-9]/].filter((r) => r.test(p))
.length < 2
);
});
const passCounts = {};
pwItems.forEach((i) => {
const p = i.plain.password;
(passCounts[p] = passCounts[p] || []).push(i);
});
const reused = Object.values(passCounts)
.filter((a) => a.length > 1)
.flat();
// Send sync results immediately so background badge updates quickly.
chrome.runtime
.sendMessage({
type: "HEALTH_UPDATE",
breached: 0,
weak: weak.length,
reused: reused.length,
})
.catch(() => {});
// HIBP — k-anonymity, parallel.
const hibpResults = await Promise.all(
pwItems.map(async (item) => ({
item,
count: await _popupCheckHibp(item.plain.password),
})),
);
const breachedCount = hibpResults.filter((r) => r.count > 0).length;
// Send final results with breach count.
chrome.runtime
.sendMessage({
type: "HEALTH_UPDATE",
breached: breachedCount,
weak: weak.length,
reused: reused.length,
})
.catch(() => {});
} catch (err) {
console.error("[PassKeeper] Popup health check failed:", err);
}
}
/**
* HIBP k-anonymity check — identical to the web app's checkHibp().
* Only the first 5 hex chars of SHA-1(password) are sent; the full
* hash never leaves the browser.
*/
async function _popupCheckHibp(password) {
try {
const enc = new TextEncoder().encode(password);
const hashBuf = await crypto.subtle.digest("SHA-1", enc);
const hashHex = Array.from(new Uint8Array(hashBuf))
.map((b) => b.toString(16).padStart(2, "0"))
.join("")
.toUpperCase();
const prefix = hashHex.slice(0, 5);
const suffix = hashHex.slice(5);
const res = await fetch(
`https://api.pwnedpasswords.com/range/${prefix}`,
{ headers: { "Add-Padding": "true" } },
);
if (!res.ok) return 0;
const text = await res.text();
const line = text.split("\n").find((l) => l.startsWith(suffix));
if (!line) return 0;
return parseInt(line.split(":")[1], 10) || 0;
} catch {
return 0;
}
}
// ── TOTP engine (RFC 6238) — pure Web Crypto, no library ─────────────────────
function _extractTotpSecret(uri) {