Hofmann Elimination

Password authentication where the server never sees the password. Pure Java. Drop-in Spring Boot and Dropwizard integration.

RFC 9807 OPAQUE RFC 9497 OPRF RFC 9380 Hash-to-Curve Java 21 TypeScript Apache 2.0

The Problem with Password Authentication

Every major password breach follows the same pattern: an attacker gains access to the server's credential database, then runs offline dictionary attacks against the stored hashes. Bcrypt, scrypt, and Argon2id slow these attacks down, but they don't eliminate them. The server still holds everything needed to test password guesses.

Traditional password auth

  • Server receives the plaintext password on every login
  • Server stores a hash that can be attacked offline
  • TLS termination proxies and logging can leak passwords
  • A breached database enables offline dictionary attacks
  • Password reuse across sites amplifies the damage

OPAQUE (RFC 9807)

  • Password never leaves the client, not even during registration
  • Server stores only cryptographic values it cannot reverse
  • No password in transit means nothing to intercept or log
  • A breached database is cryptographically useless to an attacker
  • Mutual authentication proves both client and server identity

How OPAQUE Works

OPAQUE is an asymmetric Password-Authenticated Key Exchange (aPAKE) standardized in RFC 9807. It combines an Oblivious Pseudorandom Function (RFC 9497 OPRF) with a 3-party Diffie-Hellman key exchange. The password is used cryptographically on the client side but is never transmitted or derivable from stored server state.

Registration (one-time, per user)

Client                                   Server
  |                                        |
  |  blind = hash_to_curve(pwd) * r        |
  |---- blindedElement, credentialId ----->|
  |                                        |  eval = blind * oprfKey
  |<---- evaluatedElement, serverPubKey ---|
  |                                        |
  |  oprfOutput = eval * r^-1              |
  |  randomizedPwd = KSF(oprfOutput)       |
  |  derive client keys + seal envelope    |
  |                                        |
  |---- clientPubKey, envelope ----------->|  store credential record
  |<------ 204 OK -------------------------|

Authentication (each login)

Client                                  Server
  |                                       |
  |  blind = hash_to_curve(pwd) * r       |
  |---- KE1 (blind + ephemeral key) ----->|
  |                                       |  eval = blind * oprfKey
  |                                       |  mask envelope with one-time pad
  |                                       |  compute server MAC
  |<---- KE2 (eval + masked env + MAC) ---|
  |                                       |
  |  oprfOutput = eval * r^-1             |
  |  unseal envelope, recover keys        |
  |  verify server MAC                    |
  |  compute client MAC                   |
  |                                       |
  |---- KE3 (client MAC) ---------------->|  verify client MAC
  |<---- session key + JWT ---------------|  mutual auth complete

At no point does the server see the password or hold values that allow offline guessing. Even if the entire credential database is stolen, an attacker cannot run a dictionary attack because the stored record is bound to the server's secret OPRF key, which is not stored alongside the credentials.

Security Properties

Zero password exposure

The password never leaves the client device. Not during registration, not during login, not ever. There is no plaintext or hash for the server to leak.

Offline attack resistance

A stolen credential database cannot be dictionary-attacked. The stored values are bound to the server's OPRF secret key, making them useless without it.

Mutual authentication

Both the client and server prove their identity during the handshake. A man-in-the-middle cannot impersonate either party.

Forward secrecy

Each session uses ephemeral Diffie-Hellman keys. Compromise of long-term keys does not expose past session traffic.

User enumeration resistance

Unknown credential identifiers receive a fake-but-valid-looking response. Attackers cannot distinguish "user not found" from "wrong password."

Pre-computation resistance

OPRF evaluation is keyed per server, so precomputed rainbow tables are useless. Each server's OPRF key makes its credential space unique.

OPAQUE is not a silver bullet. It does not protect against online brute force, real-time phishing proxies, or quantum computers. Browser-based clients inherit the "trust the server to serve honest code" limitation of all browser cryptography. For a full discussion of known concerns and trade-offs, see SECURITY.md.

Quick Start

Add one dependency and the framework starter handles the rest. Both starters auto-configure all OPAQUE and OPRF endpoints with sensible defaults.

Spring Boot
Dropwizard
TypeScript Client
// build.gradle.kts
dependencies {
    implementation("com.codeheadsystems:hofmann-springboot:<version>")
}
# application.yml
hofmann:
  context: my-app-v1
  server-key-seed-hex: ${SERVER_KEY_SEED_HEX}
  oprf-seed-hex: ${OPRF_SEED_HEX}
  oprf-master-key-hex: ${OPRF_MASTER_KEY_HEX}
  jwt-secret-hex: ${JWT_SECRET_HEX}
  argon2-memory-kib: 65536
  argon2-iterations: 3
  argon2-parallelism: 1

All endpoints are registered automatically. Override any bean with @ConditionalOnMissingBean to plug in your own CredentialStore, SessionStore, or SecureRandom.

// build.gradle.kts
dependencies {
    implementation("com.codeheadsystems:hofmann-dropwizard:<version>")
}
// In your Application class
@Override
public void initialize(Bootstrap<MyConfig> bootstrap) {
    bootstrap.addBundle(new HofmannBundle<>());
}

For production, supply persistent stores: new HofmannBundle<>(credentialStore, sessionStore, null)

// npm install hofmann-client

import { OpaqueHttpClient } from 'hofmann-client';

// Auto-configures cipher suite and KSF params from server
const client = await OpaqueHttpClient.create('https://api.example.com');

// Register
await client.register('user@example.com', 'correct-horse-battery-staple');

// Authenticate
const session = await client.authenticate('user@example.com', 'correct-horse-battery-staple');
console.log(session.jwt);  // Bearer token for subsequent API calls

The TypeScript client runs in the browser or Node.js. It handles the full OPAQUE handshake including Argon2id key stretching on the client side.

Example Application

The hofmann-elimination-example repository contains a complete working application that demonstrates OPAQUE integration end-to-end. It includes server configuration, persistent credential storage, and client authentication flows. Use it as a starting point for your own integration.

git clone https://github.com/codeheadsystems/hofmann-elimination-example.git
cd hofmann-elimination-example
./gradlew run

The example covers production-oriented patterns: database-backed credential storage, externalized key configuration, and proper Argon2id parameter tuning. See the repository README for full setup instructions.

Resources

Supported Cipher Suites

Both the Java server and TypeScript client support multiple cipher suites. The server and client auto-negotiate via the /opaque/config endpoint.

Suite Curve Hash Security Level Status
P256_SHA256 NIST P-256 SHA-256 128-bit RFC 9807 reference suite
P384_SHA384 NIST P-384 SHA-384 192-bit Supported
P521_SHA512 NIST P-521 SHA-512 256-bit Supported
RISTRETTO255_SHA512 ristretto255 SHA-512 128-bit Supported

Related Projects

Several projects implement OPRF or OPAQUE. The table below compares RFC compliance, cipher suite support, and key features. Hofmann Elimination is the only pure-Java, RFC-compliant implementation with framework integrations.

Project Language RFC 9497 RFC 9807 Cipher suites Audit
Hofmann Elimination Pure Java + TS Yes Yes P-256, P-384, P-521, Ristretto255 No
facebook/opaque-ke Rust Yes Yes Ristretto255, P-256, P-384, P-521 NCC Group
bytemare/opaque Go Yes Yes P-256, P-384, P-521, Ristretto255 No
@serenity-kit/opaque JS/WASM (Rust) Yes Yes Ristretto255, P-256 7ASecurity
@cloudflare/opaque-ts TypeScript Draft Draft P-256, P-384, P-521 No
aldenml/ecc Java/JNI over C Draft Draft Ristretto255 No
stef/libopaque C + Java JNI Draft Draft Ristretto25519 No
cloudflare/opaque-ea Go Draft Draft No

Project Details

facebook/opaque-ke

Rust implementation of RFC 9807. Audited by NCC Group (sponsored by WhatsApp for end-to-end encrypted backups). Supports multiple cipher suites via feature flags. v4.1.0-pre.1. No Java bindings.

bytemare/opaque

Go implementation of RFC 9497 OPRF + RFC 9807 OPAQUE. Actively maintained with full multi-suite support. No Java bindings.

@serenity-kit/opaque

JavaScript/WASM library with a Rust core. Implements final RFC 9807, audited by 7ASecurity via OTF Red Team Lab. Published as opaque-auth.com with Next.js, Vite, and password reset examples. JS/WASM only.

@cloudflare/opaque-ts

TypeScript library from Cloudflare. Implements draft-irtf-cfrg-opaque-07 (not final RFC 9807). Last npm release v0.7.5, February 2022. Receives only dependabot updates.

aldenml/ecc

Java bindings over a C core (JNI). Implements pre-RFC OPRF and OPAQUE drafts. Ristretto255 only. Last release v1.1.0, October 2023. Has not tracked final RFCs.

stef/libopaque

C implementation with Java JNI bindings. Pre-RFC OPAQUE draft with Ristretto25519 and Argon2id via libsodium. Requires native compilation — cannot be distributed as a self-contained JAR. v1.0.1, February 2025.

cloudflare/opaque-ea

Go proof-of-concept combining OPAQUE with TLS Exported Authenticators. 4 commits total. Explicitly marked "DO NOT use in production systems."

pvriel/oprf4j

Pure Java OPRF research artifact implementing KISS17/KALES19 variants for private set intersection — not the IETF standard. Archived May 2025.