Skip to content

Security Model

  • Keep vault plaintext on the client whenever possible
  • Separate authentication concerns from vault encryption concerns
  • Fail fast on missing critical secrets at startup
  • Validate and sanitize all incoming data
  • Keep auth, inactivity, and handover actions auditable
  • Vault entries are encrypted in the browser before being sent to the API
  • AES-256-GCM via the Web Crypto API
  • Key derivation: PBKDF2 with SHA-256, 100,000 iterations
  • The server stores encrypted payloads, IVs, salts, and metadata only — never plaintext
  • Client derives an auth key from password + email via PBKDF2 before sending — the raw password never leaves the browser
  • Server stores bcrypt hash of the auth key (rounds = 12)
  • Browser auth uses httpOnly, Secure JWT cookies
  • API clients may also authenticate with Authorization: Bearer <token>
  • Protected routes validate both the JWT and the backing session record
  • Refresh tokens are path-scoped to /api/v1/auth/refresh
  • Every login creates a tracked server-side session
  • Users can view active sessions, revoke one, or invalidate all others
  • Session cleanup runs as a scheduled maintenance task
  • TOTP 2FA with configurable issuer label
  • Recovery codes for lost authenticator access
  • 2FA is validated as part of the login flow, not as a separate step that can be bypassed
  • Failed attempts tracked per account in Redis over a 15-minute window
  • Instead of locking the account, repeated failures add an exponentially increasing delay (capped at ~5 seconds)
  • A correct password is never blocked — only briefly slowed
  • This prevents brute-force guessing without creating a denial-of-service vector on a dead man’s switch account (a locked account could prevent the owner from cancelling an active handover)
  • Complemented by per-IP rate limiting and slow bcrypt hashing
  • General API limiter, stricter auth limiter, registration limiter, contact form limiter
  • Implemented with express-rate-limit
  • All request schemas validated with Zod
  • Content-Type checked on JSON mutation routes
  • Input sanitization removes dangerous HTML/script patterns and suspicious protocols
  • Prototype-pollution keys (__proto__, constructor, prototype) are explicitly rejected
  • Production cookies are Secure and configured for browser usage
  • CORS uses an explicit allowlist via CORS_ORIGINS
  • Helmet.js sets security headers including frameAncestors: none
  • Structured Pino logging throughout the API
  • Activity records are HMAC-signed with ACTIVITY_HMAC_SECRET for integrity verification
  • Activity history is queryable through authenticated endpoints
  • User activity comes from both authenticated actions and explicit check-ins
  • Manual check-in, secure emailed check-in links, and logins all reset the inactivity state
  • Active grace-period handovers are cancelled when the user checks in
  • Successors must verify their email identity before accessing successor flows
  • Owners can restrict each successor to assigned vault entries only
Limitation Notes
Rate limiting not Redis-backed Not safe for multi-instance deployments without an external limiter
Admin authorization is allowlist-based No full role model yet
No WebAuthn / passkeys TOTP only
No external security audit The codebase has not been professionally penetration-tested
CSP / HSTS Expected to be enforced by the frontend host or reverse proxy

The following must be set before the API starts:

  • JWT_SECRET — signs access and refresh tokens
  • ACTIVITY_HMAC_SECRET — signs activity log records

Generate both with openssl rand -base64 64.

Please do not open public GitHub issues for security problems.

Email security@handoverkey.app — see SECURITY.md for the full disclosure process.

  • WebAuthn / passkey support
  • Redis-backed distributed rate limiting
  • Richer role-based authorization
  • External security review and penetration testing
  • Stronger deployment-level security header baselines