Picture the worst Monday: someone walked off with a dump of your user table. Emails, names, and the password column.
The good news is you don't store passwords in the clear, you store hashes with Argon2id. The bad news is that matters less than it seems.
Why the hash alone isn't enough
Argon2id makes each guess cost memory and time. It's a real defense and you must use it. But notice what holds it up: making each of the attacker's guesses expensive.
And the attacker who has your database plays by other rules:
- They're in no hurry. No rate-limit, no account lockout, no logs to give them away. They can take months.
- They don't guess at random. They start with leaked password lists, which cover an uncomfortable fraction of any real user base.
- They don't use your server. They use GPUs rented by the hour.
Raising Argon2's cost helps, but you pay it on every legitimate login and they pay it once per candidate. The arithmetic isn't on your side: if a password is in the first ten thousand of a leaked list, no reasonable parameters save it.
The underlying problem is that the attacker has everything they need. The hash, the salt and the parameters are all in the same dump. They can compute the whole function on their own.
The idea: leave them missing a piece
What if computing the hash depended on a secret that is not in the database?
The simple version is the pepper: a key in the application config that's mixed with the password before hashing. It helps, and it has two serious problems:
- It usually leaks with the database. If the attacker got into the server, the environment variable was one
cataway. - It can't be rotated. Changing the pepper invalidates every hash at once.
An OPRF solves the first one at the root: the secret lives in another service, and the elegant part is that this service never sees the password.
What an OPRF is, without the math
An Oblivious Pseudo-Random Function is a two-party computation with this odd property:
- You send it your password blinded — mathematically masked. The server receives noise.
- The server applies its secret key
kto that noise and returns more noise. - You unblind it and get a value that can only be obtained with that key
kand that password.
The server doesn't learn your password. You don't learn its key. And without talking to it, no one can compute that value.
That value is what you hash with Argon2id and store.
Now the attacker with your database has the hash, the salt, the parameters... and is missing k. They can't test a single candidate offline. To attack, they have to talk to the OPRF service once per guess — and there you do have rate-limits, quotas and logs.
They've turned an unlimited offline attack into a measurable online one. That's the whole trick, and it's huge.
The part almost no one implements: the 'verifiable'
There's a catch. If the OPRF server is compromised or simply uses the wrong key, it returns garbage values and you don't notice: you hash garbage, store it, and all your users get locked out. Or worse, someone controlling the server could respond with a key they know.
That's why the variant that matters is the verifiable one (VOPRF, RFC 9497): the server attaches a DLEQ proof that it used the right key, and the client verifies it against a public key it has pinned.
And here's the detail that makes half the integrations you'll see out there decorative: that public key has to be pinned out of band. If your client asks the very server it's about to verify for the key, you've verified nothing — you've asked the examinee to bring their own answers.
It's an easy mistake to make. I made it: two of my own clients would fetch the key from the server if the integrator omitted it. It was fixed by making it mandatory.
What breaks, said before it happens to you
No architectural decision is free, and this one has three real costs:
1. Your logins depend on another service. If the OPRF doesn't respond, you can't verify passwords. And here the only correct answer is to fail closed: if the service is down or the proof doesn't validate, the login fails. The temptation to 'degrade to unhardened' turns an outage into a bypass — and an attacker who can take the service down skips the whole protection.
2. The key k never rotates. Rotating it invalidates every secret hardened with it. It's a once-and-for-all decision, and you have to treat it as such.
3. It adds latency to login: one network round trip. Measurable, small, but not zero.
If your application can't accept point 1, this isn't for you. I'd rather say it here than afterward.
How to use it in Django
I wrote the primitives in Rust following RFC 9497 and verifying them against the official vectors in Appendix A.1.2, and packaged them for Python. The Django integration is a hasher that plugs in where you already have yours:
It is free software. Since August 2026 the code is no longer published on registries or on GitHub — we work in security, and having our source extracted would be an argument against the product: it is delivered on request, signed and with its SHA-256, at contacto@xiliux.com.
# settings.py
PASSWORD_HASHERS = [
"quipu_oprf_django.hashers.OprfArgon2PasswordHasher", # preferred
"django.contrib.auth.hashers.Argon2PasswordHasher", # migration
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
]
QUIPU_OPRF = {
"BASE_URL": os.environ["QUIPU_OPRF_URL"],
"API_KEY": os.environ["QUIPU_OPRF_API_KEY"],
# 64 hex, PINNED OUT OF BAND. Do not ask the server for it.
"PUBLIC_KEY": os.environ["QUIPU_OPRF_PUBKEY"],
"TIMEOUT": 5.0,
}
The migration is gradual: existing users keep logging in with their old hasher and get hardened the next time they sign in.
The package is Apache-2.0 on purpose. It lives inside your authentication server, and putting a network-copyleft license in there would be indefensible.
Self-host it or pay for it
The server is open source (AGPL) and you can run it yourself: it's a binary with SQLite and needs nothing more. If you do that, you pay me nothing and that's perfectly fine.
What oprf.xiliux.com charges for is not having to operate it: availability, quota, and making sure the key k survives your deployments. Because the day you lose that key, you lose all the passwords at once.
What this is not
It's not magic and it doesn't replace anything you should already be doing: keep using Argon2id with serious parameters, keep a second factor, keep limiting attempts.
What it does is take away the attacker's most profitable move against a stolen database. That's not nothing.
The code is at github.com/isazajuancarlos/quipu. The VOPRF primitives are Apache-2.0 and RFC 9497-compliant. The project does not yet have an independent cryptographic audit, and that has to be said too.
Xiliux