diff --git a/CLAUDE.md b/CLAUDE.md index 83623e2..8b2f677 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -272,6 +272,26 @@ Requires Chrome 111+. `content.js` `onChanged` listener watches `area === 'sessi ## Key Implementation Details +### Vault health notifications (background checks) + +`runBackgroundHealthCheck()` fires after every `loadVault()` call — async, non-blocking. + +**Flow:** +1. Computes weak + reused counts synchronously from `_items` → updates sidebar badge instantly +2. Runs HIBP k-anonymity checks in parallel (`checkHibp`) → updates badge + banner when done + +**Module state:** +- `_healthCache` — `{ weak, reused, breached, breachedItems, hibpResults }` — set after first run +- `_hibpRunning` — boolean guard prevents concurrent runs + +**`_updateHealthUI({ weak, reused, breached })`** — renders: +- **Sidebar badge** (`#security-badge`) on the 🛡️ Security link: red for breaches, amber for weak/reused only, hidden when clean +- **Dismissible banner** (`#health-banner`) above the vault list: summarises issues with "View report" link → Security tab; dismiss hides for the session (`banner.dataset.dismissed = "1"`); resets on next vault load + +**Security tab caching:** `renderSecurityDashboard` checks `_healthCache?.hibpResults` before querying HIBP — avoids double-calling the API within the same session. + +**Banner reset:** `banner.dataset.dismissed` is set to `"0"` on each vault reload so fresh results (e.g. after a password change) are visible again. + ### WebAuthn / Passkey **Library:** `py-webauthn` (`webauthn>=2.0`). Installed via `pip install webauthn`. @@ -594,6 +614,8 @@ webauthn>=2.0 # Passkey / WebAuthn (py-webauthn) ## Notes - Never log decrypted vault data server-side — audit details use `item_type` + `id` only +- Background health check runs after every vault load — badge + banner update without user action +- HIBP results cached in `_healthCache` per session — Security tab reuses them, no double-query - Tags live in `enc_data` as `plain.tags: string[]` — no schema change ever needed - `vault_items_cs` is in `chrome.storage.session` — decrypted data never written to disk - HIBP checks run progressively — synchronous sections render first, then parallel async checks diff --git a/README.md b/README.md index fca6f94..24ad638 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ A self-hosted, zero-knowledge password manager — web app and Chrome/Firefox ex - **Item sharing** — ECDH P-256 zero-knowledge re-encryption; item name encrypted with shared key — server never sees it - **Emergency access** — configurable wait-timer access grant for a trusted contact - **Security dashboard** — weak / reused / old / **no 2FA saved** / **HaveIBeenPwned breach check** (k-anonymity — passwords never transmitted) +- **Proactive health notifications** — sidebar badge and dismissible banner on vault load when breached, weak, or reused passwords are detected; HIBP runs in the background without blocking the UI; results cached so opening the Security tab is instant - **Import / Export** — encrypted JSON backup; CSV export (plaintext, handle carefully); import from Chrome, Bitwarden, and 1Password CSV formats (RFC 4180 compliant parser) - **Account MFA** — TOTP-based login (Google Authenticator / Authy); single-use code enforcement prevents replay attacks - **Passkeys / WebAuthn** — register device biometrics or hardware keys as a sign-in method; master password still required to unlock vault (zero-knowledge preserved); manage passkeys in Account Settings diff --git a/app/static/js/vault.js b/app/static/js/vault.js index b1778ff..0dd04e7 100644 --- a/app/static/js/vault.js +++ b/app/static/js/vault.js @@ -1513,15 +1513,23 @@ const Vault = (() => { } ul.innerHTML = shares .map( - (s) => ` + (s) => { + // Resolve the display name: prefer the decrypted name from _items + // (owner's vault is already in memory), fall back to item_name which + // is now the item_type label for new shares, or a legacy plaintext + // name for shares created before the enc_name migration. + const vaultItem = _items.find((i) => i.id === s.item_id); + const displayName = vaultItem?.name || s.item_name; + return `
  • ${itemIcon(s.item_type)}
    - +
    -
  • `, + `; + }, ) .join("");