63fac5b3b78d345664feefcd99b6648e0a66d813
22 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
33e4b4b614 |
feat(autostart): "Start with Windows" toggle
- PM.AutoStart wraps HKCU\Software\Microsoft\Windows\CurrentVersion\Run.
Value "PMServer" = "<exe>" -tray. Per-user, no admin required, shows
up in Task Manager → Startup so the user can override from there.
- UMainForm honours the -tray CLI flag (set by the registry entry):
after the server starts, MinimizeToTray via TThread.ForceQueue so the
app comes up directly in the tray with no visible window flash.
- Bridge cmd://autostart/{get,set} + Bridge.getAutoStart() /
setAutoStart() / onAutoStartStatus(). Settings exposes a toggle in
the Security section, visible only when Bridge.active (the PHP
frontend can't touch the registry).
- Toggle reads "on" only when the registered command matches the
current exe path, so a stale entry from a moved exe lets the user
re-enable to refresh.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
749dc87058 |
feat(unlock): Quick unlock via DPAPI (remember on this device)
User-controlled opt-in to skip the master-password prompt on subsequent
app starts. The vault state (raw AES key + salt + username + session
token) is bundled and handed to the Delphi side, which DPAPI-encrypts
it with CRYPTPROTECT_CURRENT_USER and stashes the blob at
%LOCALAPPDATA%\PMServer\quickunlock.bin.
Honest threat model
===================
This is NOT biometric authentication. The DPAPI scope is the Windows
USER ACCOUNT — any process running as the same user can decrypt the
blob via the same DPAPI call. The security perimeter is the Windows
account itself. The Settings UI label is "Quick unlock" with an
explainer:
"convenient on a personal machine, not safe on a shared one"
If the user has Windows Hello / fingerprint / PIN configured at the
OS level, then Windows login is biometric-gated and that gating
transitively applies to DPAPI access — but the cryptographic strength
of the encryption isn't tied to the biometric, it's tied to the
Windows account secret. Honest framing matters here, so the feature
isn't sold as "biometric".
Backend
=======
New unit Source/PM.QuickUnlock.pas:
- StoreQuickUnlock(bytes) → DPAPI-encrypt and persist to
%LOCALAPPDATA%\PMServer\quickunlock.bin
- LoadQuickUnlock(out bytes) → read file, DPAPI-decrypt
- ClearQuickUnlock → forget-me
- HasQuickUnlock → file existence probe
DPAPI declarations are local (CryptProtectData / CryptUnprotectData
from crypt32.dll) — Winapi.WinCrypt's signatures drift across Delphi
versions and we don't want to fight that.
Bridge commands (UMainForm.HandleBridgeCommand):
cmd://quickunlock/store?data=<base64> payload opaque to Delphi
cmd://quickunlock/get → ExecuteJavaScript callback
Bridge.onQuickUnlockResult(b64|null)
cmd://quickunlock/clear forget-me
cmd://quickunlock/status → Bridge.onQuickUnlockStatus(bool)
The get / status results are returned via ExecuteJavaScript rather than
HTTP (the bridge is request-only) — JS resolves a Promise that the
caller awaited.
Client
======
state.quickUnlockEnabled mirrors localStorage flag, lazy-cleared if the
backing DPAPI blob has gone missing (e.g., user reset Windows profile).
enableQuickUnlock():
1. askReauth + /reauth to verify it's actually the user.
2. exportKey('raw', state.cryptoKey) — extractable already.
3. JSON-bundle { v, username, salt, token, csrf, key } → base64.
4. cmd://quickunlock/store sends the blob to Delphi.
tryQuickUnlock() (called from init):
1. Probe localStorage flag.
2. cmd://quickunlock/get, await Bridge.onQuickUnlockResult.
3. Decode JSON, importKey, restore state.* + sessionStorage.
4. Return true on success, false to fall through to master-pw login.
Two restore scenarios both covered:
A. Same app session (sessionStorage still populated, only cryptoKey
was wiped by lock). tryQuickUnlock just restores the key.
B. Cold start (sessionStorage empty). tryQuickUnlock restores
EVERYTHING from the DPAPI blob, including the session token.
UI
==
Settings panel → new "Quick unlock" section above Recovery key.
Single toggle button: "Enable on this device" / "Disable" with status
line above. Opens settings → bridgeQuickUnlockStatus() reconciles the
JS-side flag with the actual file (drift detection).
Stale-blob protection
=====================
The stored blob holds the AES key BYTES, which would become useless
if the vault were re-encrypted under a different key. Three paths
that re-encrypt the vault now also wipe the DPAPI blob:
- Explicit doLogout (user said "I'm done")
- Master password change (new key, old blob can't decrypt anything)
- (Recovery redeem already forces master pw change → covered.)
The blob persists across the passive lockVault() flow on purpose —
that's the whole point: lock without losing convenience.
Init wiring
===========
On app start, the existing "restore session from sessionStorage" path
now falls through to tryQuickUnlock if either sessionStorage is empty
OR the cryptoKey is gone. Auth screen shows up only after both
attempts fail.
|
||
|
|
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.
|
||
|
|
cca8184b81 |
feat(auth): change master password with full vault re-encryption
Adds the canonical PM feature: let the user pick a new master password
and have every entry transparently re-encrypted under the new key,
without ever exposing plaintext to the server.
Backend endpoint: POST /change-master-password
==============================================
Body:
{
currentMasterPassword, verified against current stored hash
newMasterPassword, basis for the new hash + new client key
newSalt, 64-char hex, client-generated
entries: [{ id, encrypted_password, iv,
totp_secret?, totp_iv? }, ...]
}
Flow:
1. Authenticate + RequireCSRF (caller already logged in).
2. RejectIfAccountLocked — pw change is brute-forceable through a
hijacked session, so it respects the same per-account lockout as
/login.
3. Verify currentMasterPassword against the stored hash. Branches on
hash_algo to handle both legacy 'pbkdf2' and current 'pbkdf2-sha256'.
Wrong pw → RecordFailedAccountAttempt + audit + 401.
4. Compute new auth hash = SHA256(PBKDF2(new_pw, new_salt, 600k)),
always using the current scheme (migration baked in).
5. ATOMIC transaction:
UPDATE users SET password_hash, salt, kdf_iterations, hash_algo
UPDATE vault_entries SET encrypted_password, iv, totp_secret, totp_iv
(per entry)
Any failure → rollback, user stays on the old config.
6. DeleteAllUserSessions — every OTHER session is invalidated so a
leaked old token can't keep working past the rotation. The current
caller's session stays valid.
7. ClearAccountLockout + audit_log entry.
8. Returns { message, salt, kdfIterations }.
Client
======
New modal in index.html (#changeMasterModal) with three password
fields (current / new / confirm) + inline error display. Added a
"Change master password" button in the Settings panel → Account
section. Escape-key handler routes through it like the other modals.
doChangeMasterPassword():
1. Local validation: all fields filled, new ≥ 8 chars, new == confirm,
new ≠ current. Fast failure beats a round trip.
2. randomHexSalt() → 32 secure random bytes, hex-encoded.
3. Derive newKey = PBKDF2(new_pw, new_salt, 600k).
4. Walk state.entries: decrypt password + (optional) TOTP under the
current key, re-encrypt under newKey with fresh random IVs.
One decrypt failure aborts the whole change — better than partial
commit.
5. POST to /change-master-password.
6. On success: swap state.salt + state.cryptoKey, persistCryptoKey,
update sessionStorage, refresh cached ciphertexts in state.entries,
close modal, toast.
7. On 401 / 429 / generic error: show inline error in the modal so
the user can fix and retry without re-typing everything.
Threat model notes
==================
- The current session token stays valid because the new server hash
only invalidates OTHER sessions. Self-logout would be needlessly
disruptive (user already proved knowledge of both pws).
- Server still sees the old + new master pws transiently in /change-
master-password. Same trade-off as /login — eliminating it requires
redesigning to send pre-computed verifiers (SRP-style), tracked
separately.
- The salt rotates with the password — best-practice against any
precomputed dictionary attack tied to the previous salt.
|
||
|
|
3c786366fc |
feat(export): encrypt vault backups with an independent password
Replaces the plaintext JSON exporter with an encrypted container.
The previous plaintext flow was a known security gap — a backup file
on disk or in a cloud sync folder gave full plaintext access to
every password if accessed by anyone (or anything) other than the
user.
Container format
================
Self-describing JSON:
{
"format": "pm-encrypted-export-v1",
"kdf": "pbkdf2-sha256",
"kdf_iterations": 600000,
"kdf_salt": "<base64 32B>",
"iv": "<base64 12B>",
"ciphertext": "<base64 AES-GCM(payload)>",
"created_at": "<ISO>"
}
payload = same shape as the legacy plaintext exporter (entries array
with site, username, password, folder, tags, favorite, totp_secret,
timestamps), so the round-trip through the JSON importer works
without a separate code path.
Export password
===============
User-chosen, INDEPENDENT of the master password — the export modal
explicitly explains this. Rationale:
- A master-password change doesn't invalidate old backups.
- The backup file can be shared with another person without
revealing the master pw.
- Trade-off: one more password for the user to remember. We assume
they're storing the backup intentionally and can record the pw.
Minimum length 6 enforced client-side.
Flow
====
Export:
1. askReauth(master pw) → server /reauth verifies (defense against
someone reaching the unlocked laptop and dumping the vault).
2. promptDialog(password: true) → export password.
3. Walk state.entries, decrypt each password + TOTP with the vault
key, assemble payload.
4. encryptExportPayload(payload, exportPwd) — random 32B salt,
random 12B IV, PBKDF2 600k, AES-GCM-256.
5. Download the container as
vault-export-YYYY-MM-DD.json.
Import:
1. Read file, detect format. JSON with format === "pm-encrypted-
export-v1" → prompt for the export password.
2. decryptExportContainer → plaintext payload, then JSON.stringify
back into the existing parseEntriesFromJSON path so the rest of
the import flow (preview confirm, bulk encrypt, /entries/bulk-
import) is unchanged.
3. Wrong password → AES-GCM tag fails → "Decryption failed" toast,
user retries.
Other changes
=============
- promptDialog gains a `password: true` option that flips the
confirm input's type so the value is masked on screen.
- Export modal copy in the Settings panel updated to mention the
encrypted format and the independent password.
- The 429-lockout path on /reauth is now handled explicitly in
doExport (was previously falling through to "wrong password").
Backward compatibility
======================
Plaintext JSON exports produced by the previous version still
import — parseEntriesFromJSON doesn't care whether the input came
from a fresh decryption or directly from a plaintext file. The
exporter no longer produces plaintext though; users with old
backups should re-export after upgrading.
|
||
|
|
4b15811221 |
feat(import): JSON / CSV vault import with heuristic column mapping
Round-trip companion to the existing doExport(). Supports two file
formats with auto-detection (extension + first-char sniff):
JSON
====
Native shape produced by doExport() AND a forgiving fallback for any
flat array of entry objects with site/url + password fields. Accepts:
- { version, exported_at, entries: [...] } (native)
- [{ ... }, { ... }] (flat array)
- mixed keys: site|url|name, username|user|login|email, etc.
CSV
===
RFC-4180-ish parser (~30 lines): quoted fields, escaped "", commas
inside quotes, CRLF line endings. No streaming since password-manager
imports are realistically MB-scale at most.
Heuristic column mapping (case + underscore tolerant) covers the
common exporters out of the box:
Site/URL : name, title, url, site, website, login_uri, login_url
Username : login_username, username, user, login, email
Password : login_password, password, pass, pwd
Folder : folder, group, category, path, collection
Tags : tags, labels (comma/semicolon-split)
Notes : notes, note, comment (short notes joined into tags)
TOTP : login_totp, totp, otpauth, authenticator, two_factor
If the TOTP column holds a full otpauth:// URI it's parsed and only
the secret param is stored — same path used by the slide-over TOTP
field. Invalid base32 TOTP secrets are dropped silently rather than
failing the whole import.
Backend
=======
New endpoint: POST /entries/bulk-import
Body: { entries: [{ site, username, encrypted_password, iv, folder,
tags, totp_secret, totp_iv }, ... ] }
Caps at 10,000 entries per request as a sanity bound. Inserts inside
a single SQLite transaction — partial failure rolls back cleanly, the
user retries from the same source file. Returns { imported: N }.
Rows missing site or ciphertext are skipped within the transaction
(not failed) so one bad row in a 500-entry import doesn't blow up
the whole batch.
Client flow
===========
doImport():
1. Hidden <input type="file" accept=".json,.csv"> picker
2. Read text, detect format, route to parseEntriesFromJSON or CSV
3. confirmDialog preview: count + first 3 sample sites + skipped rows
4. On confirm: encryptImportEntry() each plaintext entry with the
current vault key (reuses encryptPwd / base32Decode validation)
5. Single POST to /entries/bulk-import
6. Reload entries, refresh UI, trigger HIBP scan if enabled
UI
==
Two entry points (mirroring Export):
- Sidebar "Import vault" nav item, next to "Export vault"
- Settings panel "Import" section with descriptive blurb
Both call doImport(). New i-log-in icon added to the SVG sprite (mirror
of i-log-out used by Export).
Limitations
===========
- No de-duplication: importing the same file twice yields duplicate
entries. Trade-off to keep the v1 simple — the user can sort it
out with the existing trash/multi-select UI.
- No password-protected vault formats (Bitwarden encrypted JSON,
KeePass kdbx). Only plaintext exports — same trade-off as
doExport() which produces plaintext JSON.
|
||
|
|
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; |
||
|
|
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). | ||
|
|
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. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
c8c7b69ae0 |
Phase 5: CSRF, bcrypt hashing, audit logging, export re-auth
- Add CSRF token per session, validated on all state-changing requests (POST/PUT/DELETE) - Upgrade password hashing from PBKDF2 to bcrypt; auto-upgrade old hashes on login - Add audit_log table tracking all security events (login, export, delete, etc.) - Add /reauth endpoint requiring master password before export - Client-side: re-auth modal before export, X-CSRF-Token header on mutations |
||
|
|
5ad1498afc | add delete confirm, cancel shortcut + batch operations | ||
|
|
a0a081e2db | add clear for search + regroup options in settings btn | ||
|
|
8743f023b2 | Fix refresh logout+ add trash (undo deleted) | ||
|
|
a4528eb1f7 | Standard password manager |