ZK-Auth explores a promising direction for high-assurance authentication: combine memory-hard client-side KDFs, server-side peppered commitments, and transparent proof-system techniques. The live demo is intentionally labeled as a research prototype: it verifies a MiMC/FRI consistency wrapper over a client-side KDF output, but it does not yet prove full Argon2id or bcrypt execution inside the STARK.
The project is useful and technically interesting, but its current claims need to be precise. Treat the submitted proof body and encoded KDF output as request-scoped credential material. The server must verify them, compute the peppered commitment, and then discard them.
| Item | Status in this prototype | Accurate interpretation |
|---|---|---|
| Raw password | Not intentionally transmitted | Stays in browser memory; never serialised or sent |
| Argon2id encoded output | Request-scoped credential material | Used to bind the proof transcript; server must not log or store it |
| Server pepper | Server-only | Stored in server env / HSM; client never sees it |
| Commitment C = Poseidon(mimc_output, pepper) | Server computes at registration | Client sends mimc_output; server applies pepper itself |
| STARK-style proof | Sent and verified | Checks MiMC algebraic consistency, Merkle openings, sampled FRI folds, transcript binding, and PoW |
| Argon2id salt & params | Public | Stored in DB; returned to client on login so it can re-run Argon2id |
The system supports several demo paths with different security / complexity trade-offs. Pattern 2 is the primary focus: it keeps the pepper server-side and stores only a peppered commitment, while acknowledging that full Argon2id-in-STARK proof support is future work.
A classical bcrypt or Argon2id hash is computed client-side and sent to the server. This live API path is disabled because it is not a STARK-backed authentication proof. It remains useful only as a conceptual baseline.
Legacy mode where client-side code computes a pepper commitment. It is useful for experimentation, but it requires pepper material to be available to the client execution context and should not be used for serious deployments.
The client generates a STARK-style consistency proof with no pepper computation at all.
After the server verifies the proof, it independently computes
Poseidon(mimc_output, serverPepper) and compares against the stored
commitment. The pepper is an opaque server secret the client never observes.
No external KDF. The password itself is encoded as the circuit witness via
hashBytesToWitness. Useful for constrained environments but lacks
Argon2id's memory-hardness against offline attacks.
The cryptographic foundation. Provides finite-field arithmetic over the BN254
scalar field (p ≈ 2¹⁵⁴), the Number Theoretic Transform (NTT),
the friFold function, Merkle trees, the Poseidon-2 hash, and
the Fiat-Shamir Transcript sponge with integrated PoW grinding.
Exported as window.StarkMath in browsers and
module.exports in Node.js. Used by both prover and verifier.
Runs entirely in the browser. Accepts a password and server challenge, runs Argon2id (WASM), derives the MiMC witness from the encoded KDF output, builds the MiMC-7 execution trace, performs the Low-Degree Extension, constructs the quotient polynomial, commits via Merkle trees, runs FRI, grinds PoW, and returns the complete proof object.
In Pattern 2 mode (algo: 'argon2id-p2'), pepper_commit
is deliberately omitted from public_inputs.
Runs on the server (Node.js). Re-derives mimcKey from the proof
nonce and server secret, replays the Fiat-Shamir transcript, checks all Merkle
inclusion proofs, enforces AIR constraints at each query index, checks FRI
consistency across all folds, and validates the PoW nonce.
Exposes verifyPepperCommitP2(mimc_output, storedC) separately —
called by the routing layer after verify() succeeds in Pattern 2.
Uses timing-safe comparison for all commitment checks.
Demonstrates the full Pattern 2 registration and login flow. Fetches a server
challenge, runs Argon2id in-browser via WASM, generates a STARK proof, and
POSTs to /api/p2/register or /api/p2/login. Displays
live step-by-step progress with success/error states.
Complete specification of the field, hash functions, cipher, proof system parameters, and security argument for the ZK-Auth STARK construction.
All arithmetic is performed modulo the BN254 scalar field prime.
Used for the Fiat-Shamir transcript sponge, the KDF chain commitment, Merkle tree leaf/node hashing, the round-constant commitment, and the pepper commitment.
Argon2id is the OWASP-recommended password hashing function. It is memory-hard, defeating GPU and ASIC brute-force attacks. The following parameter floor is enforced by both prover and verifier. In this prototype, Argon2id still runs outside the AIR; the proof binds to the resulting encoded KDF string and checks the downstream algebraic wrapper.
encodedStr changes h1, which changes the transcript nonce, which
invalidates every Merkle path and FRI check in the proof. This is a useful integrity
check, but it means the encoded string is credential material during the request and
must be discarded after verification.
MiMC is a block cipher designed for ZK-friendliness — its step function is a single low-degree polynomial over the field (degree 7 with the x⁷ exponent). This makes it directly expressible as an Algebraic Intermediate Representation (AIR) constraint.
MiMC is attractive here because the transition is a low-degree polynomial, which is exactly the kind of computation STARKs like to prove. The live demo should be read as a prototype construction, not as an independently audited MiMC security claim.
The STARK-style proof encodes the MiMC execution trace as a polynomial, checks that it satisfies the MiMC AIR constraint everywhere on the trace domain, then uses FRI to prove the resulting quotient polynomial is low-degree at sampled openings.
Both prover and verifier must absorb values in exactly this order. Deviations cause all challenge values to diverge, making every check fail.
| Component | What it gives today | Status |
|---|---|---|
| FRI query sampling | 35 sampled openings over a 32x LDE give a strong consistency check for the MiMC wrapper | Prototype heuristic |
| Fiat-Shamir PoW grinding | Requires about 2^24 SHA-256 trials to find an accepted transcript grind nonce | Concrete work factor |
| MiMC-7 algebraic security | Low-degree field transition suitable for AIR experiments; constants and parameters are not third-party audited | Research primitive use |
| Argon2id memory-hardness | Applies to client-side password derivation and offline guessing cost; not proven inside the STARK yet | Real KDF, external to AIR |
| Timing-safe comparisons | All commitment checks use crypto.timingSafeEqual (Phase 3) | Implementation hardening |
| Session replay prevention | server_challenge absorbed first; each proof bound to one login | Protocol hardening |
| Production-grade ZK claim | A legitimate 128-bit soundness claim requires an audited proof backend and an AIR/zkVM that includes Argon2id/bcrypt itself | Future work |
Step-by-step walkthrough of Pattern 2 — the primary auth mode — showing how a browser-side Argon2id result is transformed into a stored peppered commitment. The password is not sent; the proof body and encoded KDF output are request-scoped credential material and must be discarded after verification.
C = Poseidon(mimc_output, serverPepper).
The client runs Argon2id locally, then the prototype proof verifies the downstream MiMC/FRI
consistency wrapper over that KDF-derived witness. The server applies the pepper itself —
the client never computes or sees C.
The user types their username and password into the browser. The password is not sent over the network by this flow. As with any browser-side password system, the page must avoid logging it and must be served over trusted HTTPS.
The client calls GET /api/nonce. The server returns a freshly
generated 256-bit cryptographically random hex string. This nonce is absorbed
first into the Fiat-Shamir transcript, binding the entire proof to this specific
login session. Replaying the proof against a different session is impossible.
The client generates a 16-byte random salt using crypto.getRandomValues.
Argon2id is then run in-browser via WebAssembly with parameters at or above the
enforced floor (t ≥ 3, m ≥ 65 536 KB). The result is the full encoded string —
including the parameters and base64-encoded hash — which is used to derive the
MiMC witness. That encoded string is not stored, but it is request-scoped
credential material while verification is happening.
The MiMC encryption key is not fixed — it is derived fresh for each session using HMAC-SHA256. The key material depends on both the hash witness (nonce) and the server secret, making it unpredictable to an attacker who does not know the server secret. The server independently re-derives this key during verification.
The prover builds a 256-row execution trace. The first row is the field witness derived from the encoded Argon2id output. Each subsequent row applies one MiMC-7 step: add the key and round constant, then raise to the 7th power. This is the computation the consistency proof will check.
The 256-row trace is interpolated into a polynomial via inverse NTT, then evaluated at 8 192 points (blowup factor 32). This "low-degree extension" is committed to with a Merkle tree. The same is done for the quotient polynomial and the KDF trace.
The Fast Reed-Solomon IOP (FRI) protocol interactively reduces the claim that the combined polynomial is low-degree. Each fold halves the domain size (8192 → 4096 → … → 16). Random folding challenges are drawn from the transcript after each Merkle commitment, making the protocol non-interactive (Fiat-Shamir).
After all commitments are absorbed, the prover must find a 32-bit nonce N such that the first 24 bits of SHA-256(transcriptState ‖ N) are zero. This requires ~16 million SHA-256 evaluations on average, making it computationally expensive to forge transcripts by trying different commitment sequences.
The verifier challenges the prover with 35 random indices (drawn from the transcript). At each index, the prover opens the trace polynomial, the quotient polynomial, and all FRI layers with Merkle inclusion proofs. These openings form the bulk of the proof blob.
The complete proof is POSTed to /api/p2/register. Crucially, in Pattern 2,
pepper_commit is absent from public_inputs.
The client never computed it. mimc_output is present as a public output
of the circuit.
The server's ZKVerifier.verify(proof, {skipPepperCheck: true})
performs the prototype consistency verification: re-derives mimcKey, replays the transcript,
checks all Merkle paths, verifies the AIR constraint at all 35 query indices,
checks FRI consistency across all 9 folds, and validates the PoW nonce.
After proof verification succeeds, the routing layer calls
verifyPepperCommitP2. During registration, the server
computes C and stores it alongside the username and Argon2id salt.
The pepper is read from the server environment (HSM in production).
The client never sees this value. The proof body, encoded Argon2id output,
and intermediate witness are discarded after the request.
C. To reverse it they need both
mimc_output (requires running MiMC on Argon2id output) and the pepper
(server secret). Brute-forcing requires running Argon2id + MiMC-7 per password guess.
Identical to registration: the client calls GET /api/nonce and receives
a fresh 256-bit challenge. This is the first value absorbed into the Fiat-Shamir
transcript, preventing replay of a previously valid proof.
The client calls GET /api/p2/salt/:username. The server returns
the Argon2id salt and parameters that were stored at registration. The salt is
public — knowing it without the password yields nothing.
Argon2id is a deterministic function: given the same password, salt, and parameters, it always produces the same output. The client re-runs Argon2id with the retrieved salt to reproduce the same field witness that was used during registration.
Steps 4–9 of registration repeat identically: derive mimcKey, build MiMC trace,
LDE, Merkle trees, FRI folds, PoW grinding, and query openings. The resulting
mimc_output = trace[220] will match the registration value only
if the KDF-derived witness is the same — which happens only if the password is correct.
The server runs the consistency verifier (verify(proof, {skipPepperCheck: true})),
confirming that the submitted proof is structurally valid and internally consistent.
It checks the MiMC/FRI wrapper and binding to the submitted encoded KDF output;
it does not yet prove Argon2id itself inside the STARK.
After the consistency proof is verified, verifyPepperCommitP2(mimc_output, storedC)
is called. The server recomputes Poseidon(mimc_output, serverPepper) and
compares it — using timing-safe equality — to the stored commitment C. If they match,
authentication succeeds.
mimc_output. The pepper check will fail. A real production-grade
ZK password proof would strengthen this by proving Argon2id/bcrypt itself inside
the proof system.