OPRFs, secure value recovery, and zero-knowledge credentials
The mathematics Enchant uses to do the impossible jobs: recover your secrets from a server that must never learn them, and prove who you are without revealing who you are.
In the annals of applied cryptography, few problems are as elegantly framed as the one Enchant's backup system solves. The client wants to store an encrypted copy of its master key with a server. The server will one day be called upon to return it. And the entire arrangement must work even though the server is not to be trusted with the password, the key, or even the ability to test guesses offline. That last clause is the hard one, and it is why a plain "password-derived-key encrypts everything" scheme — the industry default for decades — is not good enough.
The tool that makes it possible is called an oblivious pseudorandom function, or OPRF. This post derives the construction exactly as it runs in the LibEnchant cryptographic library: the OPRF itself, the 2-of-3 secret sharing that protects it, and the zero-knowledge credential machinery that lets a client prove things about itself without ever revealing what it is.
The problem: a server that must not be able to guess
Set up the threat model first. A backup server holds, at all times, three things: a secret-shared evaluation key, a copy of the user's encrypted master key, and a counter of how many guesses the user has made. An attacker who steals the entire database — the classic server compromise — gets the encrypted blob and the shares. They do not get the password, and they must not be able to find it: a scheme where an offline attacker can run a dictionary of a billion passwords against the stolen blob is a scheme that has already failed. Every password manager that encrypts with a plain PBKDF-derived key has exactly this failure mode — offline guessing is limited only by the attacker's hardware.
The OPRF kills that attack at the root: the server holds a secret that is mixed into the key derivation, and it refuses to perform the mix more than a handful of times per account. Offline guessing becomes impossible because the missing ingredient (the server's secret evaluation key) is not in the stolen database. Online guessing is limited to ten tries, because the server counts them.
The building block: an oblivious pseudorandom function
What it is. An OPRF is a function that lets the client evaluate with the server's secret key — without the server learning the password, and without the client learning .
Why it is the right tool. The client and the server both need to contribute to the final key: the password (known only to the client) and the secret key (known only to the server). A normal PRF would require one side to hand its secret to the other. The OPRF's trick — the blindness — is that the client encrypts its input with a random scalar, the server evaluates on the blinded value, and the client unblinds the result. The server sees a random-looking group element and learns nothing about the password; the client receives but never sees .
How it is computed. All of this happens in the group of ristretto255
points — a prime-order group built on Curve25519, chosen because prime
order means no cofactor attacks, no small-subgroup tricks, and a
compact 32-byte representation for every point. The client's password
is mapped into the group deterministically — hashed into a point using
HKDF-SHA256 with the domain-separation label
"enchant_OPRFS_HashToGroup_20250101" (info = "hash_to_point"),
so the mapping cannot collide with any other protocol output:
The client chooses a uniform random scalar — the blind — and sends . The server multiplies by its secret key and returns . The client divides out its blind () and recovers — while the server has only ever seen , a uniformly random point that carries no information about the password:
Client (password) Server (secret k)
──────────────── ─────────────────
P = hash_to_group(password)
r ← random scalar
B = r·P ──────────────────────► B' = k·B (sees only B)
◄────────────────────────────── return B'
k·P = r⁻¹·B'
out = HKDF(P ‖ k·P, ...) (knows password, not k)The final OPRF output is derived from both the original point
and the evaluated point , through HKDF-SHA256 with the label
"enchant_OPRFS_Output_20250101" (info = "output"):
Two properties fall out of this construction. First, the server can
evaluate without knowing what it evaluated — it cannot learn the
password from , because the blindness is information-theoretically
hiding (a random group point). Second, the client's output is
uncorrelated with anyone else's: the hash-to-group step and the
final HKDF both bind the password to the output, so two users with the
same password get different keys. Enchant exposes this directly:
enchant_oprf_evaluate (server side) and enchant_oprf_finalize
(client side).
SVR3: splitting the server's secret
A single server with is a single point of failure — and a single point of abuse. If lives in one database, that database becomes the target of every coercion, every warrant, every break-in. The Secure Value Recovery scheme (SVR3) removes the single point: the evaluation key is split into three additive shares, and only two of three are needed to reconstruct it.
Where does the key come from? The subtle move in SVR3 is that the server's secret is derived from the password itself. Both the client and the server compute the same 64 bytes from the password:
The first 32 bytes become the OPRF key (reduced modulo the group order); the second 32 bytes are a key-confirmation hint the client uses to verify the password without the server learning it. Neither side stores this key: it is re-derived on every backup and restore.
How the shares work. The OPRF key is split into three additive shares over the scalar field of ristretto255 — random values whose sum is the key:
Each share is delivered to a different server. The three servers never
hold the whole key; they hold shares that are individually
meaningless. Two shares reconstruct the key (2-of-3 threshold); one
share reconstructs nothing. This is deliberately not Shamir
secret sharing — the threshold is fixed at 2, the arithmetic is
ordinary addition mod the group order, and the combination function is
a plain sum. In LibEnchant the operations are literally
split_additive_shares and combine_additive_shares.
What the split gives. An attacker who compromises one server gets one share — enough to prove they compromised the server, useless for recovering anything. An attacker who compromises two servers can reconstruct , but they still cannot recover the backup without the password, and they still cannot test passwords offline (the blinded evaluation requires the client's cooperation). And an attacker who wants to brute-force a password must now break two servers that each rate-limit — the online brute-force surface shrinks to whatever the weakest two servers allow.
The full backup and restore flows
Backup. The client has a master key (the seed for the whole keyring). To back it up:
- Derive from the password (both client and server do this independently).
- Split into three additive shares, one per server.
- Blind the password (), send the blinded point to each server, and have each server evaluate with its share: .
- Combine any two responses () and derive the OPRF output, exactly as above.
- Derive the encryption key from the OPRF output:
- Encrypt the master key with XChaCha20-Poly1305 under ; store the masked secret plus the hint on the servers.
Restore runs the same ladder in reverse: blind the password, get
evaluations from two servers, unblind, derive ,
decrypt the masked secret. The password never leaves the device in
any usable form, the servers never see it, and every guess the
attacker makes is met with a rate limit, not an oracle. Enchant's C
API exposes the full flow as enchant_svr_create_backup,
enchant_svr_restore_backup, and enchant_svr_change_pin.
The rate limit is load-bearing. Each server keeps a per-account
counter of failed guesses (SVR3_MAX_ATTEMPTS = 10). Ten wrong
passwords, and the server stops cooperating on that account. This is
not a minor feature; it is the only thing standing between an
attacker and an online dictionary attack. The blinding means the
server cannot verify a guess itself — it can only count attempts and
refuse further service. Authentication of legitimate clients rides on
a server-held HMAC proof (enchant_svr_generate_auth_proof /
enchant_svr_verify_auth_proof): a keyed MAC over the client ID and a
fresh nonce, so the server knows who it is talking to without a static
token that could be replayed.
Zero-knowledge credentials: proving without revealing
The OPRF protects one secret. A separate layer — the zkgroup system in LibEnchant — protects identity. The problem: a client must prove to a server that it holds some credential (a group membership, a profile key, a receipt) — without revealing which credential, or anything else about itself. The server must learn exactly one bit: valid or invalid.
The mechanism is a POKSHO (proof of knowledge of a signature with hidden origin) with blind credentials:
- Issuance. The server signs a credential — a group membership,
a profile key, a call-link ticket — producing a signature over the
credential's attributes. Enchant's zkgroup package carries a full
suite of these:
auth_credential,group_credential,group_send_endorsement(the proof that lets a member endorse a group message),call_link_credential,profile_key_credential,receipt_credential, andexpiring_profile_key_credential. - Blinding. The client blinds the credential before showing it,
via
enchant_blind_keygen/enchant_blind_point/enchant_unblind_point: the signer's public key is blinded with a random scalar so the server cannot recognize the signature it issued. - Presentation. The client produces a zero-knowledge proof
(
enchant_poksho_prove) that it holds a valid signature on the required attributes — without revealing the signature or the attributes. The server verifies (enchant_poksho_verify) and learns only "this client holds a valid credential of this type."
The math is the standard Schnorr-family trick lifted to ristretto255: the prover commits to blinded group elements, the verifier challenges, the prover answers with scalar linear combinations that reveal nothing about the underlying secrets. Because everything lives in the same prime-order group as the OPRF, the whole system shares one library, one implementation, and one threat model.
The unified picture
| Job | Tool | What it guarantees |
|---|---|---|
| Backup without offline guessing | OPRF + additive 2-of-3 shares | Stolen DB ≠ guessable password; servers never see it |
| Limited online guessing | Per-server rate limit (10) | Brute force dies at two compromised-but-counting servers |
| Key confirmation | Password-derived hint | Client verifies its password without server learning it |
| Proving membership | zkgroup POKSHO proofs | Server learns only "valid," never "who" |
| Authenticating clients | Server HMAC auth proofs | No replayable static tokens |
The through-line of every construction in this post is the same: the server holds exactly as much information as the math forces it to, and no more. The OPRF is built so the server evaluates without knowing; the shares are split so one server holds nothing; the proofs are written so the verifier learns one bit. This is zero-access by construction, and it is the same philosophy that powers zero-access servers and the Veil. In LibEnchant, it is not a diagram in a whitepaper — it is the code path a backup takes.
Continue reading

