Zero Accessby Railmandocs

Security model

Trust boundaries, non-negotiables, and what residual risk remains.

Security model

This is a zero-knowledge-oriented design: the server is untrusted for plaintext vault material. It is still trusted for availability, authorization of who may store wraps, and session integrity.

Trust boundaries

 Device (browser)              App server + plugins              Database
 ┌──────────────────┐         ┌──────────────────────┐        ┌──────────┐
 │ MEK in memory    │  TLS    │ Sessions, elevate    │        │ Wrap     │
 │ PRF / password   │ ──────► │ Blind wraps only     │ ──────►│ ciphertext│
 │ Recovery phrase  │         │ Never raw MEK        │        │ metadata │
 └──────────────────┘         └──────────────────────┘        └──────────┘
ZoneTrusted forUntrusted for
User device (unlocked)Decrypting that user’s dataOther users
App JS (your frontend)Same as the deviceServer secrets
Better Auth serverSessions, policy, wrap storagePlaintext MEK
Database dump attackerReading wrap ciphertextUnwrapping without client secrets

Non-negotiables

  1. Elevate never unlocks MEK — no “if elevated, open vault” shortcuts.
  2. Server stores wraps + metadata only — never raw MEK, mnemonic, PRF output, or vault password.
  3. PRF info binds purpose + userId — cross-user mixups must fail.
  4. Authenticator enforce filters fail closed when attestation is not explicitly available.
  5. Authorize elevate on the server from the session row (and MAC where used) — not from a client-supplied claim blob alone.
  6. Creating a vault is client/product code — the BA plugin only accepts already-wrapped material.

M6 — raw secret denylist

Request bodies to key-store routes are scanned for forbidden keys (mnemonic, rawMek, prfSecret, authSecret, …). Matching keys return 400 with a stable error code. Allowed recovery fields are carefully limited (for example public keys / verifiers, never the phrase).

Persistent key authority

zeroAccess({ secret?: string }) owns one server-only root. Omission captures Better Auth's resolved secret; an explicit stable Railman secret separates persistent-data lifecycle from session-secret rotation. The root derives non-extractable MAC and storage-ID families with a different purpose for each row or index class. Applications never receive or manage child authorities.

The MAC family protects persistent vault/E2E rows, recovery control rows, and the passkey counter watermark. It detects field tampering by a database attacker who lacks the root. It does not detect deletion or rollback of a complete, previously valid snapshot.

The storage-ID family covers low-entropy pseudonyms, rate/replay/challenge keys, and ephemeral recovery-bearer lookup. Long-lived E2E row reachability instead uses versioned public deterministic SHA-256 identifiers over canonical natural inputs. Those identifiers remain server-only and are included in row MACs.

This pre-1.0 format is a clean cutover. There are no legacy readers or migration path. Replace old security rows before serving the new format. Root loss or rotation invalidates current authenticated rows and passkey watermarks; no rotation CLI exists yet.

Residual risks (honest)

ResidualWhy it remains
Stolen unlocked browser tabMEK is in memory by design while unlocked — use idle lock
Stolen session cookieAttacker can store wraps as that user, not unwrap without L2
Stolen session after elevateShort TTL + session bind + optional rotation
Weak vault passwordClient iterations floor helps; users can still pick poorly
Hosted demo D1 wipeLab data is disposable

Verification

Maintainers run the hosted-equivalent core gate and security suites:

pnpm ci:core         # build · lint · typecheck · unit:core · security · package check
pnpm test:security   # tests/security matrix only

There is no separate security npm package — checks are tests.

Builders integrating the plugins should walk the integration checklist and the live Vault lab.

Pin the exact reviewed Better Auth release (1.6.28 today) — do not use a peer range for the core plugins.

On this page