Third slice of the app.js split. Moves the encrypted export container,
CSV/JSON import parsing (parseCSV, findColumn, parseEntriesFromCSV,
parseEntriesFromJSON), and the doImport/doExport/doExportCSV flows to
js/app.import.js. encryptImportEntry moves here too (also called by
app.sync.js — resolved via shared global scope at call time).
- Byte-for-byte identical to the extracted block; no duplicate const;
no top-level import ref left in app.js.
- Load order: BEFORE app.js (pure declarations, no top-level side effects),
alongside app.crypto.js. Full order: argon2 → crypto → import → app → sync.
- index.html + BuildAssets whitelist + harness APP_PARTS updated.
- Safety net: the 14 CSV tests exercise parseCSV/parseEntriesFromCSV from
the extracted file and stay green (42/42).
app.js: 11936 → 10253 lines (crypto + sync + import now separate, ~1700
lines moved into 3 modules).
NOTE: assets.res not regenerated here (needs brcc32/Delphi) — run
BuildAssets before the next Delphi build to embed js/app.import.js.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second slice of the app.js split (after crypto). Moves the WebDAV sync
section to js/app.sync.js: transport (_webdavCall), buildSyncSnapshot,
applyRemoteSnapshot (merge + tombstone arbitration), runSyncNow, and the
sync settings UI.
- Byte-for-byte identical to the extracted block (verified before removal);
no duplicate const; no top-level sync reference left in app.js.
- Load order: AFTER app.js (unlike crypto, which loads before) because this
module has a top-level side effect — `Bridge.onWebdavResult = …` — that
needs Bridge/state/api already declared. Rule documented in CLAUDE.md.
- index.html + BuildAssets whitelist + harness APP_PARTS updated; assets
rebuilt to embed the new file.
- Safety net: the existing merge tests exercise applyRemoteSnapshot /
buildSyncSnapshot from the extracted file and stay green (42/42).
app.js: 11936 → 11256 lines (crypto + sync now separate).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First slice of the app.js split. Approach: ordered classic-script files
loaded via separate <script> tags (argon2.js → app.crypto.js → app.js),
NOT ES modules / a bundler. Classic scripts share one global lexical
environment, so consts/functions cross-reference across files exactly as
in the monofile — zero call-site rewrites, near-zero risk. Chosen over the
audit's esbuild/ES-module suggestion because the code is written entirely
in global scope (functions call each other by bare name everywhere).
- js/app.crypto.js: KDF (PBKDF2 + Argon2id), verifier, AES-GCM encrypt/
decrypt, key persist/restore. Verified byte-for-byte identical to the
original block before removal; no duplicate const across the two scripts.
- index.html + BuildAssets whitelist + test harness updated for the load
order. Harness CONCATENATES app.crypto.js + app.js (node:vm doesn't share
top-level const across separate runInContext calls the way browsers share
it across <script> tags); argon2.js stays a separate IIFE.
- Runtime-validated: rebuilt exe unlocks via quick-unlock and loads/decrypts
entries — the extracted crypto (restoreCryptoKey, verifierFromKeyHex,
decryptPwd) works from the separate file. 42/42 tests green.
- Docs: CLAUDE.md "Découpage frontend" (pattern + rules), file map, tests
README, CODE_AUDIT §3.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 1 of CODE_AUDIT §1.2 — additive, no live account uses Argon2id yet.
- Vendor @noble/hashes@2.2.0 argon2id as js/argon2.js (esbuild IIFE exposing
globalThis.NobleArgon2). Pure-JS, not WASM: CSP is script-src 'self' with no
wasm-unsafe-eval, so WASM would require weakening it. Verified against the
RFC 9106 §5.3 test vector. Server needs zero Argon2 (zero-knowledge: it only
ever SHA256-wraps the client verifier).
- app.js: deriveKeyBytes(pwd, salt, algo, iters, argonParams) branches Argon2id
vs PBKDF2; deriveKeyAndVerifier refactored around it. New markers
HASH_ALGO_ARGON2='argon2id-v2' + ARGON2_DEFAULT_PARAMS (OWASP m=19MiB,t=2,p=1,
~0.65s/unlock). isDecoupledVerifierAlgo() generalises the decoupled-verifier
rule to any '-v2' scheme so argon2id-v2 inherits it. AES key is still ALWAYS
the raw KDF output → entries decryptable, legacy accounts untouched.
- index.html loads js/argon2.js before app.js; added to BuildAssets whitelist;
test harness loads it into the sandbox first.
- Tests: +5 (RFC 9106 vector via vendored bundle, argon2 branch derives Argon2
key not PBKDF2, decoupled verifier, AES round-trip under Argon2 key). 40/40.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two CODE_AUDIT items in one session.
§3.2 — Frontend regression net (js/tests/, 35 tests, node:test, zero deps):
- harness.js loads app.js (monofile, no exports) into a node:vm with browser
globals stubbed, surfacing internals via an export epilogue.
- crypto: deriveKeyAndVerifier (AES key == raw PBKDF2, cross-checked vs Node
pbkdf2Sync), legacy-vs-v2 verifier decoupling, encrypt/decrypt round-trip,
IV uniqueness, AEAD tamper/wrong-key.
- csv: parseCSV tokenizer, findColumn heuristics, Bitwarden/KeePass mapping.
- merge: applyRemoteSnapshot add/update/skip (LWW), tombstone delete,
resurrection arbitration (both NaN branches), local-tombstone veto,
additive folder merge. Only api() is stubbed; loadEntries/encryptImportEntry
run for real.
- Wired as a build gate in BuildAssets.ps1 (after node --check, bypass
PM_SKIP_TESTS=1).
§2.2 — Unify timestamps on UTC:
- Entry created_at/updated_at were written via Delphi FormatDateTime(Now)
= LOCAL, while deleted_at/tombstones use SQLite CURRENT_TIMESTAMP = UTC.
The tombstone-resurrection arbitration compared the two zones, skewing by
the machine's UTC offset even single-device.
- Add NowUTC/NowUTCStr to PM.Database, swap in at every entry/attachment
write site (Entries create/update/bulk, Attachments POST echo).
- No JS change needed: arbitration now compares same-zone values.
- Existing rows self-heal on next edit (no destructive migration).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The zero-knowledge verifier sent to /login used to be the raw PBKDF2
output in hex — i.e. the exact bytes of the AES key that encrypts every
entry. Intercepting a /login body (loopback, but still) handed over the
vault key. This introduces a decoupled scheme where the transmitted
verifier is a one-way function of the key.
New auth-hash scheme
- users.hash_algo 'pbkdf2-sha256-v2': the client sends
verifier = SHA256(keyHex + "pmserver/auth-verifier/v2") instead of
keyHex. Stored form is still SHA256(verifier) (identical server wrap
to 'pbkdf2-sha256'), so only the algo LABEL differs — it tells the
client which verifier formula to use. Verification needs no new server
branch (VerifierToStoredHash already SHA256-wraps any non-legacy
verifier).
- The AES key (cryptoKey) stays hex(PBKDF2) for EVERY algo, so entries
remain decryptable and switching schemes never re-encrypts data.
Adoption: new-registration + master-pw-change only
- Register and change-master-password write v2. Existing accounts keep
their algo until they rotate — the login/reauth migration signal now
fires only for LEGACY 'pbkdf2' (was: anything != CURRENT), so
sha256/v2 accounts are never force-migrated (which would have
downgraded v2 → sha256 via migrate-kdf).
Client (js/app.js): algo-aware everywhere
- verifierFromKeyHex(keyHex, algo) central helper; deriveKeyAndVerifier
/ computeVerifier take an algo arg. state.hashAlgo caches the account
scheme, set from /login/challenge, register, change-master, the
quick-unlock / PIN cold-start blobs, and the /recovery-key/redeem
response. All ~12 verifier sites updated (login, register, reauth ×4,
change-master current+new, migrate-kdf, quick-unlock + PIN cold-start,
recovery-mode current verifier).
Safety invariant: unknown/empty hashAlgo → key hex → byte-identical to
the old behaviour, so every pre-decoupling account (and every existing
quick-unlock / PIN blob without the new field) keeps working unchanged.
Verified: existing account + pre-change quick-unlock still unlocks; a
master-pw change now writes 'pbkdf2-sha256-v2' in vault.db.
Server: recovery redeem returns hashAlgo; register + change-master store
the decoupled algo; login + reauth migration signal narrowed to legacy.
Also: BuildAssets.ps1 pipes $null into node --check so the JS syntax
gate can't block on stdin in the Delphi pre-build environment.
Addresses CODE_AUDIT.md section 1.1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Quick search (Ctrl+Shift+Q fill mode)
- Enter / left-click → full autofill (username + Tab + password), like
Ctrl+Shift+L.
- Shift+Enter / right-click → username only (new Delphi username-only
SendInput path via field=user; ExecuteAutofill AUsernameOnly param).
- Ctrl+Enter / Ctrl+click → password only.
- Copy mode (tray / palette) unchanged: Enter/left = password,
Shift+Enter/right = username.
- Clipboard fix: copy-then-minimise no longer wipes the just-copied
password — MinimizeToTray takes an AClearClipboard flag (False on the
quick-search copy path, driven by app/minimize?keepclip=1). The 30s
auto-clear still guards it.
- Right-click on a result row suppresses the native/custom context menu
(preventDefault + stopPropagation).
Editable custom-field combobox
- Option-backed custom fields (card brand, expiry year/month, etc.) now
render a custom editable combobox instead of a locked <select>: an
arrow drops a menu of ALL options (a native <datalist> filtered to the
typed text, which confused users), while the input stays freely
typeable for values not in the list. Storage shape unchanged.
- Outside-click closes the menu via the existing slideover mousedown
handler; item mousedown + preventDefault so blur doesn't race the pick.
Build safety
- BuildAssets.ps1 runs `node --check` on every embedded .js before
generating assets.res. A syntax error now aborts the asset build
(exit 1, file + line logged) instead of shipping a dead bundle that
only surfaces after a full Delphi rebuild. Node is optional: absent →
warn and continue.
Docs
- CODE_AUDIT.md: full static-analysis report (security, latent bugs,
maintainability, future features, prioritized action plan).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.