Commit Graph

58 Commits

Author SHA1 Message Date
Zaki 60aa106a30 fix(crypto): SHA-256 wrap auth hash so vault.db at rest no longer = AES key
THE PROBLEM
===========
Before this commit, users.password_hash stored on the server contained
PBKDF2(pw, salt, iters) in hex — the exact same 32 bytes the client
uses as the AES-GCM key to encrypt every entry. Anyone who got hold of
vault.db (filesystem access, backup leak, etc.) had the encryption key
in their hand, no brute force needed. The increased PBKDF2 iteration
count from the previous commit helped against the cipher-text path,
but the easier path was right there in the user row.

THE FIX
=======
Wrap the PBKDF2 output in SHA-256 before storing:

  password_hash = SHA256(PBKDF2(pw, salt, iters))

SHA-256 is one-way. The stored hash can still be verified at login
(server recomputes PBKDF2 from the posted master pw, then SHA-256s it,
compares to stored), but the AES key can no longer be recovered from
it. At rest, vault.db only contains an irreversible derivative.

The server still sees pw transiently during /login while computing
the comparison — eliminating that requires a redesigned auth
protocol where the client sends a pre-computed verifier (SRP, OPAQUE,
or simply SHA-256(PBKDF2(pw, salt, iters)) sent from the client).
That's a separate, larger refactor. This commit closes the at-rest
hole, which is the realistic attack surface for vault file leaks.

SCHEMA / MARKER
===============
users.hash_algo distinguishes the two schemes:
  'pbkdf2'        — LEGACY (raw hex, = AES key)
  'pbkdf2-sha256' — CURRENT (SHA-256-wrapped, one-way)

A constant HASH_ALGO_CURRENT replaces the string literal everywhere
to avoid silent drift between the writer and the reader sides.

MIGRATION
=========
Folded into the existing /migrate-kdf endpoint introduced for the
100k→600k iteration bump. Login response now signals migration on
EITHER:
  - kdf_iterations < PBKDF2_ITERATIONS_TARGET, OR
  - hash_algo != 'pbkdf2-sha256'

The endpoint handles both transitions in one atomic transaction:
  UPDATE users SET password_hash = SHA256(PBKDF2(pw, salt, 600k)),
                   kdf_iterations = 600000,
                   hash_algo = 'pbkdf2-sha256'
  UPDATE vault_entries SET encrypted_password, iv (per entry, if KDF changed)

Idempotency tightened: the "already at target" short-circuit now
requires BOTH conditions, not just the iteration count. Without this,
users who migrated KDF before this commit landed would have been
stuck on the legacy hash format.

CLIENT
======
runKdfMigration() branches on whether the KDF actually changed:
  - kdfChange (fromIters !== toIters): re-encrypt all entries with the
    new key, send them in the entries array, swap state.cryptoKey on
    success. Shows "Vault security upgraded" toast.
  - !kdfChange (hash format only): skip the entry re-encryption loop
    entirely, send entries: []. Silent — the user didn't perceive a
    weakness change worth toasting about.

LOGIN / REAUTH
==============
Both now branch on hash_algo to pick the right verifier:
  HASH_ALGO_LEGACY  → ConstantTimeEquals(stored, PBKDF2(pw, salt, iters))
  HASH_ALGO_CURRENT → ConstantTimeEquals(stored, SHA256(PBKDF2(pw, salt, iters)))

Same constant-time comparison helper as before. Same legacy bcrypt
fallback (still 501-not-implemented).

ALL THREE SCENARIOS AFTER THIS COMMIT
=====================================
1. New register: starts at HASH_ALGO_CURRENT + 600k. No migration ever.
2. Legacy 100k + 'pbkdf2': full migration on next login (hash format
   + iter count + entry re-encryption) in one transaction.
3. Mid-state (already-migrated KDF + still-'pbkdf2'): hash format
   upgrade only on next login, no entry re-encryption.
2026-05-23 05:19:39 +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 e0e452306e feat(crypto): PBKDF2 iterations 100k → 600k with transparent re-encryption
Bumps the PBKDF2-SHA256 iteration count from 100,000 (OWASP 2017) to
600,000 (OWASP 2023). 6x slowdown on every brute-force attempt against
either the server-stored auth hash OR the AES-GCM ciphertext of the
entries — both currently use the same PBKDF2 output (see KNOWN ISSUE
below for why that's another problem to fix later).

Schema
======
users.kdf_iterations INTEGER DEFAULT 100000
  Per-user iteration count. Legacy rows predating the column default
  to 100k via the DEFAULT clause. New accounts insert 600k explicitly.

Migration flow
==============
Atomic from the user's perspective. No partial state ever persisted.

  1. /login (or /reauth):
     server reads users.kdf_iterations and verifies the master pw at
     that count. Login succeeds at the legacy strength. Response now
     includes kdfIterations (current) and optionally kdfMigration =
     { target: 600000 } when an upgrade is recommended.

  2. Client:
     derives the AES key at the OLD count to decrypt current entries
     (state.cryptoKey). enterApp() loads the vault normally.

  3. runKdfMigration() (background, after enterApp):
     - derives the NEW key at target iterations
     - decrypts every entry with the old key
     - re-encrypts every entry with the new key + fresh random IVs
     - POSTs { masterPassword, entries: [...] } to /migrate-kdf

  4. /migrate-kdf (new endpoint):
     - verifies the master pw against the OLD hash
     - in a single transaction:
        UPDATE users  SET password_hash = pbkdf2(pw, salt, 600k),
                          kdf_iterations = 600000
        UPDATE vault_entries SET encrypted_password, iv (per entry)
     - on any failure: ROLLBACK. User stays at legacy config, retries
       at next login. No half-migrated state possible.

  5. Client (post-commit):
     swaps state.cryptoKey to the new key, persists it, updates the
     cached ciphertext in state.entries, shows a "Vault security
     upgraded" toast.

Idempotency: server's /migrate-kdf short-circuits with "Already at
target" if users.kdf_iterations >= PBKDF2_ITERATIONS_TARGET.

Race conditions: two concurrent migrations from two tabs both
recompute the SAME new key (deterministic PBKDF2). The losing
transaction's entries get re-encrypted with the winning one's IVs,
but both clients can decrypt because the keys are identical.

KNOWN ISSUE (not fixed by this commit)
======================================
The server's password_hash IS the client's AES key, in hex form —
both sides compute PBKDF2(pw, salt, iters) and store/use the same
32 bytes. This means a stolen vault.db gives the attacker the
encryption key directly, without needing to brute-force anything.
The 100k → 600k bump still helps because the AES-GCM ciphertext
itself is also a brute-force target, but the architectural fix
(server stores SHA256(aes_key) instead of aes_key in hex) is a
separate concern that needs its own migration.

Other changes
=============
 - HandleRegister: new accounts insert kdf_iterations=600000.
 - HandleReauth: response upgraded to JSON with kdfIterations
   + optional kdfMigration. Unlock path now also triggers migration.
 - SendAuthSuccess: extended signature, all callers updated.
 - deriveKey(pwd, saltHex, iterations) in app.js: iterations param
   required, defaults to 100000 for back-compat with any legacy caller.
2026-05-23 04:54:05 +01:00
Zaki 9f6636defc feat(auth): per-account brute-force lockout with exponential backoff
Existing protection was per-IP only (login_attempts table). On a loopback
deployment everyone hits 127.0.0.1, so the per-IP counter is mostly
ornamental — the real attacker is on the same machine. Adds a second
defense layer that tracks failures per username with an exponential
backoff schedule.

Schema:
  account_lockouts (username TEXT PK, failed_count INT,
                    locked_until DATETIME, last_attempt_at, last_attempt_ip)

Backoff after threshold (4+ failures):
  1, 2, 3 failures → no lockout (grace window for typos)
  4th             → 60 s
  5th             → 5 min
  6th             → 15 min
  7th             → 1 h
  8th             → 6 h
  9th and beyond  → 24 h (capped)

Counter resets to 0 on successful login or reauth. Old non-locked rows
older than 30 days are pruned by CleanupExpired alongside the existing
sessions / audit_log / login_attempts cleanups.

Wiring:
 - HandleLogin / HandleReauth both check RejectIfAccountLocked() before
   touching the users table. Lockout responses are 429 with JSON body
   { error, retry_after } and a Retry-After header.
 - Failed attempts are recorded against the username even when the user
   doesn't exist, preventing account enumeration via differential
   "is this account locked?" probes.
 - PBKDF2 hash comparison was already constant-time (ConstantTimeEquals);
   no change there.

Client (js/app.js):
 - api() now preserves response status + body on Error so callers can
   distinguish 429-lockout from other errors.
 - New showLockoutCountdown(seconds) renders a live "Account locked —
   try again in Xm Ys" message in #authHint, disables #loginBtn until
   the countdown reaches 0, then re-enables it.
 - doLogin / doUnlock both branch on err.status === 429 + retry_after
   to call showLockoutCountdown instead of a generic error toast.

Known limitation: an attacker can DoS-lock arbitrary usernames by
spamming /login with that name. This is intentional — the alternative
(per-(username,IP) tracking) would let attackers enumerate accounts.
DoS-lock is acceptable; auth bypass is not.
2026-05-23 00:14:44 +01:00
Zaki 506aee7e6f feat: Delphi backend + JS↔Delphi bridge (clipboard, tray, auto-lock)
Introduces the Delphi 12 FMX backend (PMServer) that hosts the embedded
WebView2 vault on 127.0.0.1, and a native bridge between JS and Delphi
that wires three privacy-focused features:

1. Secure clipboard
   Copying a password registers the Win32 "ExcludeClipboardContentFromMonitorProcessing"
   format alongside CF_UNICODETEXT, so Win+V clipboard history never sees
   the value. Auto-clears after 30s via TTimer. Bridge.copySecure() in
   app.js routes all password/username/secret copy paths through the
   native layer when running inside the Delphi WebView2 (falls back to
   navigator.clipboard for the PHP standalone).

2. Tray icon (X-to-tray when server running)
   Closing the dev panel hides both the form HWND and the TFMAppClass
   per-process proxy window that owns the FMX taskbar entry — the form's
   HWND alone is not the taskbar-visible one in FMX (took some iteration
   to discover). Tray menu: Open, Lock vault, Quit. Clipboard is force-
   cleared on minimize as extra safety. First-time minimize fires a
   balloon notification so the user knows the app is still running.

3. Auto-lock on Windows session lock (Win+L)
   wtsapi32.dll!WTSRegisterSessionNotification on a dedicated message-only
   window. On WM_WTSSESSION_CHANGE / WTS_SESSION_LOCK, the bridge calls
   ExecuteJavaScript('lockVault()'). Same path used by the tray "Lock vault"
   menu item.

Bridge architecture:
 - JS → Delphi via cmd:// URLs intercepted in OnBeforeNavigate
   (pattern lifted from DeskInsight Monaco). Currently exposes
   cmd://clipboard/copy?text=...&clear=... and cmd://clipboard/clear.
 - Delphi → JS via TTMSFNCWebBrowser.ExecuteJavaScript with guarded
   calls (typeof check) so the bridge degrades cleanly if app.js isn't
   loaded yet.

Files:
 - Source/PM.Bridge.pas (new) — TSecureClipboard + TPMBridge
 - UMainForm.pas/.fmx — bridge wiring, FormCloseQuery intercept, tray
   callbacks (BridgeTrayRestore / BridgeLockRequest / BridgeQuit)
 - js/app.js — Bridge object, 5 navigator.clipboard sites migrated to
   Bridge.copySecure with PHP-compatible fallback, Bridge.onTrayRestore
   handler that resets the auto-lock timer

.gitignore extended with Delphi build artifacts (*.dcu, Win32/, Win64/,
__history/, __recovery/, *.identcache, *.dsk, *.local, etc.) so source
checkouts stay clean.
2026-05-22 23:47:57 +01:00
Zaki 159e02ae81 Fix double toast on single delete: pass noToast to delEntry. Remove auto-dismiss timeout on undo toasts (visible until clicked). 2026-05-12 19:55:30 +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 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 194c11ece2 Fix Shift+arrow range selection: track anchor (fixed) and focus (moving) separately
Previous approach always started from the lowest-indexed selected entry, causing the range to snap to anchor on arrow reversal. Now arrowFocus tracks the moving end; arrowAnchor stays fixed. Non-Shift arrow resets both to the new position.
2026-05-09 23:36:39 +01:00
Zaki 0a0e380b95 Fix shift+arrow anchor and detail view arrow navigation
Shift+arrow now uses arrowAnchor (set on first arrow press) instead of lastSelectedId (which was updated on each non-Shift move). Fixes 'only 2 entries' bug. Detail view: fixed inverted auth check for ArrowLeft/Right; general arrow handler now excludes detail view.
2026-05-09 23:32:36 +01:00
Zaki 3802f1fc23 Grid view: arrow up/down moves vertically by column count; left/right moves linearly 2026-05-09 23:23:30 +01:00
Zaki b0785fbe8d Arrow left/right navigate entries; Shift+arrow extends multi-selection 2026-05-09 23:16:17 +01:00
Zaki 72d7d1015a Arrow up/down to navigate entries, Enter to open selected for editing 2026-05-09 23:12:09 +01:00
Zaki 52a4242a52 Auto-focus: loginUsername on load, website in add/edit modals. Enter saves from anywhere inside modals. 2026-05-09 22:41:52 +01:00
Zaki 69b17e4505 Fix password generator modulo bias with rejection sampling
Use single-byte rejection sampling: generate byte, reject if >= largest multiple of charset length, then modulo. Eliminates bias from c.charAt(arr[i] % c.length).
2026-05-09 22:28:54 +01:00
Zaki 4ddf2e9be6 Fix Enter in edit modal: switch from keypress (deprecated) to keydown 2026-05-09 22:20:09 +01:00
Zaki ca7555a603 Fix Enter key in add modal: password field id is addPassword, not passwordInput 2026-05-09 22:14:46 +01:00
Zaki f4a542885a Enter key saves in add/edit modals from any field
Pressing Enter in addSite, addUsername, passwordInput calls addEntry(). Enter in editSite, editUsername, editPassword calls saveEdit().
2026-05-09 21:27:59 +01:00
Zaki f00bb2cdeb Add double-click on entry to open edit modal
Double-click any entry card/row (all views incl. detail) opens the edit modal. Excluded from trash view.
2026-05-09 21:08:51 +01:00
Zaki 70c22ce153 Change shortcuts: Alt+N for new entry, Alt+T for toggle trash
Ctrl+N and Ctrl+T cannot be intercepted by Chrome (browser-level shortcuts). Replaced with Alt+N and Alt+T which work reliably. Updated keyboard shortcuts help.
2026-05-09 21:03:32 +01:00
Zaki eb82bd416d Fix Ctrl+N/T: use e.key instead of e.code, remove duplicate guard
Switched from e.code (physical key position) to e.key (character value) which is more reliable across keyboard layouts and browsers. Also removed duplicate Ctrl guard line.
2026-05-09 19:44:44 +01:00
Zaki bfe20d29ec Fix Ctrl+N/T: use window capture phase to intercept before browser chrome
document-level keydown bubbling phase is too late for browser-level shortcuts Ctrl+N (new window) and Ctrl+T (new tab). Moved handlers to a window.addEventListener('keydown', ..., true) capture-phase handler that fires before the browser chrome acts. Removed redundant document-level copies.
2026-05-09 19:24:36 +01:00
Zaki 03c20fcb8c Fix Ctrl+N/T opening browser windows instead of app actions
Moved Ctrl+N and Ctrl+T handlers before the generic Ctrl guard with their own e.preventDefault() and early return, matching the pattern used for Ctrl+A. Removed them from the old else-if chain and combined preventDefault block.
2026-05-09 19:22:15 +01:00
Zaki 37c15ecad6 Fix Ctrl+A: use e.key instead of e.code, move before generic Ctrl guard
Ctrl+A was selecting page text instead of entries. Switched from e.code === 'KeyA' (layout-dependent) to e.key === 'a'. Moved handler before the generic Ctrl+key else-if chain and before the combined preventDefault block, with its own e.preventDefault() and early return. Handles both 'a' and 'A' key values.
2026-05-09 19:13:58 +01:00
Zaki e414b4f600 Fix Ctrl+A and add Delete key shortcut
Ctrl+A auth check was inverted (!hidden instead of hidden), preventing selection in vault. Added Delete key shortcut: moves selected entries to trash (or batch permanent delete in trash view). Added Del to keyboard shortcuts help.
2026-05-09 19:04:36 +01:00
Zaki f40024cc67 Fix: mousedown clears selectedIds before batch callback runs
Document mousedown handler was clearing selectedIds when clicking 'Yes' (button element). Since mousedown fires before click, selectedIds was empty by the time the async callback read it, causing no-op deletes but still showing the toast. Added .batch-confirm-overlay to the mousedown exclusion list so clicking the confirm dialog won't trigger clearSelection().
2026-05-09 18:46:27 +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 131bb91cd2 Replace standard confirm() with custom centered confirm dialog
Add showCenterConfirm() function for dialogs without button anchor. Replaced all standard confirm() calls in batchDelete, batchPermanentDelete, permanentDelete, emptyTrash. Batch permanent delete shows single custom confirm with entry count, no nested dialogs. permanentDelete() accepts silent param to suppress UI for batch operations. Order cleanup added to permanentDelete.
2026-05-09 18:20:06 +01:00
Zaki 7e623067b0 Fix: batch toast spam on trash drag, restore icon, stale ref, reorder safety
Trash drop handler now uses noToast + single message. Restore button icon changed to ♻️. Removed stale .view-toggle reference in click-outside handler. Added safety check in reorder drop handler to prevent entries being lost from order array.
2026-05-09 17:55:52 +01:00
Zaki 0b3f886337 Fix batch toast spam and add Ctrl+A select all
Batch delete/restore now shows single toast with count instead of one per entry. delEntry() and restoreEntry() accept noToast param. Added Ctrl+A to select all visible entries. Updated keyboard shortcuts help.
2026-05-09 17:44:12 +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 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 efbf8f450a Replace count-badge drag ghost with stacked-card visual for multi-drag 2026-05-09 15:41:08 +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 ca159ec4f3 Add custom drag ghost showing selected count for multi-drag 2026-05-09 15:23:21 +01:00
Zaki f7db36d423 Dim all selected cards during drag, not just the dragged one 2026-05-09 15:15:17 +01:00
Zaki 34d20c5224 Fix rect selection to work on vault background, not just entriesContainer
The entriesContainer has no padding so cards fill the grid edge-to-edge;
there is no clickable empty space at the left/top/bottom of the card area
within entriesContainer.  Moved rect-selection initiation into the
document-level mousedown handler so it fires when clicking on ANY
non-interactive element inside .vault (padding, grid gaps, etc.).
Interactive elements (buttons, inputs, folders bar, toolbar, etc.)
still clear selection as expected.
2026-05-09 14:57:13 +01:00
Zaki f9707ebba8 Add click-outside deselection (clear selection on mousedown outside any entry card)
Document mousedown handler clears selection when clicking on any element
that is not an entry card, the batch bar, or an open modal/settings menu.
Works in tandem with entriesContainer rect-select handler: when clicking
inside entriesContainer on empty space the container handler runs first
(setting rectState.active=true) so the document handler skips;
when clicking outside entriesContainer the document handler fires alone
and calls clearSelection().
2026-05-09 14:42:17 +01:00
Zaki a97d05412e Fix duplicate trash drop listener and rectangle selection bugs
- Move trash button drop listener out of setupDrag() to an IIFE so it
  only registers once (was accumulating listeners on every render() call,
  causing multiple toast messages on single drop).
- Fix rectangle selection mousedown handler: use optional chaining on
  closest() for safety with text nodes; avoid calling render() inside
  mousedown (which would detach the event target's children mid-handler)
  and instead just remove .selected class directly from the DOM.
2026-05-09 14:34:20 +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 367b85e991 Add WebAuthn passkey biometric unlock + dark toggle on auth screen
- Dark/light theme toggle now always visible (moved outside auth/vault sections)
- New passkey_challenges and passkey_credentials tables
- /passkey/register/begin + /passkey/register/complete endpoints
- /passkey/login/begin + /passkey/login/complete endpoints
- CBOR decoder + COSE key parser for WebAuthn attestation/assertion
- ES256 (P-256) signature verification via OpenSSL
- Client-side: register passkey button in settings, passkey login on auth screen
- First passkey login prompts for master password once to derive AES-GCM key
- Passkey login requires username input before biometric prompt
2026-05-09 13:01:59 +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