Files
Password-Manager/js/tests
r-zakarya 5e88ad33d1 feat(crypto): adopt Argon2id (argon2id-v2) on register + master-pw change
Phase 2 of CODE_AUDIT §1.2 — live adoption of the Argon2id foundation.
Verified at runtime: a rotated account shows hash_algo=argon2id-v2 with
argon2_m=19456,t=2,p=1 in vault.db.

Server (never runs Argon2 — zero-knowledge, only stores/echoes params):
- DB: users.argon2_m/t/p columns (default 0 = PBKDF2).
- PM.Handler.Auth: HASH_ALGO_ARGON2 + param bounds, ReadArgon2Params /
  AppendArgon2Params helpers. /register and /change-master-password accept
  hashAlgo='argon2id-v2' + argon2:{m,t,p} and persist them; /login/challenge
  echoes them. Verify path (VerifierToStoredHash/CheckVerifier) is
  KDF-agnostic — the 64-hex verifier is SHA256-wrapped as for any -v2 scheme.

Client (app.js):
- state.argon2Params, cached from the challenge and persisted to
  sessionStorage + the quick-unlock / PIN cold-start blobs (so a cold-started
  session can still derive-from-password for reauth/rotation).
- Register + master-pw rotation derive with argon2id-v2 + ARGON2_DEFAULT_PARAMS
  (OWASP m=19MiB,t=2,p=1) and send the params. Rotation re-encrypts the whole
  vault under the new Argon2 key (natural migration point). Existing accounts
  stay PBKDF2 until they rotate.
- Params threaded through every derive-from-password site (login, reauth,
  recovery setup, change-pw current verifier). Cold-start verifier-from-raw-key
  paths need no params (isDecoupledVerifierAlgo handles the -v2 wrap).

Tests: +2 param-contract tests (register<->login determinism, param
sensitivity). 42/42. Assets rebuilt to embed js/argon2.js.

Docs: CLAUDE.md auth-hash section rewritten (4 markers); CODE_AUDIT §1.2 +
table + plan marked done.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 14:52:11 +01:00
..

Frontend unit tests

Regression net for the highest-risk pure/near-pure logic in js/app.js: crypto round-trip, KDF/verifier derivation, CSV import parsing, and the sync-merge / tombstone-resurrection arbitration. Addresses CODE_AUDIT.md §3.2 (aucun test automatisé).

Running

npm test
# or directly:
node --test "js/tests/**/*.test.js"

Zero dependencies — uses the Node built-in test runner (node:test) and webcrypto. Requires Node ≥ 18 (developed on v24). Runs in ~1.7 s.

How it works — harness.js

app.js is a ~12k-line browser monofile with no module exports and one top-level side effect (a DOMContentLoaded listener). The harness loads the file's source into a node:vm context with browser globals stubbed (crypto, localStorage, document, location, …) so init() never fires, then appends an export epilogue that surfaces the internals on globalThis.__test.

Two gotchas the harness works around, both documented inline:

  • const/let don't attach to the vm global. Top-level function/var declarations become properties of the context global, but const state, const HASH_ALGO_V2, etc. do not — hence the explicit export epilogue.
  • Cross-realm prototypes. Values returned from the sandbox carry the sandbox realm's prototypes, so assert.deepStrictEqual trips on the prototype check. Structural comparisons normalize through JSON first (see eqDeep in csv.test.js).

The merge tests stub only the api() seam (a function declaration → overridable global property) with an in-memory fake server; loadEntries, loadFolders, and encryptImportEntry all run for real against it — so the tests exercise the actual pull→merge path, not a re-implementation.

Suites

File Covers
crypto.test.js deriveKeyAndVerifier (AES key == raw PBKDF2, cross-checked vs Node's pbkdf2Sync), legacy vs -v2 verifier decoupling, encryptPwd/decryptPwd round-trip, IV uniqueness, AEAD tamper/wrong-key → [ERROR]
csv.test.js parseCSV tokenizer (quotes, escaped "", CRLF, trailing field), findColumn header heuristics, parseEntriesFromCSV for Bitwarden/KeePass shapes, note-vs-login classification
merge.test.js applyRemoteSnapshot: add/update/skip (last-write-wins), tombstone delete, resurrection arbitration (both NaN branches), local-tombstone veto, additive folder merge

Notes on encoded behavior

merge.test.js locks in one deliberately-asymmetric behavior: an unparseable local updated_at favours KEEP (resurrection wins), but an unparseable remote deleted_at still applies the delete. In production deleted_at is always server-formatted (parseable), so this only matters for a corrupted remote snapshot. If that arbitration is ever changed, the two unparseable … tests are where to update the expectation.