In standard password authentication, the client sends the password to the server. The server verifies it against a stored hash. This model means the password (or at minimum its hash-equivalent) traverses the network and exists on the server during the authentication exchange. A compromised TLS session, a logging misconfiguration, or a server-side exploit can expose the credential. Zero-knowledge authentication protocols eliminate this: the client proves knowledge of the password without ever transmitting it or any value derivable from it.
What zero-knowledge actually means in auth
The term "zero-knowledge" in cryptography has a precise definition: a zero-knowledge proof allows a prover to convince a verifier that a statement is true without revealing any information beyond the truth of the statement itself. In authentication, the statement is "I know the password for this account." A ZK authentication protocol lets the client prove this to the server without the server learning the password or anything that would help an eavesdropper verify the password independently.
This is distinct from simply hashing the password before sending it. Sending SHA256(password) over the wire still sends a static credential that can be captured and replayed. True ZK protocols use interactive challenge-response exchanges where each exchange produces different values and where observing any number of exchanges provides no advantage to an attacker.
The Secure Remote Password (SRP-6a) protocol
SRP (RFC 2945, with improvements in SRP-6a) is the most widely studied and deployed ZK-style authentication protocol. It provides mutual authentication — both parties verify each other — and produces a shared session key as a byproduct, which can be used to encrypt the session without a separate key exchange.
The protocol uses a large prime N and a generator g. During registration, the client derives a password verifier v = g^x mod N where x is a hash of the salt and password. The server stores (salt, v) — not the password, and not something from which the password is directly derivable. During authentication, client and server exchange public values and each independently compute a shared session key. If the keys match (proven via HMAC exchanges), both parties are authenticated.
// SRP-6a with the 'secure-remote-password' npm package
import { SRP, SrpClient, SrpServer } from 'fast-srp-hap';
import crypto from 'crypto';
const params = SRP.params['4096']; // Use 4096-bit prime for security
// Registration: client generates verifier, sends (email, salt, verifier) to server
async function srpRegister(email, password) {
const salt = crypto.randomBytes(32);
const verifier = await SRP.computeVerifier(params, salt, Buffer.from(email), Buffer.from(password));
await db.users.create({
email,
srpSalt: salt.toString('hex'),
srpVerifier: verifier.toString('hex'),
// No password hash stored — the verifier is the registration artifact
});
}
// Authentication Step 1: client sends email, server returns salt and B (server public key)
async function srpAuthStep1(email) {
const user = await db.users.findByEmail(email);
if (!user?.srpVerifier) throw new Error('user_not_found');
const salt = Buffer.from(user.srpSalt, 'hex');
const verifier = Buffer.from(user.srpVerifier, 'hex');
const server = new SrpServer(params, verifier);
const serverPublicKey = server.computeB(); // Server's public key B
// Store server state for step 2
await redis.setex(`srp:${email}`, 120, JSON.stringify({
serverSecret: server.getb().toString('hex'),
serverPublicKey: serverPublicKey.toString('hex'),
}));
return {
salt: salt.toString('hex'),
serverPublicKey: serverPublicKey.toString('hex'),
};
}
// Authentication Step 2: client sends A and M1, server responds with M2
async function srpAuthStep2(email, clientPublicKey, clientProof) {
const user = await db.users.findByEmail(email);
const srpState = await redis.get(`srp:${email}`);
if (!srpState) throw new Error('srp_session_expired');
const { serverSecret, serverPublicKey } = JSON.parse(srpState);
const salt = Buffer.from(user.srpSalt, 'hex');
const verifier = Buffer.from(user.srpVerifier, 'hex');
const server = new SrpServer(params, verifier);
server.setb(Buffer.from(serverSecret, 'hex'));
const A = Buffer.from(clientPublicKey, 'hex');
const M1 = Buffer.from(clientProof, 'hex');
// Verify client proof — this confirms client knows the password
server.setA(A);
const serverM2 = server.computeM2(M1); // Throws if M1 is invalid
await redis.del(`srp:${email}`);
// Issue session — both parties have proven knowledge of the password
return { serverProof: serverM2.toString('hex'), session: await issueSession(user) };
}
Why SRP is not widely deployed
SRP has been available since 1998 and is cryptographically sound. Yet it is not the default in web applications. Several practical barriers account for this:
- Library ecosystem gaps: high-quality, well-maintained SRP libraries exist for most languages, but they are not as well-known or widely used as standard bcrypt/argon2 implementations. Teams default to what they know.
- Two-round-trip authentication: SRP requires at minimum two round trips (step 1 to get salt and B, step 2 to send A and M1). Standard password auth is one POST. For mobile apps on high-latency connections, this is a noticeable difference.
- Infrastructure integration: passwords can be managed and migrated relatively easily. SRP verifiers are cryptographically distinct from password hashes and cannot be migrated from an existing bcrypt database without users resetting their passwords.
- HTTPS already protects the transport layer: for web applications where TLS is always present, the additional protection from not transmitting the password is less compelling than it would be in a protocol without transport encryption.
Practical ZK principles in modern auth
Even without full SRP deployment, zero-knowledge thinking influences modern auth design. FIDO2/WebAuthn is arguably ZK in spirit: the server never sees the private key, never sees anything from which the private key could be derived, and a valid authentication response proves possession of the key without revealing it. The HIBP k-anonymity password check sends only 5 hex characters of the hash to the API, proving knowledge of the hash prefix without revealing the full hash. Blind signatures in privacy-preserving token schemes let clients redeem tokens without the issuer learning which token is being used.
For most web applications, TLS-protected bcrypt or Argon2 password hashing with a well-implemented auth flow provides adequate security without the deployment complexity of SRP. The cases where SRP or true ZK auth makes the most sense are end-to-end encrypted applications where the server genuinely must never have access to the user's credentials — password managers, encrypted messaging apps, and similar high-assurance systems where server compromise must not compromise user secrets.