OPAQUE Password Authentication in TypeScript: OPRFs, Envelopes, and 3DH
Published · 23 minute read
Topics: OPAQUE, augmented PAKE, OPRF, 3DH, password security, TypeScript, WebCrypto, RFC 9807
Plain English: OPAQUE lets a website verify that you know your password without the website receiving that password. A successful login gives your browser and the server the same temporary secret key, which they can use for the session.
Short version. OPAQUE password authentication is an augmented password-authenticated key exchange (aPAKE) protocol standardized as RFC 9807. It lets a client prove knowledge of a password and establish a mutually authenticated session key without sending that password to the server. The protocol combines an oblivious pseudorandom function (OPRF), client-side password hardening, a password-protected credential envelope, and an authenticated key exchange such as 3DH. Registration stores a credential record containing the client public key, a masking key, and an envelope; it does not store the plaintext password. During login, the correct password reconstructs the client's private key and unlocks the envelope, after which both parties authenticate the transcript and derive the same fresh session key. OPAQUE does not prevent online guessing or make weak passwords safe. If an attacker steals the complete server state, offline guesses remain possible, but they must be performed per user after the breach rather than through a precomputed table.
A normal password login has an awkward moment that most diagrams hide: the server receives the password in plaintext after TLS decryption. Maybe it hashes the value immediately. Maybe an application trace records the request body first. Maybe TLS ends at a reverse proxy outside the security boundary. The password has already crossed into server-controlled memory.
OPAQUE changes that contract. The password stays on the client, yet the server can authenticate the client and both sides finish with the same high-entropy session key. No certificate-based PKI is required during login. Registration still needs an authenticated confidential channel, and TLS remains sensible defense in depth.
I traced the full flow through the opaque-ts repository: client and server state machines, OPRF evaluation, envelope recovery, 3DH key agreement, transcript MACs, serialization, fake records, and vector tests. The code makes a dense protocol much easier to inspect because each cryptographic responsibility lives in a separate module.
No cryptography background required
Read the next three sections first if PAKE, OPRF, envelopes, and key exchange are new terms. They explain the idea without curve arithmetic. The later sections repeat the same journey using the actual message names and TypeScript modules.
Contents
- 1. OPAQUE in one minute
- 2. Plain-English glossary
- 3. Alice and Bob: the whole story
- 4. What OPAQUE actually solves
- 5. The four protocol pieces
- 6. Registration, message by message
- 7. Login: KE1, KE2, and KE3
- 8. How the TypeScript implementation maps to the protocol
- 9. Security boundaries and failure modes
- 10. What OPAQUE is not
- 11. Production checklist
OPAQUE in one minute
Start with an ordinary website login. You type a password into the browser. TLS encrypts it while it travels across the internet. At the website's server, TLS is removed and the application receives the original password. The application hashes it and compares the result with a stored value. The network was protected, but the server still saw the password.
OPAQUE moves the password check into a cryptographic conversation. The browser transforms the password into a blinded message before sending anything. The server applies its own secret to that blinded value without learning the password. Only the browser can finish the calculation. If the password is correct, the browser recovers its cryptographic identity and both sides derive the same fresh session key. If the password is wrong, the recovered material is nonsense and the final authentication checks fail.
Think of the server as holding a locked credential box and one part of the unlocking process. The client holds the password, which is the other part. The server can help the client try to open the box, but it cannot see the password used in the attempt. This is only a mental model—the code uses elliptic-curve group operations, HKDF, MACs, and an authenticated envelope rather than a literal box.
password stays on client
|
v
blind password ---------> server applies secret OPRF key
|<------------- server returns evaluated element
v
unblind + run scrypt
v
recover client credentials from envelope
v
run mutual 3DH authentication with server
v
both sides obtain the same fresh session keyOne sentence to remember: the password unlocks a client identity, and that identity performs the login. The server checks cryptographic proof produced by that identity instead of receiving the password and hashing it itself.
Plain-English glossary
Protocol writing becomes unreadable when every noun is an acronym. These are the terms used throughout the rest of the article.
- Hash function
- A one-way transformation from input bytes to a fixed-size result. The same input produces the same result. Hashing alone does not stop password guessing.
- Salt
- A unique value mixed into password hashing so two users with the same password do not have the same stored result. A normal salt is public; OPAQUE uses a server-secret OPRF contribution instead.
- Password hardening or KSF
- An intentionally expensive calculation such as scrypt or Argon2 that makes every password guess consume more time and memory.
- PAKE
- Password-Authenticated Key Exchange. Two parties use a password to authenticate and create a strong shared key without placing the password itself on the wire.
- Augmented PAKE or aPAKE
- A client-server PAKE where only the client knows the password. The server stores a credential record instead of a password-equivalent secret.
- OPRF
- Oblivious Pseudorandom Function. The client supplies a hidden input, the server supplies a secret key, and only the client learns the final output.
- Blinding
- Randomizing the password-derived group element before the server sees it. The client can later remove the randomization; the server cannot inspect the original input.
- Credential envelope
- Password-protected authentication material stored by the server for the client. A correct password recreates the keys needed to verify and use it.
- AKE
- Authenticated Key Exchange. A protocol where both parties prove who they are and agree on a new shared session key.
- 3DH
- Triple Diffie-Hellman. Three key-agreement calculations combine long-term identity keys with fresh one-time keys.
- MAC
- Message Authentication Code. A keyed integrity check proving that a message came from someone holding the expected secret and was not changed.
- Session key
- A fresh high-entropy key agreed during one login. It can authenticate or encrypt later application traffic; it is not the password.
- Forward secrecy
- Protection for old session keys if a long-term key or password is exposed later. Past recorded sessions should not become readable from the later leak.
Alice and Bob: the whole story
Cryptography papers call the user Alice and the server Bob. Here Alice is a browser user creating an account at Bob's service. She chooses correct horse battery staple as an example password. The words never need to reach Bob.
Account creation
- Alice's browser converts the password to bytes and blinds it with fresh randomness.
- Bob receives only the blinded group element. He derives an OPRF key tied to Alice's credential identifier and evaluates the blinded element.
- Alice receives the evaluation, removes her blinding, and runs the result through scrypt. Bob cannot reproduce this client result from the message he saw.
- Alice derives an authentication key, export key, masking key, and a stable client key pair. In this implementation the private key is derived from the randomized password; it is not stored as plaintext inside the envelope.
- Alice creates the envelope authentication tag and uploads a registration record. Bob stores it under her account.
Every later login
- Alice blinds the entered password again and creates a fresh one-time key share. This becomes
KE1. - Bob evaluates the blinded password, masks the stored envelope, creates his own fresh key share, and computes proof that he holds the server keys. This becomes
KE2. - Alice unblinds the OPRF result and tries to recover her credentials. The wrong password derives the wrong keys, so the envelope tag cannot verify.
- With the correct password, Alice verifies Bob's proof and sends her own proof in
KE3. - Bob verifies
KE3. Alice and Bob now hold the same session key even though Bob never received the password.
What Bob stores
| Stored item | Purpose |
|---|---|
| Server OPRF seed | Derives the secret OPRF key associated with a credential identifier. |
| Server AKE key pair | Provides Bob’s long-term cryptographic identity. |
| Client public key | Lets Bob authenticate the client during 3DH. |
| Masking key | Hides the server public key and envelope inside the login response. |
| Envelope | Carries the nonce and authentication tag needed for client credential recovery. |
Bob does not store Alice's plaintext password, client private key, finished OPRF output, or previous session keys. Protecting the OPRF seed and server AKE private key separately from the credential database preserves an important distinction: a stolen database is not automatically the same as a complete server compromise.
What OPAQUE password authentication actually solves
Salted password hashing protects stored credentials. It does not stop the application from seeing each login password, and a full database leak gives an attacker the salt and verifier needed to test guesses offline. Memory-hard functions such as Argon2 or scrypt make every guess more expensive, which is valuable, but the server still handles the secret during every login.
A symmetric PAKE lets two parties that share a password establish a strong key without revealing the password on the wire. That is the wrong storage model for a public service: the server would need the password itself or an equivalent secret. An augmented PAKE, written aPAKE, gives the server a credential record instead. OPAQUE is a strong aPAKE designed to resist pre-computation after server compromise.
| Property | Password over TLS | Salted hash | OPAQUE |
|---|---|---|---|
| Server sees login password | Yes | Yes, before hashing | No |
| Produces an authenticated session key | TLS does | TLS does | Protocol output |
| DB-only leak exposes a verifier | Depends on storage | Yes | Credential record; server secret is separate |
| Full server compromise permits guessing | Yes | Yes | Yes, but guesses are per user and post-compromise |
The last row matters. OPAQUE does not make weak passwords strong, stop online guessing, or turn a compromised server into a harmless event. Its sharper claim is that an attacker who obtains the server's secret material and credential file must attack users individually after the compromise. There is no public salt that lets the attacker prepare the expensive work years in advance.
The four pieces of OPAQUE
1. An oblivious pseudorandom function
An OPRF evaluates a keyed function with the input and key held by different parties. The client has the password. The server has an OPRF seed from which it derives a credential-specific key. The client blinds the password into a group element, the server evaluates that element, and the client unblinds the response. The client learns the function output; the server does not learn the password.
Client(password) Server(oprf_seed)
| |
blind(password) |
|---- blinded element --------------------->|
| derive key for user
| evaluate(blinded)
|<--- evaluated element --------------------|
finalize(password, blind, evaluation)
|
OPRF outputThis is not encryption of the password. It is a two-party computation. The server's contribution acts like a secret salt, while blinding prevents the server from seeing the client input. RFC 9497 specifies the underlying OPRF family used by modern OPAQUE constructions.
2. A key-stretching step
The raw OPRF output is high entropy, but the password behind it may be terrible. The client runs a key-stretching function before deriving its randomized password. In this codebase the default is scrypt with N = 32768, r = 8, and p = 1. That cost belongs on the client, which means a browser should run it in a worker and a Node.js service should measure event-loop impact. Synchronous password hardening on the main thread is a self-inflicted availability bug.
3. A credential envelope
The stretched OPRF result feeds HKDF expansions for an authentication key, export key, masking key, and deterministic seed for the client's AKE key pair. The envelope stores a random nonce and an authentication tag over the server public key plus both identities. A correct password reproduces the same key material and verifies the tag. A wrong password produces unrelated bytes and ends at EnvelopeRecoveryError.
The server stores the envelope but cannot open it by itself. The client can also use export_key for application data that should be recoverable from the password-derived secret without becoming the login session key.
4. An authenticated key exchange
Recovering a client private key is not enough. Client and server still need fresh key material, mutual authentication, transcript binding, and forward secrecy. The reviewed implementation uses 3DH: three elliptic-curve Diffie-Hellman results combining long-term and ephemeral keys. HKDF turns their concatenation into server and client MAC keys plus the session key.
Registration, message by message
OPAQUE registration has three messages. The client sends a blinded password element. The server evaluates it and returns that result with its AKE public key. The client finalizes the OPRF, hardens the result, creates its envelope and AKE key pair, then uploads the registration record.
Client Server
| |
| RegistrationRequest(blinded password) ---->|
| | derive per-user OPRF key
|<---- RegistrationResponse(evaluation, pkS) |
| |
| finalize OPRF + scrypt |
| derive envelope, client key pair, |
| masking key, export key |
| |
| RegistrationRecord(pkU, masking, envelope)>|
| | store credential recordIn code, OpaqueClient.registerInit() stores the blind and encoded password in a one-use state machine. OpaqueServer.registerInit() delegates toOpaqueCoreServer.createRegistrationResponse(), which derives an OPRF key from the server seed and credential identifier. The client's registerFinish() callsOpaqueCoreClient.finalizeRequest() and returns aRegistrationRecord plus export_key.
Registration is the stage that needs a server-authenticated, confidential channel. OPAQUE can remove PKI dependence from later login exchanges, but it cannot authenticate a server that the client has never met. Ship registration over TLS and bind the intended identities into the protocol inputs.
Login: KE1, KE2, and KE3
Login runs credential recovery and authenticated key exchange together. The first message, KE1, contains a new blinded password request, a client nonce, and an ephemeral client key share. The server returns KE2: the OPRF evaluation, a masked credential response, a server nonce, an ephemeral server key share, and a server MAC.
The client finalizes the OPRF again. With the correct password it reconstructs the masking key, unmasks the server public key and envelope, derives the same persistent client key pair, and checks the envelope tag. Then both sides compute the same three DH terms:
DH(client ephemeral secret, server ephemeral public)
DH(client ephemeral secret, server static public)
DH(client static secret, server ephemeral public)Those values bind fresh ephemeral keys to the long-term identities recovered from registration. The protocol hashes a preamble that includes the version label, application context, client identity, serialized KE1, server identity, credential response, server nonce, and server key share. Change any one of them and the MAC check fails.
After verifying the server MAC, the client sends KE3 with its own transcript MAC. The server compares it in constant time. Only then does it release the session key. Three messages, explicit authentication in both directions, and no password on the wire.
How the TypeScript code maps to the protocol
The public API is intentionally small. OpaqueClient exposes two registration methods and two authentication methods.OpaqueServer mirrors those phases. Below them, the repository splits credential recovery from 3DH, which is the right boundary for reading and testing protocol code.
opaque_client.tsandopaque_server.tsown session state and the application-facing flow.core_client.tscreates and recovers the credential envelope;core_server.tshandles OPRF evaluation and response masking.3dh_client.tsand3dh_server.tsderive the shared key material and verify transcript MACs.common.tsdefines labels, OPRF operations, transcript construction, HKDF derivation, and the three DH multiplications.messages.tsowns fixed-size wire encoding for envelopes, records, and KE messages. Protocol serialization is security code, not plumbing.suites.tsconnects P-256/SHA-256, P-384/SHA-384, and P-521/SHA-512 to WebCrypto, HMAC, HKDF, and@cloudflare/voprf-ts.
The tests cover full registration and login, message serialization, HKDF behavior, credential storage, and protocol vectors across all three suites. Test vectors matter more than line coverage here. A protocol implementation can execute every branch and still be byte-for-byte incompatible with every other implementation.
A version warning hidden in plain sight
The repository identifies itself as package version 0.9.0. Its README links OPAQUE draft v07, while the test fixture is namedvectors_v16.json.gz. The final CFRG publication is RFC 9807, published in 2025. Do not infer final-RFC wire compatibility from the package name or passing local tests. Pin the exact protocol version, compare against RFC 9807 vectors, and run cross-implementation tests before production use.
Security boundaries and failure modes
OPAQUE does not replace TLS everywhere
The final RFC requires a confidential, authenticated registration channel. TLS also hides account identifiers and protocol metadata, limits active network interference, and fits existing operational controls. OPAQUE removes the need to reveal the password to the server during authentication; it is not a reason to serve login traffic over plaintext HTTP.
User enumeration needs a fake-record path
If an unknown username causes an immediate error while a real user triggers OPRF, masking, and 3DH work, the timing difference becomes an account oracle. The repository's vector tests construct a fakeRegistrationRecord and use it as the database default. A real integration must preserve that behavior, return indistinguishable errors, and rate-limit both known and unknown accounts.
JavaScript cannot promise perfect secret erasure
OpaqueClient.clean() drops references to the password and blind after a completed exchange. That is good lifecycle hygiene, but garbage-collected runtimes can copy buffers and choose when to reclaim them. Setting a field to undefined is not verified zeroization. Keep secret lifetimes short, avoid logs and crash dumps, isolate cryptographic work where possible, and be honest about the runtime's limits.
The client and server objects are stateful
A client instance permits one active registration or login. The 3DH server instance stores the expected client MAC and session key between KE2 and KE3. Reusing one object across concurrent requests risks state collision. Treat each handshake as an isolated state machine, bind it to an expiring server-side session, and reject replayed or out-of-order messages.
Uniform errors are part of the protocol surface
Wrong password, malformed group element, invalid envelope, unknown account, and failed transcript MAC are useful distinctions inside a secured diagnostic channel. They should not become five different public responses with five different timings. The application layer must collapse authentication failure into one observable result while retaining carefully redacted internal telemetry.
What OPAQUE is not
Security terminology overlaps just enough to create bad mental models. OPAQUE has a specific job: password-based mutual authentication and key agreement.
- It is not a zero-knowledge proof system. The server learns no password, but OPAQUE is built from an OPRF, credential recovery, and an AKE—not a SNARK or general zero-knowledge proof.
- It is not a passkey. WebAuthn authenticates with a device-held public-key credential and can remove passwords. OPAQUE improves authentication for systems that still use a human-memorable password.
- It is not a password manager. It does not create, remember, or synchronize passwords for the user.
- It is not protection for a compromised client.Malware or a hostile browser extension can read the password before the protocol begins.
- It is not automatic application encryption. The login produces a session key. The application must decide how to use that key safely and which messages it protects.
- It is not permission to skip rate limiting. A live server can still receive password guesses one attempt at a time.
Production checklist for an OPAQUE TypeScript deployment
- Target RFC 9807 explicitly. Record the suite, OPRF mode, wire format, labels, and dependency versions. Never call a floating draft simply “OPAQUE.”
- Run final-RFC and cross-language vectors. Test registration, real login, wrong passwords, unknown users, malformed points, altered identities, and changed context.
- Keep registration on authenticated TLS. Pin the intended server identity into the OPAQUE transcript as well.
- Move scrypt off the UI thread. Benchmark memory and latency on low-end devices; tune parameters as a security and product decision, not a copied constant.
- Use a fake record for unknown accounts. Keep public errors and computational paths as similar as practical.
- Rate-limit online guesses. OPAQUE constrains offline attacks. It cannot stop an attacker from asking the live server to test one password at a time.
- Isolate handshake state. Give each login its own client/server state, expiration, replay protection, and request correlation identifier.
- Protect the OPRF seed and server AKE key separately from credential records. A database-only breach and a full server-secret breach should not collapse into the same event.
- Audit dependencies and generated bundles.Review group-element validation, constant-time comparisons, RNG use, browser polyfills, bundler substitutions, and supply-chain controls.
Source notes and further reading
The architecture described here comes from a direct code review of the linked TypeScript repository and the final protocol documents. The source tree retains Cloudflare's BSD-3-Clause copyright and package identity. My repository link is a research snapshot, not a claim that I authored the upstream implementation.
- RFC 9807 — The OPAQUE Augmented PAKE Protocol
- RFC 9497 — Oblivious Pseudorandom Functions
- OPAQUE: An Asymmetric PAKE Protocol
- opaque-ts source reviewed for this article
- Cloudflare: OPAQUE and password authentication
About the author
I am a hands-on technical lead and security researcher working across production Node.js systems, protocol review, zero-knowledge systems, threshold signatures, and lattice cryptanalysis. My related work covers ZK circuit security, lattice attacks, and cross-chain protocol architecture.
If you are implementing a password protocol, the hardest bugs will not sit inside one primitive. They will sit between protocol versioning, state lifetime, wire encoding, identity binding, and application error handling. That boundary is where I prefer to work.