Commit Graph

34 Commits

Author SHA1 Message Date
r-zakarya b00da43ab0 feat: PIN unlock + table column picker + edit-position chooser + UX
- PIN unlock: device-local 4-12 digit shortcut, DPAPI-wrapped vault
  key. Three modes (state.unlockMode): pw / pin / pw+pin. PIN
  derives a wrap key via PBKDF2(pin, salt, 100k) and unwraps the
  stored vault key (mirrors the Quick Unlock blob shape).
  Anti-brute-force: 5 wrong attempts wipes the blob. Setup gated by
  master-pw reauth so an unattended unlocked laptop can't be
  backdoored. Master pw rotation clears the PIN blob (key drift).
  loadServerSettings post-sync demotes pin/both -> pw when the local
  blob is missing, so a wiped device re-syncs the correct mode up.
  New unit PM.PinUnlock.pas + cmd://pin/{store,get,clear,status}.
- Table column picker: ⚙ in topbar (table view only), checkbox menu
  for Site/Username/Folder/Updated. Site also drives showSiteOnCards
  so the existing "Show site / URL" toggle in Settings stays in
  sync. NAME column auto-widths (180px min, content max, +32px
  right padding) so column hugs the next one without truncating.
- Editor position chooser (Appearance setting): Slide-over right /
  left / Centered modal. Scoped to #slideover + #settingsPanel so
  the click-outside / pointer-events logic doesn't accidentally
  trap the modal-style empty viewport.
- Confirm before discarding unsaved edits: state.confirmOnUnsaved
  setting (default ON), prompts on X / Esc / click-outside / switch-
  to-other-entry. Also gates Lock vault / Sign out actions when the
  editor is dirty; auto-lock and system-lock paths bypass to avoid
  blocking on an unattended machine.
- Open-in-browser button added to the actions cell of the table
  view (was card-only).
- Entry templates pass folder customization + template id through
  duplicate / export / import / auto-backup roundtrips.
- Folder color + icon now persisted across export/import: payload.
  folders carries name/color/icon; import creates missing folders
  additively (existing local customisation kept).
- Bulk move-to-folder, batch add-tag, single add-tag now re-ship
  the full entry payload so partial PUTs don't silently wipe
  TOTP / custom_fields / kind / template.
- FireDAC: switched ftString -> ftMemo for icon_b64 / custom_fields
  / TOTP / template params and replaced .AsString with .Value so a
  large (~200 KB) DeepSeek favicon no longer gets truncated at the
  default ANSI 4000-char cap.
- Unicode filenames: attachment INSERT now uses ftWideString +
  .AsWideString so non-ANSI filenames round-trip instead of being
  mangled to "?".
- HandleSetEntryIcon cap raised 256 KB -> 512 KB chars to accept
  base64 data URIs produced by max-raw favicon fetches.
- promptDialog + askReauth support inline `error` line + retry-
  with-count loops on doExport reauth and auto-backup password
  setup (5 attempts cap before bailing).
- Recently used moved from Tools to Vault section in the sidebar.
- Auth screen passkey button hidden (Delphi backend stubs WebAuthn).
- Sensitive cmd://favicon/refresh-style buttons in Settings now
  stopPropagation so the document-level "close panel" handler
  doesn't dismiss Settings mid-async during DOM reparenting.
- TEST_PLAN.md: +PIN unlock section.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-29 04:41:39 +01:00
r-zakarya e23a78dda7 feat: entry templates + tag autocomplete + slideover push + robustness bundle
- Entry templates: new vault_entries.template column drives a typed
  sub-kind ('credit-card', 'ssh-key', 'server', 'recovery-codes'). Card
  + table label off the template, badge reads "credit card" instead of
  "note". Templates seed kind=note (no site/password required), use
  custom_fields with optional dropdown options (brand, month/year,
  protocol). Round-tripped across export/import/duplicate/master-pw
  rotation, preserved by partial PUTs via a HasTemplate flag.
- Custom fields: support per-field `options[]` rendering as <select>
  (card brand, expiry MM/YYYY, SSH/server protocol).
- Tags: existing-tag autocomplete dropdown under the chip input,
  filtered against what's already selected.
- Search history: per-query X for individual delete + 1s debounced
  commit (no Enter required).
- Slideover: clicking outside closes again (drag-selection respected
  via mousedown origin tracker), Esc closes, X closes. App shell is
  pushed left by 420px when the panel is open so the table / pagination
  / sort / search stay visible and interactive.
- Export/import: JSON now round-trips custom_fields, attachments
  (decrypted to base64, re-encrypted under current key on restore),
  icon_b64, and template. CSV warning lists what's not included.
- Auto-backup: same payload shape as user-driven export.
- Notes: import (JSON + CSV) accepts kind=note with empty site,
  preserves title/template/custom_fields. CSV parser detects kind/
  template columns.
- Bulk-import response returns `ids[]` parallel to input so the
  client can map back to new entry IDs (drives attachment restore).
- Move-to-folder bugs fixed: moveEntryToFolder, batchMoveToFolder,
  addTag, batchAddTag were all silently wiping TOTP / custom_fields
  / kind / template via partial PUT. Now re-ship full payload.
- Master-pw rotation: server mints a fresh session token + csrf so
  the very next request after rotation no longer ESessionRejects.
  Client adopts the new pair. Attachments are re-encrypted client-side
  during rotation (GET old → decrypt with old key → encrypt with new
  → PUT). New endpoints: GET /attachments/all, PUT /attachments/:id.
- Duplicate: carries icon_b64 + template + attachments to the copy.
- HandleCreateEntry: accepts icon_b64.
- FireDAC param fix: all blob/icon/custom_fields params use ftMemo +
  .Value assignment so SQLite TEXT no longer truncates to 4000 chars
  (deepseek's 200+ KB favicon was being wiped on lock/unlock).
- HandleSetEntryIcon cap: 262144 → 524288 chars (base64 of a 256 KB
  raw fetch overflows the old cap, fails silently in saveEntryIcon).
- Native save dialog: surfaces server errors instead of swallowing.
- Modals: reauth (export) + backup-password prompt support inline
  error display, retry up to 5 attempts, then hard-stop.
- Keyboard cursor (j/k): bootstraps to current page, auto-paginates
  when the cursor crosses a page boundary, Enter opens slideover.
- Slideover focuses Title on edit-open so j/k → Enter → type Just
  Works.
- TOTP tool: Esc closes the modal.
- App version + launch mode (auto/manual): exposed via bridge,
  surfaced in Settings → Account. Autostart launches suppress the
  first-time tray balloon.
- Passkey button hidden (Delphi backend stubs WebAuthn at 501).
- TEST_PLAN.md captured for regression coverage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-26 21:20:07 +01:00
Zaki fa7ea191be feat: native save + auto-backup + folder customization + attachments + UX bundle
- File: native Save As dialog via Bridge.saveFile (replaces WebView2
  browser download popup) for encrypted JSON + CSV exports.
- Auto-backup: silent periodic encrypted JSON to a chosen folder,
  user-set interval + retention, separate DPAPI-stored password, runs
  5s after unlock if due. New file/* bridge cmds (folder/pick,
  file/write, file/listMatch, file/delete).
- Folders: per-folder color + icon (8-swatch palette, 8 icon presets),
  drag-reorder via HTML5 DnD with insert-line indicators, edit pencil
  on hover. New POST /folders/reorder + PUT /folders/{name}. Folder
  chip on cards inherits custom icon + color.
- Recently used: vault_entries.accessed_at + POST /entries/{id}/touch
  (debounced 2s), sidebar Tools entry showing top-10 by accessed_at.
- Encrypted attachments: per-entry file storage (5MB cap), AES-GCM
  with vault key, native Save As download, paperclip upload in
  slideover. New entry_attachments table + PM.Handler.Attachments.
- Password expiry: vault_entries.password_changed_at (conditional bump
  via SQL CASE only when ciphertext differs), passwordExpiryDays
  setting, "Aged" badge on cards + matching Filters chip.
- Recovery: Print button on generated code modal (A4 printable sheet
  via @media print, code in 32px monospace + instructions).
- Audit log viewer (sidebar Tools, GET /audit with pagination cursor).
- Plaintext CSV export + Filters dropdown with 9 predicates.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-21 23:12:06 +01:00
Zaki 63fac5b3b7 feat: secure notes + password history + custom fields + quick-win bundle
Big feature trio
- Secure notes (kind='login'|'note') reusing the encrypted_password+iv
  pipeline for the body. New sidebar entry, slideover variant (title +
  multiline body), distinct card / table-view rendering, badge in name
  column, copy-content button replacing the password copy on note rows.
- Password history: entries_password_history table keeps up to 20 prior
  ciphertexts per entry. HandleUpdateEntry pushes the pre-update
  encrypted_password into history ONLY when it actually differs from
  the incoming one (JS reuses originalEncrypted bit-for-bit when the
  plaintext is unchanged — avoids spamming history on title/folder edits).
  GET /entries/{id}/history endpoint. Slideover modal lists versions
  with mask/reveal/copy/revert. Master-pw rotation wipes history (old
  ciphertext can't be decrypted with the new key).
- Custom fields: per-entry encrypted JSON array of {label, value,
  is_secret}. Same crypto pipeline as the password. Slideover row UI
  with label/value inputs, secret toggle (eye), copy, delete. Re-
  encryption flows through bulk-import, change-master-password, and
  duplicate.

Quick wins
- Cheatsheet overlay (press '?' or topbar button or Ctrl+K). Lists all
  hotkeys + global / tray / card actions. SVG icons inline so the
  cheatsheet matches the actual app glyphs (no emoji mismatch).
- Open URL button on entry cards: ShellExecute via cmd://app/open-url,
  http(s) only, validates entry.site looks like a real hostname.
- Trash auto-purge: setting "Empty trash after N days" (never/7/30/90).
  DELETE /entries/trash/old?days=N called at every unlock.

Favicon strategy
- Subdomains (chat.deepseek.com, app.X.com…) now try the SLD first
  (deepseek.com.ico) before the full host. DDG often returns a generic
  placeholder for subdomains that passes the byte threshold; the SLD-first
  switch surfaces the real brand icon.
- Cap bumped 64 KB → 256 KB on all three sides (Delphi fetch, server
  endpoint, JS upload). DDG sometimes serves the full-res asset.

UX polish
- Click-outside-slideover: stopPropagation everywhere it bites. Custom
  fields buttons (add / delete / secret toggle / copy / eye) all stop
  the click bubble so the document-level "close on outside click" handler
  doesn't fire when rerender() detaches the target from the DOM.
- Native search-cancel button restyled: cyan accent X via mask-image,
  cursor: pointer, breathing room before the Ctrl+K kbd chip.
- Password history modal: scrollable body, multiline wrapped passwords,
  hover border highlight.
- Cheatsheet panel widened (560 → 720 px) so the descriptions no longer
  ellipsis-clip.
- "+ New" topbar splits into a small dropdown: New login / New note.
- Notes show a "note" badge in table-view name column, italic
  "Encrypted note" placeholder in the username column.

Internals
- duplicateEntry copies kind + custom_fields too (one-line forgotten
  earlier).
- entries_password_history dropped on master-pw rotation — the old
  ciphertexts are unrecoverable with the new key.
- bulk-import re-encryption path includes custom_fields.

CLAUDE.md
- "Entry payload — call sites à toucher ensemble" lists the 6 spots
  to update when adding a new (en)crypted field. Notes the historical
  miss of kind in duplicateEntry and custom_fields in the rotation +
  duplicate.

Repo hygiene
- .gitattributes forces CRLF on Delphi sources (RAD Studio refuses LF).
  text=auto for web frontend / docs, binary for .res / .exe / images.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 20:17:19 +01:00
Zaki f047fba9a3 feat: tray quick-search + privacy hardening + race fixes
Quick search from tray
- New "Quick search…" entry in the tray context menu (between Open
  and Lock vault).
- Compact modal with live-filtered top-8 entries, arrow keys / Enter
  to copy the password (Shift+Enter copies the username instead),
  Esc to dismiss. Each row shows the favicon when cached.
- Locked vault → focus the master password input instead of opening
  the modal (same pattern as the locked-autofill-hotkey path).
- Window-state restore: Delphi remembers whether the window was
  hidden before the menu was opened and tells JS via the
  Bridge.openQuickSearch(wasHidden) arg. After the copy (or cancel)
  we hide back to the tray so the previously-foreground app comes
  back and Ctrl+V drops the password in.

Tray notifications toggle
- New Settings → Security "Show tray notifications" toggle. Gates
  Shell_NotifyIcon NIF_INFO balloons (currently only the "still
  running in the tray" first-time popup). Default ON, synced via
  settings_json so it follows the user across devices.
- PM.Bridge.ShowNotifications exposed as a public property; JS
  pushes the value on every settings sync.

Privacy: WebView2 phone-home killed
- WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS set in the unit
  initialization section (before the TMS WebBrowser instantiates
  its CoreWebView2Environment). Disables: background networking,
  sync, component updates, breakpad/crashpad, domain reliability,
  client-side phishing detection, experiments, UMA upload,
  MediaRouter, OptimizationHints, SafeBrowsing enhanced, autofill
  server, privacy sandbox APIs. Verified via Resource Monitor: only
  127.0.0.1 connections remain (plus DDG when favicons are on).

Fixes
- Blank-window-on-launch race: the 1.5 s navigation timer assumes
  WebView2 finishes init in time, but on slow machines Edge
  Chromium needs 2-3 s and the Navigate() call is silently
  dropped. WebBrowserInitialized now also navigates if a URL is
  still pending — first to run wins.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 19:35:37 +01:00
Zaki 2ef636ce30 feat: unified entry slideover + custom icons + UX fixes
Unified create/edit slideover
- openSlideOver(id) now accepts null for new entries. Same UI
  (icon, name, site, user, password, TOTP, folder, tags) for both
  create and edit. Drops the separate entry modal — no more "save
  first, then add TOTP" two-step.
- "+ New" button, Ctrl+K → New entry, and Ctrl+Shift+A all route
  through the slideover. Ctrl+Shift+A pre-fills the title with the
  foreground window's name.
- Save button visible from the start in new mode (no dirty wait).
- Title shows mode unambiguously: cyan "+ New entry" vs
  "Edit · <name>".

Custom icon upload (soIconField)
- 56×56 preview at the top of every slideover + Upload icon /
  Remove buttons. Same POST /entries/{id}/icon endpoint as the
  auto-fetch path. Validates type / size (64 KB cap matching server).
- Solves the case where DDG doesn't index a domain (self-hosted
  apps, private sites): the user pastes any image and it sticks.

Favicon: privacy-first, DDG only
- Removed the direct-fetch fallback steps (3-5). Privacy stance:
  zero DNS leak outside icons.duckduckgo.com. Domains DDG doesn't
  cover stay icon-less until the user uploads a custom one.
- PM.Favicon.FetchFaviconDataUri takes an optional TFaviconLog
  callback so UMainForm can stream per-step trace into LogLine for
  diagnostics.

Fixes
- Slideover z-index 30 → 50. The topbar's backdrop-filter creates a
  stacking context at z-index 40 which was clipping the slideover
  header (title + close button hidden behind topbar).
- RestoreFromTray no longer un-maximises a maximised window when
  called outside a tray-restore context (Ctrl+Shift+A, Ctrl+Shift+L
  picker, app/focus cmd). SW_RESTORE on a maximised window reverts
  to normal — now we only SW_RESTORE if IsIconic.
- "Show all"/"Show less" per-category state survives renderGrid
  re-renders (healthExpanded map).
- "+ New" and dashboard "Fix" buttons stopPropagation so the
  document-level click-outside handler doesn't close the slideover
  they just opened.
- soDirtyCheck keeps Save visible while in new mode regardless of
  diff.
- openSlideover → openSlideOver typo fix across all call sites.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 00:15:20 +01:00
Zaki ad5fb21a18 feat: website favicons + vault health dashboard
Favicons
- PM.Favicon (new): THTTPClient/WinHTTP proxy to icons.duckduckgo.com.
  Native Windows TLS — no OpenSSL DLLs to ship (Indy would fail
  silently without them). 5 s timeout, max 3 redirects, 64 KB cap,
  magic-byte MIME sniffing.
- DB: vault_entries.icon_b64 TEXT (idempotent migration).
- Endpoints: POST /entries/{id}/icon stores a cached data URI without
  forcing a full PUT (which would re-encrypt the password). DELETE
  /entries/icons/all purges the cache.
- Bridge cmd://favicon/fetch?host=X&reqId=Y runs in an anonymous thread
  so the up-to-5 s HTTP GET doesn't block the main thread; result
  shipped back via Bridge.onFaviconResult(reqId, host, dataUri).
- Hostname validated on both sides (JS faviconHost + Delphi
  NormalizeHost) so brand labels like "Gitea" never leak upstream.
- Settings: opt-in "Fetch website icons" toggle (synced), three explicit
  actions (Fetch missing / Re-fetch all / Clear cache) that bypass the
  toggle — manual user actions always work.
- Entry card avatar shows <img> when cached, falls back to initials.
  onerror handler recovers silently from a corrupt data URI.

Vault health
- New sidebar Tools → "Vault health" view. Four category cards:
  Weak (strength < 50), Reused (same plaintext on ≥ 2 entries), Old
  (updated_at > 365d), Pwned (HIBP cache).
- Score 0-100 with colour band (Good/Fair/At risk/Critical).
- One-shot computation cached per session (healthCache), invalidated
  on lockVault, entry save, and the explicit "Recompute" button.
- "Fix" button on each item opens the slideover for the affected
  entry, unmasks the password, focuses it, and pulses the dice button
  — full context preserved, user decides how to fix.
- Click handler stopPropagation prevents the document-level
  "click outside slideover" listener from closing the panel that
  we just opened in the same click event.

Fixes
- openSlideover typo (lowercase O) → openSlideOver across all call
  sites. Was silently breaking the Authenticator card click and the
  Vault health Fix button.
- W1050 WideChar warning in PM.Favicon — replaced set-membership
  with explicit Ord-style range comparisons.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 00:05:54 +01:00
Zaki 40b3154a34 feat: MFA tools, single-instance, tray polish, prefs persistence
Session highlights:

- feat(prefs): DPAPI-backed key/value store (PM.UserPrefs) — fixes
  rememberedUsername being lost across reboots due to the random
  ephemeral HTTP port changing the localStorage origin every launch.
  Bridge cmd://prefs/{get,set} round-trips through Delphi.

- feat(tray): icon visible from startup (NIM_ADD at constructor, not
  at first minimize). Tray context menu themed via uxtheme!135
  SetPreferredAppMode so it follows the app's dark/light setting.

- feat(single-instance): named mutex + RegisterWindowMessage broadcast.
  Second launch posts WM_PMSHOW to HWND_BROADCAST and exits; the
  running bridge restores the window from tray. Mutex lives in Local\
  namespace so distinct Windows users can still each run one.

- feat(mfa): Authenticator sidebar view (live TOTP codes for every
  entry with a secret) + standalone TOTP generator modal (paste
  base32 / otpauth:// URI, or generate a random 20-byte secret).

- feat(sidebar): Folders / Tags / Tools sections collapsible with
  chevron toggle. Badge counts stay visible when collapsed. State
  persisted in settings_json (synced across devices).

- feat(autofill): hotkey when vault is locked now restores the app
  and focuses the master password input instead of no-op'ing
  silently. Cleaner UX for the common "I hit Ctrl+Shift+L but the
  vault was locked" path.

- feat(quick-unlock): when enabled, skip lockVault on Windows lock /
  sleep. Rationale: the DPAPI blob already gates access via the
  Windows account, so re-locking on top of the OS lock is redundant.
  Idle auto-lock still fires (separate opt-in).

- fix(quick-unlock): re-sync state.quickUnlockEnabled from DPAPI
  source-of-truth at boot, instead of trusting (now-volatile)
  localStorage.

- docs: CLAUDE.md updated with all new modules, bridge commands,
  and the port-ephemeral pitfall.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 21:31:39 +01:00
Zaki 01c56edf25 feat(recovery): single-use recovery code for forgotten master password
In a zero-knowledge vault, forgetting the master password normally
means losing the data — the AES key is derived from the master pw
and the server can't help. This commit adds the standard escape
hatch: a one-time recovery code that key-wraps the AES key so the
user can get back in.

Threat model
============
The plaintext recovery code is shown to the user exactly once, at
generation time. Server only ever stores SHA-256(code) + an AES-GCM
wrap of the vault key under a KEK = PBKDF2(code, kdf_salt, 600k).
Without the plaintext code the server cannot unwrap. The code is
high-entropy (96 bits from a 32-char ambiguity-free alphabet, in 4
groups of 4) — printed form is misreading-resistant.

Single use: redeeming deletes the row inside the same DB.Lock the
lookup happened in, so concurrent redeem attempts are race-free.
Failed redemptions feed both the per-IP rate limit AND the per-
username lockout, so brute-forcing the code is infeasible.

Schema
======
recovery_keys (
  user_id      INTEGER PRIMARY KEY (1:1 with users, FK cascade),
  code_hash    TEXT NOT NULL          (SHA-256 hex of plaintext code),
  kdf_salt     TEXT NOT NULL          (PBKDF2 salt for KEK derivation),
  wrapped_key  TEXT NOT NULL          (base64 AES-GCM ciphertext of vault key),
  wrapped_iv   TEXT NOT NULL          (base64 12B IV for the wrap),
  created_at   DATETIME DEFAULT CURRENT_TIMESTAMP
)

Backend: new unit PM.Handler.Recovery
=====================================
  GET    /recovery-key/status   (auth)         -> { configured, created_at? }
  POST   /recovery-key/setup    (auth + CSRF)  body {masterPassword, codeHash,
                                                     kdfSalt, wrappedKey, wrappedIv}
  DELETE /recovery-key          (auth + CSRF)  -> remove config
  POST   /recovery-key/redeem   (NO auth)      body {username, code}
                                               -> session + wrappedKey + wrappedIv + kdfSalt
                                                  + user's current salt + kdfIterations

VerifyMasterPassword() helper handles both legacy 'pbkdf2' and
current 'pbkdf2-sha256' schemes consistently with PM.Handler.Auth.

Setup flow
==========
1. Settings → "Generate recovery code" button (asks master pw via reauth).
2. Client generates: 16-char code + fresh kdf_salt + exports the current
   AES key via crypto.subtle.exportKey('raw').
3. Client wraps the raw key under KEK=PBKDF2(code, kdf_salt, 600k)
   with a random 12B IV → base64.
4. POSTs to /recovery-key/setup. Server verifies master pw, INSERT-or-
   replaces the row (DELETE+INSERT, no UPSERT — same pattern as the
   lockout table since FireDAC's UPSERT support is patchy).
5. Confirm modal shows the plaintext code in a monospace, user-select-all
   panel. The modal is forcing: "I saved it" button is the only way out.
   Modal is the only place the code ever appears — server never sees it.

Redeem flow (forgot master pw)
==============================
1. Auth screen → "Forgot master password? Use a recovery code" link.
2. promptDialog: username, then code (masked input).
3. POST /recovery-key/redeem. Server hashes the typed code, joins with
   users by username, ConstantTimeEquals against stored hash. On match:
     - deletes the recovery_keys row (single-use)
     - issues a fresh session token + CSRF
     - returns: { token, csrfToken, salt, kdfIterations, kdfSalt,
                  wrappedKey, wrappedIv, userId }
4. Client unwraps the AES key with PBKDF2(code, kdfSalt, 600k) → raw bytes
   → importKey('raw') back into a CryptoKey.
5. State is reconstituted from the new session, persistCryptoKey, enterApp.
6. Client immediately opens the Change-master-password modal — the
   recovery code is consumed and the account needs a fresh master pw
   AND a fresh recovery code (the user generates a new one from Settings).

Backward compat
===============
Recovery is opt-in. Existing users see "No recovery key set" in Settings
until they generate one. No migration needed — the table is created via
CREATE TABLE IF NOT EXISTS at server startup, FK cascade on user delete.

Minor UI additions
==================
 - .btn-link CSS class for the auth-screen "Forgot master password?" link.
 - Recovery-status label in Settings refreshed on every openSettings()
   via GET /recovery-key/status.
2026-05-23 11:18:34 +01:00
Zaki cf94f67488 feat(2fa): TOTP secret storage + live 6-digit code generation
Adds RFC 6238 TOTP (Google Authenticator-style) support to every entry.
The secret is encrypted client-side with the same AES-GCM key as the
password — the server stores opaque ciphertext and never sees the
plaintext base32 secret.

Schema
======
vault_entries.totp_secret TEXT  -- AES-GCM ciphertext, base64
vault_entries.totp_iv     TEXT  -- 12-byte IV, base64
Both NULL when the entry has no 2FA configured. Added via
ApplyMigrations.AddColumnIfMissing so existing vaults migrate cleanly.

Backend
=======
HandleListEntries: includes totp_secret + totp_iv in the response (or
JSON null when not configured).
HandleCreateEntry / HandleUpdateEntry: accept both fields; empty string
in the body → server stores NULL. Clearing the secret removes 2FA
from the entry.

Frontend
========
TOTP primitives (pure crypto.subtle, no external lib):
 - base32Decode(s)         — RFC 4648, tolerates spaces / lowercase
 - generateTOTP(secret)    — HMAC-SHA1 + RFC 4226 dynamic truncation
 - parseOtpAuthUri(raw)    — extracts ?secret from otpauth:// URIs

UI in the slide-over (the canonical entry detail view):
 - New "Two-factor (TOTP)" field below the password row.
 - Input is password-masked by default with eye-toggle to reveal.
 - Pasting a full otpauth:// URI auto-extracts the secret param so the
   user can copy directly from a QR-code scanner without manual cleanup.
 - X button clears the secret (= removes 2FA on next save).
 - Live code panel below: large monospace "123 456" + Copy button
   (routes through Bridge.copySecure → secure clipboard + 30s auto-clear).
 - Linear progress bar drains over the 30s window, turns red < 5s.
 - Refresh tick runs once per second while the slide-over is open;
   stops on closeSlideOver to avoid background work.

Entry card meta now shows a "2FA" chip when totp_secret is non-null —
quick visual scan for which accounts have 2FA configured without
opening the slide-over.

Validation
==========
soSave calls base32Decode(secret) before encrypting to refuse obviously
broken input. Otherwise garbled base32 would save fine and only fail
in the code panel next time.

Migration interaction (KDF 100k→600k)
=====================================
KNOWN MINOR ISSUE: /migrate-kdf only re-encrypts encrypted_password+iv,
not totp_secret+totp_iv. In practice this is harmless because:
  1) KDF migration runs immediately after login on legacy accounts —
     before the user has a chance to add a TOTP secret.
  2) New accounts start at 600k iterations, no migration ever needed.
A legacy user who somehow added a TOTP between login and the
background migration completing would end up with a TOTP encrypted
under the old key. The fix (extend /migrate-kdf to re-encrypt TOTP
fields too) is a one-line follow-up if anyone hits the edge case.
2026-05-23 05:13:50 +01:00
Zaki a45897c33d feat(security): HIBP password breach check + CSP tightening
HIBP integration
================
Opt-in (default OFF) password breach check via the Have I Been Pwned
range API. The full master / entry password never leaves the machine —
only the first 5 characters of its SHA-1 hash. HIBP returns ~500
candidate suffixes; the client matches its own suffix locally.

UI:
 - New "Check passwords against breach database (HIBP)" toggle in
   Settings → Security with an explainer hint about k-anonymity.
 - On enable: background batch scan of all entries, results cached in
   state.hibpResults keyed by entry id. Concurrency capped at 6 to
   avoid hammering HIBP / hitting browser connection limits.
 - Entry cards show a red "Pwned" chip + breach count in the tooltip
   when count > 0. New i-alert icon added to the SVG sprite.
 - Auto-scan triggered after every enterApp() when the toggle is on.

Functions added to app.js:
 - sha1Hex(text)                       — crypto.subtle wrapper
 - hibpCheckPassword(plaintext)        — single-password check, returns count
 - hibpCheckAllEntries()               — batched scan over state.entries

The "Add-Padding: true" header is sent on every range request to defeat
the response-size side-channel (HIBP adds 800-1000 random extra entries
so an observer counting bytes can't narrow the prefix queried).

CSP tightening
==============
Audited the served HTML: zero <script> tags inline, only the external
js/app.js. Removed 'unsafe-inline' from script-src — real XSS defense.

Kept 'unsafe-inline' on style-src for now because index.html contains
inline style="" attributes and app.js calls element.style.cssText
extensively. Refactoring to CSS classes is a separate cleanup. Style
injection alone cannot execute code, so the residual risk is bounded
to visual manipulation in a single-user loopback app.

Added api.pwnedpasswords.com to connect-src as the only allowed
external origin (required by the HIBP feature above). Default still
'self' — everything else stays loopback.

Before:
  script-src 'self' 'unsafe-inline';
  style-src  'self' 'unsafe-inline';
  connect-src 'self';

After:
  script-src 'self';
  style-src  'self' 'unsafe-inline';
  connect-src 'self' https://api.pwnedpasswords.com;
2026-05-23 05:05:50 +01:00
Zaki 73818e4e2e Add undo to drag-drop trash. Fix toast undo button visibility (darker bg, border, max-width). 2026-05-12 19:46:01 +01:00
Zaki ff9802e685 Add undo button in toast for trash actions (single + batch) 2026-05-12 19:40:23 +01:00
Zaki 617b8a7efe Hide FABs when auth visible (locked). Trash FAB glow shadow when active. 2026-05-12 19:36:35 +01:00
Zaki eb15d9e849 Trash FAB: always transparent background, no shadow 2026-05-12 15:49:40 +01:00
Zaki 59d407558b Trash FAB: always shows 🗑️; active mode has transparent background, no shadow 2026-05-12 15:33:12 +01:00
Zaki 5836bd168d Fix dblclick: clear selection, select entry, render, open edit. closeEdit calls render. Trash FAB: moved from toolbar to floating button at bottom-left (like add FAB). 2026-05-12 15:14:39 +01:00
Zaki d55be5f19e Fix entry card contrast (lighter card, more visible border). Fix dblclick: force-select entry before opening edit. 2026-05-12 14:20:46 +01:00
Zaki b51e9c3a73 Theme: neutral dark gray, card shadows, accent-rgb vars. Fix: arrows skip when modals open. Fix: detail view respects folder filter.
Dark theme: neutral dark grays (#0e1015 bg, #6b7280 accent). Light theme: white cards, better contrast. Cards now have subtle shadows for separation. All hardcoded rgba(59,130,246) replaced with rgba(var(--accent-rgb), ...). Arrow key handler skips when add/edit modal is open. Detail view now uses getFilteredEntries() instead of getFilteredEntries(true) to respect folder selection.
2026-05-12 13:51:41 +01:00
Zaki dfcbc3f568 Theme: neutral dark gray, higher card contrast. Detail view: folder filtering works. Modals: arrow nav blocked when modal open, Enter saves from anywhere inside modal. 2026-05-12 13:35:46 +01:00
Zaki 02e5868d66 Fix batch confirm: modal overlay + positioned beside button
Replace showCenterConfirm with showBatchConfirm(btn,message,callback) that creates a transparent modal overlay (z-index 9999) to block background clicks, positions the confirm dialog beside the trigger button. Updated all callers (batchDelete, batchPermanentDelete, permanentDelete, emptyTrash) to pass the clicked button. Added CSS for batch-confirm-overlay backdrop.
2026-05-09 18:37:57 +01:00
Zaki d50aea6845 Add 3 new views (Card, Grouped, Detail) and view dropdown
Replace view toggle buttons with dropdown menu containing all 7 views. Card view: 2-column grid with bigger cards. Grouped view: entries grouped by folder with sticky headers. Detail view: single entry at a time, large text, prev/next navigation. Arrow key navigation for detail view.
2026-05-09 16:51:38 +01:00
Zaki 757efe0908 Prevent native text selection on .vault during rectangle selection 2026-05-09 16:30:20 +01:00
Zaki 6a26dd78cb Fix rect selection in table view: prevent native text drag/selection
Added user-select:none and -webkit-user-drag:none to #entriesContainer.
Also added a dragstart listener that prevents default for any element
that is not [draggable='true'], stopping the browser from creating drag
ghosts of selected table text/headers during rectangle selection.
2026-05-09 16:18:03 +01:00
Zaki 9cc4463177 Use CSS classes for drag-dim effect and DOM-appended canvas for drag ghost 2026-05-09 15:39:14 +01:00
Zaki 5da0deaf94 Enhance selection visibility, add rectangle select, multi drag/drop
- Brighter selection highlight with stronger box-shadow, background, and left-border accent
- Rectangle/band selection: click-drag on empty space to select multiple entries via intersection test
- Drag selected entries to a folder chip to move all, or to trash button to multi-delete
- Multi-drag reorder: dragging a selected entry reorders all selected together
- Table view selection now highlights individual td cells (box-shadow on tr was invisible)
- Escape cancels rectangle selection in progress
2026-05-09 14:09:48 +01:00
Zaki eac02ed17c Replace batch mode toggle with click/Shift/Ctrl selection
- Remove separate 'select mode' toggle and checkbox UI
- Click any entry to select it (blue highlight border)
- Shift+click for range selection between two entries
- Ctrl/Cmd+click to toggle individual entries
- Click entry background or press Escape to clear selection
- Batch action bar appears automatically when items are selected
- Selected entries get accent-color border and highlight
- Single delEntry() cleans up selectedIds
- Shortcut help updated with selection tips
2026-05-09 13:48:22 +01:00
Zaki 8dd2fc71cf Fix star icon color for dark theme 2026-05-09 00:01:13 +01:00
Zaki 8abb1c5ad0 Add hover reveal, drag-to-folder, generator presets, favorites
- Inline password reveal on hover (controlled by showView setting, replaces eye button)
- Drag an entry card onto a folder chip to move it (no modal needed)
- Generator presets: Strong 16, Strong 20, Paranoid 32 buttons
- Favorites: star toggle button per entry, entries sort to top
- Add favorite column to vault_entries, toggle endpoint, star UI in all views
- Gold border/background for favorited entries
2026-05-08 23:53:47 +01:00
Zaki c6504f70d1 UI improvements: fix keyboard shortcuts, search highlighting, strength meter on register, colored folders, shortcut help
- Rewrite keyboard shortcuts using e.code and early preventDefault() to reliably override browser defaults
- Add ? key and toolbar button for shortcuts help modal
- Add password strength meter to register form
- Add search highlighting in all view modes (grid/list/compact/table)
- Add hash-based color coding for folder chips
- Add highlightText utility with regex escaping
2026-05-08 23:35:59 +01:00
Zaki 5ad1498afc add delete confirm, cancel shortcut + batch operations 2026-05-08 19:59:56 +01:00
Zaki a0a081e2db add clear for search + regroup options in settings btn 2026-05-08 17:21:38 +01:00
Zaki 8743f023b2 Fix refresh logout+ add trash (undo deleted) 2026-05-08 14:39:24 +01:00
Zaki a4528eb1f7 Standard password manager 2026-05-06 20:49:51 +01:00