Le frontend etait lisible dans l'onglet Image de Process Explorer, avec
les commentaires. BuildAssets.ps1 brouille chaque fichier avant brcc32,
PM.EmbeddedAssets le debrouille au moment de servir.
C'est de l'opacite, pas du secret : la cle est dans l'exe. Ca supprime
strings et l'onglet Image ; la memoire, un debogueur et la reponse HTTP
rendent toujours le texte. D'ou XOR plutot qu'AES — a securite egale
(nulle contre qui a l'exe), dix lignes, et aucune force affichee qu'on
n'a pas.
Une magie PMA1 prefixe le clair : sans elle, deux constantes qui derivent
serviraient du binaire au navigateur, ecran blanc sans trace.
Verifie en comparant les flux des deux cotes bloc par bloc, puis par
aller-retour sur app.js, index.html et style.css. Le frontend ne se
trouve plus dans le binaire.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
app.js 9363 -> 7962 lines. Three new classic-script modules:
- app.attachments.js (250): blob crypto + upload/download UI, pure
declarations, loads before app.js
- app.autofill.js (276): Win32 combos, title->entry matching, picker,
pure declarations, loads before app.js
- app.unlock.js (907): Quick Unlock + PIN + recovery code grouped (same
"enter without master pw" theme); assigns Bridge.onPinResult /
onQuickUnlockResult at top level so it loads AFTER app.js, like app.sync.js
Audit viewer stays in app.js (only 65 lines, not worth a file). Clipboard
bridge helpers stay too (were interleaved in the quick-unlock section but
unrelated). Registered in BuildAssets whitelist + index.html + APP_PARTS.
Verified in-app: quick unlock cold-start, attachment upload/download.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The unit suite concatenates + parses every embedded app.*.js (plus argon2)
in a vm, so a syntax error already fails it. Run the ~10 cold `node --check`
spawns (~3-4s) only when tests are bypassed (PM_SKIP_TESTS) or absent.
Cuts the pre-compile freeze from ~7s to ~4s.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Eighth slice. Quick search wasn't contiguous — its functions sat on both
sides of the cheatsheet and password-history modals (lines 725-1104). Rather
than a fiddly non-contiguous cut, the whole overlay cluster is extracted as
one byte-identical block: js/app.overlays.js (quick search + cheatsheet +
password history). Pure declarations, no top-level side effects → loads
before app.js; all state/api/Bridge/render/decryptPwd refs resolve via shared
global scope at call time.
- Byte-for-byte identical; syntax OK on all nine app parts; 62/62 tests green.
- index.html + BuildAssets whitelist + harness APP_PARTS updated.
app.js: 11936 → 9170 lines (8 modules extracted, ~2770 lines). Load order:
argon2 → crypto → totp → favicon → import → backup → health → overlays →
app → sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Seventh slice of the app.js split. Moves the vault-health dashboard
(computeHealthCache, healthScoreBand, renderHealthDashboard/Section,
openEntryForFix, entryAgeDays + scoring consts) to js/app.health.js. Pure
declarations, no top-level side effects → loads before app.js. Uses
computeStrength/decryptPwd/state/api via shared global scope at call time.
- Byte-for-byte identical extraction; syntax OK on all eight app parts.
- auditCache/auditFilter sit in this var block but drive the separate
Audit-log viewer in app.js — they ride along and resolve cross-file via
shared scope (documented).
- index.html + BuildAssets whitelist + harness APP_PARTS updated.
- 62/62 tests green.
app.js: 11936 → 9545 lines (7 modules extracted).
NOTE: quick search is NOT contiguous (interleaved with cheatsheet +
history-modal code, lines 886-1063), so a clean byte-identical extraction
isn't trivial — deferred.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sixth slice of the app.js split. Moves the favicon fetch/cache section
(faviconHost, saveEntryIcon, ensureEntryFavicon, backfillFavicons,
clearAllFavicons) to js/app.favicon.js. Pure declarations, no top-level
side effects → loads before app.js.
- Code moved byte-for-byte; no duplicate const; syntax OK on all 7 app parts.
- NEW: js/tests/favicon.test.js — 7 tests for faviconHost, the pure
URL→validated-hostname function that decides which domain is sent to the
DuckDuckGo proxy (a bug there leaks the wrong host). Covers scheme/www/
path/port stripping, non-hostname rejection, malformed dotting, unsafe
chars, and the 253-char DNS cap.
- Fixed an inaccurate source comment surfaced by the tests: it claimed raw
IPs "stay valid", but the TLD rule /\.[a-z]{2,}$/ rejects a numeric final
label, so IPs get no favicon lookup (fine). Test pins the real behaviour.
- Suite: 55 → 62 tests, all green. Assets regenerated (8 ordered JS files).
app.js: 11936 → 9790 lines (6 modules extracted).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fifth slice of the app.js split. Moves the scheduled encrypted-backup
feature (config, retention, runAutoBackupNow/runAutoBackupIfDue) to
js/app.backup.js. Pure declarations + two consts, no top-level side effects
→ loads before app.js; uses encryptExportPayload (app.import.js), Bridge,
api, state via shared global scope at call time.
- Byte-for-byte identical extraction; no duplicate const; no top-level
backup reference left in app.js; syntax OK on all six app parts.
- index.html + BuildAssets whitelist + harness APP_PARTS updated; assets
regenerated (manifest embeds all 7 ordered JS files).
- 55/55 tests green.
app.js: 11936 → 9900 lines — now under 10k. Five modules extracted
(~2000 lines): argon2 → crypto → totp → import → backup → app → sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fourth slice of the app.js split. Moves TOTP (base32Decode, generateTOTP,
parseOtpAuthUri) plus the TOTP-secret and custom-field AES-GCM wrappers to
js/app.totp.js. Loads before app.js (pure declarations), after app.crypto.js
(uses encryptPwd/decryptPwd). Also called by app.import.js and app.sync.js
via shared global scope.
- Byte-for-byte identical extraction; no duplicate const; syntax OK on all
five app parts.
- NEW: js/tests/totp.test.js — 13 tests including the 5 RFC 6238 Appendix B
reference vectors (generateTOTP reads Date.now(), so each case stubs the
sandbox clock to the vector's fixed time), base32 decode edge cases, and
parseOtpAuthUri. Extraction AND new coverage in one slice.
- Suite: 42 → 55 tests, all green.
- Assets regenerated (manifest now embeds all 6 ordered JS files:
argon2 → crypto → totp → import → app → sync); also fixes the previous
import commit's not-yet-rebuilt manifest.
- Delphi build artifacts (*.vrc, *.$manifest) gitignored.
app.js: 11936 → 10138 lines (4 modules extracted, ~1800 lines).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.