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:
- A database dump does not yield vault plaintext.
- Step-up for account actions does not imply decrypt.
- 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)
| Choice | Rationale | Alternatives considered |
|---|---|---|
| WebAuthn + PRF for daily unlock | Authenticator-bound secret that never needs to be typed or stored as a password; degrades to password/recovery when PRF is missing | Password-only vaults; custom soft tokens; server-held MEKs |
| HKDF domain separation (purpose + userId) | Prevents purpose and tenant mix-ups at the KDF boundary | Single shared “app key” derivation |
| PBKDF2-SHA256 for password/recovery wraps | Widely available in Web Crypto; high iteration floor in helpers | Argon2 (better memory-hardness, heavier browser story for our current helpers) |
| AES-GCM for seals | Authenticated encryption with Web Crypto; AAD for context binding | ChaCha20-Poly1305 (fine cryptographically; less natural in browser Web Crypto today) |
| BIP-39 recovery phrase | Human-exportable break-glass; well-understood UX | Raw high-entropy blobs only; SMS/email “recovery” (rejected for vault secrets) |
| Ed25519 for account-recovery Mode B | Compact signatures; server verifies without holding the phrase | Server-held recovery secrets |
| HMAC for elevate / login-factor / row MACs | Detects field tampering under a server root without inventing a full HSM story | JWT-only claims with no row MAC; client-supplied claim blobs |
| Single root → HKDF families | Apps should not manage key rings; rotate/lifecycle is already hard enough | User-configured multi-key rings (removed from the public surface) |
| X3DH-style + Double Ratchet for 1:1 chat | Known structure for async init and forward secrecy on a Vault-derived identity | Rolling 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
| Decision | Why |
|---|---|
| L0 / L1 / L2 as separate proofs | Collapsing them is how “sudo opened my notes” bugs appear |
| Blind store plugin vs vault UX client | Server accepts wraps; product create/unlock lives in @railman/zero-vault |
| PRF salt in browser crypto config | Salt is tenant public material for PRF ceremony, not a Better Auth server secret |
| M6 denylist on key-store routes | Fail closed if raw secrets appear in JSON bodies |
| Passkey counter guard + shared controller | Stock Better Auth counter alone is not enough for our elevate/passkey story |
| Chat consumes Vault identity seed | One 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
| Upstream | Role here | How we treat it |
|---|---|---|
better-auth | Session, plugins, adapters | Exact peer pin on core packages; hook/callback ordering is security-sensitive — bump only with a pinned-runtime pass |
@better-auth/passkey | Stock WebAuthn plugin factory | Always via enhancePasskey(passkey, …) — we freeze posture and attach the counter guard |
@better-auth/utils | OTP / 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/ed25519 | HKDF, SHA-256, X25519, Ed25519 | Small, auditable crypto building blocks for vault/E2E paths |
@scure/bip39 | Mnemonic encode/decode | Standard BIP-39 handling for recovery |
@simplewebauthn/server | WebAuthn 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.