diff --git a/CLAUDE.md b/CLAUDE.md index 8b2f677..384b62f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -272,6 +272,54 @@ Requires Chrome 111+. `content.js` `onChanged` listener watches `area === 'sessi ## Key Implementation Details +### `password_changed_at` — accurate password age tracking + +Stored as `plain.password_changed_at: ISO-8601 string` inside the encrypted `enc_data` blob. Server never sees it. + +**Written by:** +- `vault.js` `handleFormSubmit` — **create**: always set to `now`. **Edit**: only updated when the password field value actually changed vs. the decrypted existing item in `_items`. Unchanged password → existing `password_changed_at` preserved. No existing timestamp + unchanged → absent (dashboard falls back to `created_at`). +- `popup.js` `saveCredential` and `addItemToVault` — always set to `now` for new extension-saved credentials. +- `toggleFavorite` — spreads `{ ...item.plain, tags: newTags }`, automatically preserving the timestamp. + +**Read by:** +- `renderSecurityDashboard` — `ageRef = plain?.password_changed_at || created_at`; items with a recent `password_changed_at` are no longer falsely flagged as old even if the item itself is old. +- Backwards compatible: items without `password_changed_at` fall back to `created_at`. + +### Auto-lock on tab visibility change + +`_startWebIdleTracking()` now registers a `visibilitychange` listener in addition to the mouse/keyboard events. + +**Hidden:** clears the inactivity timer (user cannot be active on a hidden tab), records `_hiddenAt = Date.now()`. + +**Visible again:** compares elapsed hidden time against the idle timeout. +- Hidden `≥ timeout` → immediate lock (same `VaultSession.clear()` + `showUnlockOverlay()` + toast as inactivity lock). +- Hidden `< timeout` → clears `_hiddenAt`, resumes timer from zero. + +This catches screen-lock, minimize, and long tab switches. Short tab switches (< timeout) do not trigger a lock. + +### Emergency access stale snapshot detection + +`EmergencyAccess._enc_vault_is_legacy()` — parses `enc_vault` JSON server-side (no decryption) and returns `True` if any item has a `name` key but lacks `enc_name`. Exposed as `enc_vault_is_legacy` in `to_dict()`. + +`renderEmergencyGrants()` — `ready` grants now show three states: +- `enc_vault_is_legacy: true` → amber `⚠ Outdated snapshot` badge + **Re-provision** button +- `enc_vault_is_legacy: false` → secondary **Update Recovery Data** button +- `accepted` (no snapshot yet) → primary **Provide Recovery Data** button + +After re-provisioning, `enc_vault_is_legacy` returns `false` and the warning disappears. + +### Extension health badge + +`_runPopupHealthCheck()` in `popup.js` — fires after every `fetchAndDecryptVault()`. Computes weak/reused synchronously, sends a preliminary `HEALTH_UPDATE` to the background SW, then runs HIBP in parallel (`_popupCheckHibp`). Sends a final `HEALTH_UPDATE` with breach count. + +`background.js` / `background.firefox.js`: +- `HEALTH_UPDATE` handler: stores `{ breached, weak, reused }` in `chrome.storage.local` as `health_status`, calls `applyHealthBadge()`. +- `applyHealthBadge()`: on tabs with no match-count badge, shows red `⚠` (breach) or amber `⚠` (weak/reused). Never overwrites the blue match-count badge or the pending-save `!` badge. +- `CLEAR_SAVE_BADGE`: after clearing `!`, immediately re-applies health badge. +- Idle lock: removes `health_status` from local storage. + +**Badge priority:** pending-save `!` (red) > match count (blue) > health `⚠` (red/amber). + ### Vault health notifications (background checks) `runBackgroundHealthCheck()` fires after every `loadVault()` call — async, non-blocking. @@ -342,7 +390,7 @@ DELETE /api/webauthn/credentials/ → remove a passkey **`auth.js` login wiring:** "Sign in with Passkey" button on `login.html` calls `PasskeyAuth.loginWithPasskey(email)`. On success, stores tokens and navigates to `/vault` — unlock overlay fires if master password field was empty. -**Authenticator attachment:** currently `PLATFORM` (biometrics / device passkey). To support roaming authenticators (YubiKey, phone-as-key), remove or change `authenticator_attachment` in `register_begin`. +**Authenticator attachment:** `register_begin` accepts optional `attachment` in the POST body: `"platform"` (default — device biometrics) or `"cross-platform"` (roaming — YubiKey, phone QR, NFC). The settings UI exposes a `