Zero-Knowledge
The server verifies you know your password without ever seeing it. No password hashes to steal.
Schnorr Proof of Knowledge on Ed25519. Your password never leaves the browser β not even as a hash.
ZKP Auth lets users authenticate with a username and password without the server ever seeing the password β not even as a bcrypt hash.
Instead, the browser uses the password to derive an Ed25519 keypair and proves knowledge of the private key via a Schnorr Proof of Knowledge. The server stores only the 32-byte public key. No password database means no password database to breach.
| Package | Role | Install target |
|---|---|---|
@zkp-auth/core | Schnorr crypto primitives | Server (Node 20+) |
@zkp-auth/server | Express middleware + JWT | Server |
@zkp-auth/client | Browser SDK | Browser |
@zkp-auth/react | React hooks + context | Browser |
With traditional password authentication:
bcrypt(password) in the database.bcrypt(attempt) to stored hash.Attack surfaces: database breach exposes all hashes; offline cracking converts weak hashes to passwords; rainbow tables, timing attacks.
With ZKP Auth:
Attack surfaces eliminated: no hashes to steal, no offline cracking possible, replay attacks are provably impossible (each proof binds to a fresh server-issued challenge).
When should I use it?
ZKP Auth is a good fit when you want strong security guarantees without building an OAuth/OIDC infrastructure. It is not a replacement for MFA or certificate-based auth, but it is a dramatic improvement over bcrypt-based password stores.
Current status
This library is pre-1.0 and has not been independently audited. See the Security Model page for details. Use it in production only after your own security review.
npm install @zkp-auth/server
# or
pnpm add @zkp-auth/servernpm install @zkp-auth/clientnpm install @zkp-auth/client @zkp-auth/reactGet ZKP authentication running in five minutes.
// server.ts
import express from 'express';
import cookieParser from 'cookie-parser';
import {
zkpRegister,
zkpChallenge,
zkpVerify,
InMemoryChallengeStore,
} from '@zkp-auth/server';
const app = express();
app.use(express.json());
app.use(cookieParser());
// In-memory store β swap for Redis/Postgres in production
const store = new InMemoryChallengeStore();
// In-memory user "database" β swap for your real DB
const users = new Map<string, Uint8Array>();
// Route 1 β register: save user's public key
app.post(
'/auth/register',
zkpRegister({
savePublicKey: async (userId, publicKey) => {
users.set(userId, publicKey);
},
}),
(_req, res) => res.json({ ok: true }),
);
// Route 2 β challenge: issue a fresh one-time nonce
app.post(
'/auth/challenge',
zkpChallenge({ store }),
(req, res) => res.json({ challengeHex: res.locals.zkpChallengeHex }),
);
// Route 3 β verify: check the proof, issue a JWT
app.post(
'/auth/verify',
zkpVerify({
getPublicKey: async (userId) => users.get(userId) ?? null,
store,
jwtSecret: process.env.JWT_SECRET!,
}),
(req, res) => {
// Set the JWT as an HttpOnly cookie
res.cookie('auth', res.locals.zkpToken, {
httpOnly: true,
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production',
});
res.json({ ok: true });
},
);
app.listen(3001, () => console.log('ZKP auth server on :3001'));// main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { ZKPProvider } from '@zkp-auth/react';
import App from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ZKPProvider options={{ baseUrl: 'http://localhost:3001' }}>
<App />
</ZKPProvider>
</StrictMode>,
);// LoginForm.tsx
import { useZKPAuth } from '@zkp-auth/react';
export function LoginForm() {
const { register, login, logout, isAuthenticated, loading, error, user } =
useZKPAuth();
if (isAuthenticated) {
return (
<div>
<p>Welcome, {user?.userId}!</p>
<button onClick={logout}>Log out</button>
</div>
);
}
async function handleRegister(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const form = new FormData(e.currentTarget);
await register(
form.get('username') as string,
form.get('password') as string,
);
}
async function handleLogin(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const form = new FormData(e.currentTarget);
await login(
form.get('username') as string,
form.get('password') as string,
);
}
return (
<>
{error && <p style={{ color: 'red' }}>{error.message}</p>}
<form onSubmit={handleRegister}>
<h2>Register</h2>
<input name="username" placeholder="Username" required />
<input name="password" type="password" placeholder="Password" />
<button type="submit" disabled={loading}>Register</button>
</form>
<form onSubmit={handleLogin}>
<h2>Log in</h2>
<input name="username" placeholder="Username" required />
<input name="password" type="password" placeholder="Password" />
<button type="submit" disabled={loading}>
{loading ? 'Authenticatingβ¦' : 'Log in'}
</button>
</form>
</>
);
}That's it. No password is ever transmitted or stored on the server.