npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@codeheadsystems/hofmann-typescript

v1.3.1

Published

RFC 9497 (OPRF) and RFC 9807 (OPAQUE-3DH) browser client

Downloads

214

Readme

hofmann-typescript

Browser/Node TypeScript client for the Hofmann Elimination OPRF and OPAQUE server.

Implements:

  • RFC 9497 — Oblivious Pseudorandom Functions (OPRF)
  • RFC 9807 — OPAQUE-3DH password-authenticated key exchange

Supported cipher suites:

| Suite | Curve | Hash | Nh | Npk | |---|---|---|---|---| | P256_SHA256 | P-256 | SHA-256 | 32 bytes | 33 bytes | | P384_SHA384 | P-384 | SHA-384 | 48 bytes | 49 bytes | | P521_SHA512 | P-521 | SHA-512 | 64 bytes | 67 bytes |

The active suite is negotiated automatically from the server's /opaque/config endpoint — no hardcoding required.

All cryptography is built on @noble/curves and @noble/hashes. No native bindings, no WebAssembly (except the optional Argon2id KSF via hash-wasm).

Security notice: This implementation has not undergone a formal security audit. See the parent project for full details.


Table of Contents


Installation

npm install @codeheadsystems/hofmann-typescript

Dependencies pulled in automatically:

| Package | Purpose | |---|---| | @noble/curves | P-256, P-384, P-521 elliptic curve arithmetic and hash-to-curve | | @noble/hashes | SHA-256, SHA-384, SHA-512, HMAC, HKDF | | hash-wasm | Argon2id key stretching (only loaded when used) |


Quick Start

OPAQUE registration and login

The recommended way to create a client is via OpaqueHttpClient.create(). It fetches the server's /opaque/config endpoint and automatically configures the cipher suite, protocol context, and Argon2id parameters — no manual configuration needed:

import { OpaqueHttpClient } from '@codeheadsystems/hofmann-typescript';

// Fetches /opaque/config → resolves cipher suite (P-256/P-384/P-521),
// context string, and Argon2id parameters automatically.
const client = await OpaqueHttpClient.create('https://your-server.example.com');

// Register a new user (run once)
await client.register('[email protected]', 'hunter2');

// Authenticate (every login) — returns a JWT bearer token
const token = await client.authenticate('[email protected]', 'hunter2');
console.log('JWT:', token);

// Delete a registration (requires a valid token)
await client.deleteRegistration('[email protected]', token);

Standalone OPRF evaluation

import { OprfHttpClient } from '@codeheadsystems/hofmann-typescript';
import { strToBytes } from '@codeheadsystems/hofmann-typescript';

// Fetches /oprf/config → resolves cipher suite automatically.
const client = await OprfHttpClient.create('https://your-server.example.com');
const result = await client.evaluate(strToBytes('my-secret-input'));
// result is a stable Nh-byte Uint8Array — same every time for the same input and server key
// Nh = 32 (P-256), 48 (P-384), or 64 (P-521) depending on server configuration

Cipher Suites

A CipherSuite encapsulates all curve-specific operations (hash-to-curve, scalar arithmetic, hash/MAC/HKDF with the appropriate hash function) and the RFC 9497 domain-separation strings.

import { P256_SHA256, P384_SHA384, P521_SHA512, getCipherSuite } from '@codeheadsystems/hofmann-typescript';
import type { CipherSuite } from '@codeheadsystems/hofmann-typescript';

Named suite constants

| Export | Curve | Hash | Nh | Npk | Nsk | L | |---|---|---|---|---|---|---| | P256_SHA256 | P-256 | SHA-256 | 32 | 33 | 32 | 48 | | P384_SHA384 | P-384 | SHA-384 | 48 | 49 | 48 | 72 | | P521_SHA512 | P-521 | SHA-512 | 64 | 67 | 66 | 98 |

Nh = hash output length · Npk = compressed public key size · Nsk = scalar size · L = hashToScalar expand length

getCipherSuite(name): CipherSuite

Resolves a suite by the name string returned in server config responses:

import { getCipherSuite } from '@codeheadsystems/hofmann-typescript';

const suite = getCipherSuite('P384_SHA384'); // returns P384_SHA384

Accepted values: "P256_SHA256", "P384_SHA384", "P521_SHA512". Throws for any other value.

Using a suite directly

const suite = P384_SHA384;

// Blind an input
const { blind, blindedElement } = suite.blind(inputBytes);

// Finalize after server evaluation (returns Nh=48 bytes for P-384)
const output = suite.finalize(inputBytes, blind, evaluatedElement);

// Hash/MAC/HKDF using the suite's hash function (SHA-384 for P-384)
const hash   = suite.hash(data);
const mac    = suite.hmac(key, data);
const prk    = suite.hkdfExtract(undefined, ikm);
const keyMat = suite.hkdfExpand(prk, info, 48);

Configuration

Auto-configuration (recommended)

OpaqueHttpClient.create() and OprfHttpClient.create() call the server's config endpoint and set everything automatically. Use these factories in production and in the integration tests.

const client = await OpaqueHttpClient.create('https://your-server.example.com');
console.log(client.configResponse);
// {
//   cipherSuite:      "P384_SHA384",
//   context:          "my-app",
//   argon2MemoryKib:  65536,
//   argon2Iterations: 3,
//   argon2Parallelism: 1
// }

Manual configuration

If you need to construct the client manually (e.g., to pin a specific cipher suite or supply a custom KSF):

import { OpaqueHttpClient, P384_SHA384, argon2idKsf } from '@codeheadsystems/hofmann-typescript';

const client = new OpaqueHttpClient('http://localhost:8080', {
  suite:   P384_SHA384,
  context: 'my-app',
  ksf:     argon2idKsf(65536, 3, 1),
});

The context string and Argon2id parameters must exactly match the server's configuration. A mismatch causes silent authentication failure that is indistinguishable from a wrong password.

For a Dropwizard server, look at the YAML configuration:

# hofmann-testserver/config/config.yml (example)
cipherSuite: P384_SHA384
context: hofmann-testserver
argon2MemoryKib: 65536
argon2Iterations: 3
argon2Parallelism: 1

Identity KSF (test servers only)

If the server has argon2MemoryKib: 0 (identity KSF, no Argon2), the create() factory handles this automatically. When constructing manually, omit ksf or pass identityKsf:

import { OpaqueHttpClient, identityKsf } from '@codeheadsystems/hofmann-typescript';

const client = new OpaqueHttpClient('http://localhost:8080', {
  context: 'my-test-context',
  // ksf defaults to identityKsf when omitted
});

Do not use identity KSF against a production server. It disables password hardening.


API Reference

OpaqueHttpClient

The main entry point. Handles the full OPAQUE-3DH protocol over HTTP.

OpaqueHttpClient.create(baseUrl): Promise<OpaqueHttpClient> (recommended)

Fetches GET /opaque/config, resolves the cipher suite and KSF automatically, and returns a configured client.

const client = await OpaqueHttpClient.create('https://your-server.example.com');
// client.configResponse holds the raw server response

new OpaqueHttpClient(baseUrl, options?)

Manual constructor. All options default to P-256/SHA-256 with identity KSF when omitted.

OpaqueHttpClientOptions

| Field | Type | Default | Description | |---|---|---|---| | suite | CipherSuite | P256_SHA256 | Cipher suite. Must match the server's configured suite. | | context | string | "" | OPAQUE protocol context. Must match context in server config. | | ksf | KSF | identityKsf | Key stretching function. Use argon2idKsf(...) for production. |

register(credentialId, password, serverIdentity?, clientIdentity?): Promise<void>

Runs the three-message OPAQUE registration flow:

  1. Sends blindedElement to POST /opaque/registration/start
  2. Receives evaluatedElement and serverPublicKey
  3. Derives the envelope locally using the KSF
  4. Uploads clientPublicKey, maskingKey, and envelope to POST /opaque/registration/finish

The password never leaves the client in plaintext.

await client.register('[email protected]', 's3cr3t');

// With explicit identities (advanced — must match the server's identity config)
await client.register('[email protected]', 's3cr3t', 'server.example.com', '[email protected]');

authenticate(credentialId, password, serverIdentity?, clientIdentity?): Promise<string>

Runs the three-message OPAQUE-3DH authentication flow (KE1 → KE2 → KE3):

  1. Generates KE1 (blind password, generate ephemeral AKE key pair)
  2. Sends to POST /opaque/auth/start, receives KE2
  3. Verifies the server MAC — throws if wrong password or server mismatch
  4. Sends KE3 (client MAC) to POST /opaque/auth/finish
  5. Returns the JWT bearer token from the server
const token = await client.authenticate('[email protected]', 's3cr3t');
// Use token in subsequent requests:
// Authorization: Bearer <token>

Throws an Error if the password is incorrect, the server MAC fails, or a network/HTTP error occurs.

deleteRegistration(credentialId, token): Promise<void>

Sends DELETE /opaque/registration with the Authorization: Bearer <token> header.

await client.deleteRegistration('[email protected]', token);

OprfHttpClient

Standalone OPRF client for the /oprf endpoint. Useful when you want a server-keyed pseudorandom function without the full OPAQUE flow.

OprfHttpClient.create(baseUrl): Promise<OprfHttpClient> (recommended)

Fetches GET /oprf/config, resolves the cipher suite, and returns a configured client.

const client = await OprfHttpClient.create('https://your-server.example.com');
// client.cachedConfig.cipherSuite tells you which suite the server uses

new OprfHttpClient(baseUrl, suite?)

Manual constructor. Defaults to P256_SHA256 when suite is omitted.

evaluate(input: Uint8Array): Promise<Uint8Array>

Returns a stable Nh-byte value. The same input always produces the same output for a given server key. The server learns nothing about input — it sees only the blinded EC point.

Output length matches the suite: 32 bytes (P-256), 48 bytes (P-384), or 64 bytes (P-521).

const client = await OprfHttpClient.create('https://your-server.example.com');
const output = await client.evaluate(strToBytes('my-input'));
console.log(output.length); // 32, 48, or 64 depending on server suite

OpaqueClient (low-level)

The OpaqueClient class implements OPAQUE cryptographic operations without any HTTP transport. Pass a CipherSuite to the constructor to select the suite; defaults to P256_SHA256.

import { OpaqueClient, P384_SHA384, identityKsf, argon2idKsf } from '@codeheadsystems/hofmann-typescript';
import type { KSF, CipherSuite } from '@codeheadsystems/hofmann-typescript';

const suite: CipherSuite = P384_SHA384;
const client = new OpaqueClient(suite);
const ksf: KSF = argon2idKsf(65536, 3, 1);

// ── Registration ────────────────────────────────────────────────────────────

// Step 1a: create registration request
const regState = client.createRegistrationRequest(passwordBytes);
// regState.blindedElement (Npk bytes) → send to server

// Step 1c: finalize registration (after receiving server response)
const record = await client.finalizeRegistration(
  regState,
  { evaluatedElement, serverPublicKey },  // from server
  null,         // serverIdentity (null = use serverPublicKey)
  null,         // clientIdentity (null = use derived clientPublicKey)
  undefined,    // envelopeNonce (undefined = random)
  ksf           // key stretching function
);
// record.clientPublicKey (Npk bytes), record.maskingKey (Nh bytes),
// record.envelope.nonce (32 bytes), record.envelope.authTag (Nh bytes) → upload to server

// ── Authentication ──────────────────────────────────────────────────────────

// Step 2a: generate KE1
const { state, ke1Bytes } = client.generateKE1(passwordBytes);
// ke1Bytes = blindedElement (Npk) || clientNonce (32) || clientAkePk (Npk) → send to server

// Step 2c: generate KE3 after receiving KE2 from server
const authResult = await client.generateKE3(
  state,
  ke2,          // KE2 object with evaluatedElement, maskingNonce, maskedResponse, etc.
  null,         // clientIdentity
  null,         // serverIdentity
  contextBytes, // application context (must match server)
  ksf           // key stretching function
);
// authResult.clientMac (Nh bytes) → send to server as KE3
// authResult.sessionKey (Nh bytes) → shared session key
// authResult.exportKey  (Nh bytes) → optional export key for application use

Key sizes scale with the suite's Nh and Npk — for P-256 these are 32 and 33 bytes respectively; for P-384, 48 and 49; for P-521, 64 and 67.

Deterministic variants for testing:

// Fixed blind scalar — produces identical blinded elements for RFC test vectors
const regState = client.createRegistrationRequestDeterministic(password, blindScalar);

// Fixed nonces and AKE seed — produces identical KE1 for RFC test vectors
const { state } = client.generateKE1Deterministic(
  password, blindScalar, clientNonce, clientAkeSeed
);

Key Stretching Functions (KSF)

The KSF is applied to the raw OPRF output before HKDF derives randomizedPwd. The client and server must use the same KSF and parameters at all times — changing them after registration invalidates all existing credentials.

import { identityKsf, argon2idKsf, type KSF } from '@codeheadsystems/hofmann-typescript';

identityKsf

No stretching — the OPRF output is used directly. Appropriate only for testing with servers configured with argon2MemoryKib: 0. This is the default when ksf is omitted from OpaqueHttpClientOptions.

argon2idKsf(memoryKib, iterations, parallelism): KSF

Returns an Argon2id key stretching function with a 32-byte all-zero salt and 32-byte output, matching the server's implementation.

// Matching hofmann-testserver defaults
const ksf = argon2idKsf(65536, 3, 1);

hash-wasm is loaded on demand the first time argon2idKsf is invoked; there is no startup cost if identity KSF is used.

Custom KSF

The KSF type is (input: Uint8Array) => Promise<Uint8Array>. Any async function works:

const myKsf: KSF = async (input) => {
  return stretchedOutput; // custom key stretching
};

Publishing to npm

This package is published to the public npm registry under the @codeheadsystems scope.

Prerequisites (one-time setup)

  1. Create an npm account at https://www.npmjs.com/signup if you don't have one.

  2. Create the @codeheadsystems organization on npm (if it doesn't exist yet):

    • Go to https://www.npmjs.com/org/create
    • Name: codeheadsystems
    • Choose the free/public tier
  3. Log in to npm from your terminal:

    npm login

First publish

cd hofmann-typescript
npm run build
npm publish --access public

The --access public flag is required on the first publish of a scoped package. After the first publish, the "access": "public" setting in publishConfig makes it automatic.

Subsequent publishes

  1. Bump the version. npm will not allow you to republish the same version number.

    npm version patch   # 0.1.0 → 0.1.1  (bug fixes, minor changes)
    npm version minor   # 0.1.1 → 0.2.0  (new features, backward compatible)
    npm version major   # 0.2.0 → 1.0.0  (breaking changes)

    This updates package.json and creates a git tag (e.g., v0.1.1).

  2. Build and publish:

    npm run build
    npm publish
  3. Push the version commit and tag:

    git push && git push --tags

Interactive Demo

A browser-based demo page is included for manual testing against a running server.

npm run demo

This starts a Vite dev server at http://localhost:5173/demo.html and proxies /opaque and /oprf requests to http://localhost:8080, avoiding CORS issues.

On page load the demo automatically calls GET /opaque/config and populates the Server Configuration panel with the cipher suite, context, and Argon2id parameters from the server. Every subsequent operation re-fetches the config so it is always in sync with the server.

To target a different server:

HOFMANN_SERVER=http://other-server:8080 npm run demo

The demo provides:

  • Server Configuration — displays cipher suite, context, and Argon2id params loaded from the server (↺ Load Config button refreshes manually)
  • OPAQUE Registration — enter credential ID and password, click Register
  • OPAQUE Authentication — returns a JWT token (auto-fills the Delete form)
  • Delete Registration — removes the credential (requires the JWT from authentication)
  • Standalone OPRF — evaluate an arbitrary plaintext; output length shown dynamically
  • Activity log — timestamped protocol step log

Running Tests

RFC vector tests and round-trip tests (no server required)

npm test

Runs 34 tests:

  • test/oprf.test.ts — RFC 9497 P-256/SHA-256 vectors (DSTs, blind, finalize, deriveKeyPair); P-384 and P-521 constant and DST verification; getCipherSuite() lookup; per-suite OPRF round-trip consistency checks for all three suites
  • test/opaque.test.ts — RFC 9807 OPAQUE-3DH vectors against P-256/SHA-256 CFRG test vectors (registration records, KE1 bytes, server MAC, full AKE); multi-suite round-trip tests for P-256, P-384, and P-521

Integration tests (requires running server)

TEST_SERVER_URL=http://localhost:8080 npm test -- integration

Uses OpaqueHttpClient.create() and OprfHttpClient.create() so the cipher suite and Argon2id parameters are read from the server — no hardcoded values. Runs:

  • Cipher suite and Argon2id config verification (asserts configResponse fields)
  • Full register → authenticate → wrong-password-rejection → delete flow

Each OPAQUE operation has a 30-second timeout to accommodate Argon2id processing time. Tests are skipped automatically when TEST_SERVER_URL is not set.

Watch mode

npm run test:watch

Building

npm run build

Produces two bundles in dist/:

| File | Format | Description | |---|---|---| | dist/hofmann-typescript.js | ESM | For modern bundlers and <script type="module"> | | dist/hofmann-typescript.umd.cjs | UMD | For CommonJS environments and direct <script> tags | | dist/index.d.ts | TypeScript declarations | Included automatically by bundlers |

@noble/curves and @noble/hashes are external in the build — they are not bundled and must be present in the consuming project's node_modules. hash-wasm is a regular dependency and is bundled.

npm run typecheck   # type-check without emitting files

Project Structure

hofmann-typescript/
├── src/
│   ├── index.ts                  # Public re-exports
│   ├── crypto/
│   │   ├── primitives.ts         # i2osp, concat, xor, constantTimeEqual, fromHex, toHex
│   │   ├── encoding.ts           # base64Encode/Decode, strToBytes/bytesToStr
│   │   └── hkdf.ts               # hkdfExtract, hkdfExpand, hkdfExpandLabel (SHA-256)
│   ├── oprf/
│   │   ├── suite.ts              # CipherSuite interface; P256_SHA256, P384_SHA384,
│   │   │                         #   P521_SHA512 constants; getCipherSuite()
│   │   ├── client.ts             # blind(), finalize(), deriveKeyPair(), hashToScalar()
│   │   └── http.ts               # OprfHttpClient → POST /oprf, GET /oprf/config
│   └── opaque/
│       ├── types.ts              # Envelope, KE1, KE2, AuthResult, RegistrationRecord, …
│       ├── ksf.ts                # KSF type, identityKsf, argon2idKsf()
│       ├── envelope.ts           # storeEnvelope(), recoverEnvelope() — suite-aware sizes
│       ├── ake.ts                # buildPreamble(), derive3DHKeys(), verifyServerMac(),
│       │                         #   computeClientMac() — suite-aware hash/HMAC/HKDF
│       ├── client.ts             # OpaqueClient(suite?) — crypto only, no HTTP
│       └── http.ts               # OpaqueHttpClient — full HTTP flow, create() factory
├── test/
│   ├── oprf.test.ts              # RFC 9497 vectors + multi-suite constants + round-trips
│   ├── opaque.test.ts            # RFC 9807 vectors + multi-suite round-trips
│   └── integration.test.ts       # Live server tests (skipped without TEST_SERVER_URL)
├── demo.html                     # Interactive browser demo UI
├── demo.ts                       # Demo logic — auto-loads config from server on startup
├── vite.config.ts                # Library build config (ESM + UMD)
└── vite.demo.config.ts           # Dev server config with proxy for demo.html

Server Endpoints

For the full REST endpoint listing, request/response format, and server setup, see the server configuration guide.