feat(security): decouple the login verifier from the AES vault key

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>
This commit is contained in:
r-zakarya
2026-07-03 12:38:20 +01:00
parent 3076fec710
commit 3f8ecde571
6 changed files with 162 additions and 31 deletions
Binary file not shown.