Zero Accessby Railmandocs

Crypto and package rationale

Why we chose these primitives, how they are implemented in the suite, and how we relate to Better Auth and other upstream packages.

Crypto and package rationale

This page records why the suite looks the way it does: crypto choices, implementation shape, and upstream dependencies. It is analysis and design notes — not a scoreboard. For the inventory tables, see Crypto primitives. For WebAuthn/PRF product framing, see WebAuthn and PRF.

Stage

Pre-1.0. Rationale can change when we learn more; treat production use as your own review. Security is hard; this is an open foundation.

Problem we optimized for

We want product apps on Better Auth where:

  1. A database dump does not yield vault plaintext.
  2. Step-up for account actions does not imply decrypt.
  3. Passkeys can both sign in and (when PRF exists) unlock sealed data without shipping a second key ceremony stack.

That pushes secrets toward the browser, proofs toward the server, and package boundaries toward “one job each.”

Crypto choices (and what we rejected)

ChoiceRationaleAlternatives considered
WebAuthn + PRF for daily unlockAuthenticator-bound secret that never needs to be typed or stored as a password; degrades to password/recovery when PRF is missingPassword-only vaults; custom soft tokens; server-held MEKs
HKDF domain separation (purpose + userId)Prevents purpose and tenant mix-ups at the KDF boundarySingle shared “app key” derivation
PBKDF2-SHA256 for password/recovery wrapsWidely available in Web Crypto; high iteration floor in helpersArgon2 (better memory-hardness, heavier browser story for our current helpers)
AES-GCM for sealsAuthenticated encryption with Web Crypto; AAD for context bindingChaCha20-Poly1305 (fine cryptographically; less natural in browser Web Crypto today)
BIP-39 recovery phraseHuman-exportable break-glass; well-understood UXRaw high-entropy blobs only; SMS/email “recovery” (rejected for vault secrets)
Ed25519 for account-recovery Mode BCompact signatures; server verifies without holding the phraseServer-held recovery secrets
HMAC for elevate / login-factor / row MACsDetects field tampering under a server root without inventing a full HSM storyJWT-only claims with no row MAC; client-supplied claim blobs
Single root → HKDF familiesApps should not manage key rings; rotate/lifecycle is already hard enoughUser-configured multi-key rings (removed from the public surface)
X3DH-style + Double Ratchet for 1:1 chatKnown structure for async init and forward secrecy on a Vault-derived identityRolling a custom messenger ratchet; group/MLS (out of scope)

Honest residuals (unlocked tab, weak passwords, MAC≠rollback, pre-1.0 root rotation) stay in the security model.

Implementation shape

DecisionWhy
L0 / L1 / L2 as separate proofsCollapsing them is how “sudo opened my notes” bugs appear
Blind store plugin vs vault UX clientServer accepts wraps; product create/unlock lives in @railman/zero-vault
PRF salt in browser crypto configSalt is tenant public material for PRF ceremony, not a Better Auth server secret
M6 denylist on key-store routesFail closed if raw secrets appear in JSON bodies
Passkey counter guard + shared controllerStock Better Auth counter alone is not enough for our elevate/passkey story
Chat consumes Vault identity seedOne unlock path; zero-e2e stays free of WebAuthn

Composition sketches that typecheck: docs-site/verified-examples/ (pnpm docs:verify-examples). How to wire them: Use cases, Quick start.

Upstream packages

UpstreamRole hereHow we treat it
better-authSession, plugins, adaptersExact peer pin on core packages; hook/callback ordering is security-sensitive — bump only with a pinned-runtime pass
@better-auth/passkeyStock WebAuthn plugin factoryAlways via enhancePasskey(passkey, …) — we freeze posture and attach the counter guard
@better-auth/utilsOTP / base64 helpers (elevate)Catalog may carry a newer utils while Better Auth still peers an older exact version; docs-site matches BA’s peer where it composes BA directly
@noble/hashes / @noble/curves / @noble/ed25519HKDF, SHA-256, X25519, Ed25519Small, auditable crypto building blocks for vault/E2E paths
@scure/bip39Mnemonic encode/decodeStandard BIP-39 handling for recovery
@simplewebauthn/serverWebAuthn server verification (via BA passkey stack)Stay aligned with the Better Auth passkey release we pin

We are not a fork of Better Auth. We are plugins and clients that assume a reviewed BA line and make zero-access properties explicit.

What this page is not

  • Not a certification or compliance matrix
  • Not a substitute for the integration checklist
  • Not a claim that upstream packages share our threat model

When we change a primitive or pin, update this rationale alongside Crypto primitives and the security tests.

On this page