Files
Password-Manager/js/tests
r-zakarya 2bd0fcfbf8 feat(crypto): Argon2id KDF foundation (vendored, not yet adopted)
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>
2026-07-05 14:13:36 +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.