Project 01·Cryptography·2026

Proving truth
without
revealing it.

A HIPAA-aware zero-knowledge proof system that proves a patient qualifies for a clinical trial without ever exposing their medical record, privacy enforced by mathematics instead of policy or trust.

Role
Sole engineer & designer
Domain
Healthcare privacy · HIPAA
Stack
Circom · snarkjs · Node · Flutter
Verifier console output showing each synthetic patient marked eligible or ineligible

Verifier output: each synthetic patient is proven eligible or rejected by the circuit, the sponsor sees only the result, never the record.

The problem

Why it matters

To enroll in a clinical trial, a patient normally hands their full medical record to the trial sponsor so the sponsor can check eligibility. The sponsor only needs one bit of information, do you qualify, yes or no?, but to get it they receive everything: diagnoses, lab values, medications, age.

HIPAA's "minimum necessary" rule says you should only share the minimum data required. In practice that rule is enforced by policy and trust: contracts, audits, and the hope that nobody misuses the data. I wanted to enforce it with math instead.

What I built

A system where the sponsor learns one bit and nothing else.

  1. A circuit that encodes the eligibility rules

    I wrote a Circom arithmetic circuit enforcing eight constraints over private inputs: an age range check (18–65), two required ICD-10 diagnosis codes verified by set membership, a consent flag, a lab value within range, and a medication-exclusion check.

  2. A zk-SNARK proof of those rules

    The circuit compiles to an R1CS constraint system and is proven with Groth16 zk-SNARKs (snarkjs) over the BN254 elliptic curve. The output is a ~192-byte proof that anyone can verify but that reveals none of the inputs.

  3. A commitment that binds each proof to one record

    Because the record's 25 private values exceed circomlib's 16-input cap, I built a two-stage Poseidon hash: commitment = Poseidon( Poseidon(age, 14 diagnosis codes, consent), Poseidon(lab value, 8 medication codes) ). This stops a proof being reused for a different patient.

  4. Two services that don't trust each other

    A hospital prover (Node / Express with AES-256-GCM-encrypted SQLite) holds the PHI behind a firewall and emits the proof. A separate sponsor verifier checks the proof against a public key and learns only eligible: true / false.

  5. Real interfaces on top

    A Flutter intake app, an EHR-style provider portal, and a public landing page with a math explainer. Validated end to end with 10 synthetic patients (3 eligible, 7 ineligible) through create → prove → verify.

How the commitment binds a proof

The part that makes it safe

A proof is only useful if it cannot be copied onto a different patient. That is the job of the commitment. Before any proof is made, the hospital hashes the record's 25 private values into a single number with Poseidon, a hash function built to run cheaply inside a circuit.

circomlib caps Poseidon at 16 inputs and the record has 25, so I split it: one hash over the age, the 14 diagnosis codes and the consent flag, a second hash over the lab value and the 8 medication codes, then a final hash of those two results. The circuit recomputes that commitment from the private inputs and forces it to equal the public commitment the verifier already holds.

Change one value and the commitment no longer matches, so the proof fails. That is what binds each proof to exactly one record, and why the sponsor can trust a result without ever seeing the data behind it.

Under the hood, the code

Two pieces carry the proof: the Circom circuit that encodes the eight eligibility constraints, and the snarkjs trusted setup that compiles it and confirms the proving system is sound.

Circom circuit source encoding the eight eligibility constraints
The circuit (Circom)
snarkjs trusted-setup and verification output
Setup & verify (snarkjs)

The circuit compiles to 4,856 constraints over the bn-128 curve with 25 private inputs and a single public output, eligible or not. Proving emits a ~192-byte proof that verifies in well under a second.

See it live

The full system, including the math explainer, is deployed. Explore the live build below.

hipaa-zkp.netlify.appOpen ↗

Embedded live. If it doesn't load (some sites block embedding), open it in a new tab via the link above.

What I learned

The hardest part wasn't the cryptography library, it was threat-modeling honestly. A proof is only meaningful if you also stop it being copied, reused, or computed over fake data, which is why the Poseidon commitment and the two-service separation matter as much as the SNARK itself.

If you're learning this: start with what a zk-SNARK actually guarantees (you know a secret that satisfies a public set of constraints) before touching Circom. Understand R1CS, then Groth16's trusted setup, then build the smallest possible circuit and prove it before adding constraints.

CircomGroth16 zk-SNARKssnarkjsBN254 curvePoseidon hashingAES-256-GCMNode / ExpressSQLiteFlutter
Back toAll work