05/18 Enhanced codes and functionalities 9 (extension)
This commit is contained in:
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user