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');
|
console.log('[PassKeeper] Idle lock triggered after', seconds, 's');
|
||||||
await chrome.storage.session.clear();
|
await chrome.storage.session.clear();
|
||||||
await chrome.storage.local.remove('vault_items_cs');
|
await chrome.storage.local.remove('vault_items_cs');
|
||||||
|
await chrome.storage.local.remove(HEALTH_STATUS_KEY);
|
||||||
const tabs = await chrome.tabs.query({});
|
const tabs = await chrome.tabs.query({});
|
||||||
tabs.forEach((tab) => {
|
tabs.forEach((tab) => {
|
||||||
if (tab.id) chrome.browserAction.setBadgeText({ text: '', tabId: tab.id });
|
if (tab.id) chrome.browserAction.setBadgeText({ text: '', tabId: tab.id });
|
||||||
@@ -77,6 +78,33 @@ resetIdleTimer();
|
|||||||
|
|
||||||
// ── Badge helpers ─────────────────────────────────────────────────────────────
|
// ── 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) {
|
async function updateBadgeForTab(tabId, url) {
|
||||||
try {
|
try {
|
||||||
const { vault_items } = await chrome.storage.session.get('vault_items');
|
const { vault_items } = await chrome.storage.session.get('vault_items');
|
||||||
@@ -98,9 +126,18 @@ async function updateBadgeForTab(tabId, url) {
|
|||||||
if (matches.length > 0) {
|
if (matches.length > 0) {
|
||||||
chrome.browserAction.setBadgeText({ text: String(matches.length), tabId });
|
chrome.browserAction.setBadgeText({ text: String(matches.length), tabId });
|
||||||
chrome.browserAction.setBadgeBackgroundColor({ color: '#1a73e8', tabId });
|
chrome.browserAction.setBadgeBackgroundColor({ color: '#1a73e8', tabId });
|
||||||
|
} else {
|
||||||
|
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 {
|
} else {
|
||||||
chrome.browserAction.setBadgeText({ text: '', tabId });
|
chrome.browserAction.setBadgeText({ text: '', tabId });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch { /* tab may have closed */ }
|
} catch { /* tab may have closed */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,8 +204,15 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
chrome.browserAction.setBadgeBackgroundColor({ color: '#c0392b' });
|
chrome.browserAction.setBadgeBackgroundColor({ color: '#c0392b' });
|
||||||
sendResponse({ ok: true }); return false;
|
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') {
|
if (msg.type === 'CLEAR_SAVE_BADGE') {
|
||||||
chrome.browserAction.setBadgeText({ text: '' });
|
chrome.browserAction.setBadgeText({ text: '' });
|
||||||
|
applyHealthBadge();
|
||||||
sendResponse({ ok: true }); return false;
|
sendResponse({ ok: true }); return false;
|
||||||
}
|
}
|
||||||
if (msg.type === 'WEB_SESSION_SYNC') {
|
if (msg.type === 'WEB_SESSION_SYNC') {
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ chrome.idle.onStateChanged.addListener(async (newState) => {
|
|||||||
console.log("[PassKeeper] System", newState, "— locking vault.");
|
console.log("[PassKeeper] System", newState, "— locking vault.");
|
||||||
// session.clear() also removes vault_items_cs (moved from local to session storage).
|
// session.clear() also removes vault_items_cs (moved from local to session storage).
|
||||||
await chrome.storage.session.clear();
|
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({});
|
const tabs = await chrome.tabs.query({});
|
||||||
tabs.forEach((tab) => {
|
tabs.forEach((tab) => {
|
||||||
if (tab.id) chrome.action.setBadgeText({ text: "", tabId: tab.id });
|
if (tab.id) chrome.action.setBadgeText({ text: "", tabId: tab.id });
|
||||||
@@ -73,6 +75,40 @@ chrome.idle.onStateChanged.addListener(async (newState) => {
|
|||||||
|
|
||||||
// ── Badge helpers ────────────────────────────────────────────────────────────
|
// ── 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) {
|
async function updateBadgeForTab(tabId, url) {
|
||||||
try {
|
try {
|
||||||
const { vault_items } = await chrome.storage.session.get("vault_items");
|
const { vault_items } = await chrome.storage.session.get("vault_items");
|
||||||
@@ -106,9 +142,19 @@ async function updateBadgeForTab(tabId, url) {
|
|||||||
if (matches.length > 0) {
|
if (matches.length > 0) {
|
||||||
chrome.action.setBadgeText({ text: String(matches.length), tabId });
|
chrome.action.setBadgeText({ text: String(matches.length), tabId });
|
||||||
chrome.action.setBadgeBackgroundColor({ color: "#1a73e8", tabId });
|
chrome.action.setBadgeBackgroundColor({ color: "#1a73e8", tabId });
|
||||||
|
} else {
|
||||||
|
// 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 {
|
} else {
|
||||||
chrome.action.setBadgeText({ text: "", tabId });
|
chrome.action.setBadgeText({ text: "", tabId });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* tab may have closed */
|
/* tab may have closed */
|
||||||
}
|
}
|
||||||
@@ -213,9 +259,20 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
return false;
|
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.
|
// 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") {
|
if (msg.type === "CLEAR_SAVE_BADGE") {
|
||||||
chrome.action.setBadgeText({ text: "" });
|
chrome.action.setBadgeText({ text: "" });
|
||||||
|
applyHealthBadge();
|
||||||
sendResponse({ ok: true });
|
sendResponse({ ok: true });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -617,6 +617,11 @@ async function fetchAndDecryptVault() {
|
|||||||
vault_items: itemsForContentScript,
|
vault_items: itemsForContentScript,
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.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) {
|
} catch (err) {
|
||||||
console.error("fetchAndDecryptVault:", err);
|
console.error("fetchAndDecryptVault:", err);
|
||||||
} finally {
|
} 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 ─────────────────────
|
// ── TOTP engine (RFC 6238) — pure Web Crypto, no library ─────────────────────
|
||||||
|
|
||||||
function _extractTotpSecret(uri) {
|
function _extractTotpSecret(uri) {
|
||||||
|
|||||||
Reference in New Issue
Block a user