Skip to main content
Security White Paper

LifeVault Secure Security Architecture

Version 1.0 — Published 2026 — Applies to the LifeVault Secure web application, API, and mobile clients

Security at a glance

  • Notes, passwords, structured vault entries, and the files you upload are encrypted on the client with AES-256-GCM before any data is transmitted; the server stores only opaque ciphertext for these. Documents that LifeVault generates for you (such as legal-template PDFs) and file metadata are encrypted with server-side AES-256 at rest.
  • Your master password is processed through PBKDF2-SHA256 (600,000 iterations) locally. It never leaves your device.
  • Zero-knowledge design for your end-to-end-encrypted content: we have no technical ability to decrypt your notes, passwords, structured vault entries, or the files you upload. There is no backdoor for that content.
  • FIDO2/WebAuthn support for hardware security keys, Face ID, Touch ID, and Windows Hello.
  • Defense-in-depth server-side AES-256-GCM envelope encryption on top of the client-side key hierarchy.

1. Architecture Overview

LifeVault Secure is built on a zero-knowledge, client-side encryption model. The server stores only opaque encrypted blobs for end-to-end-encrypted content. No plaintext notes, passwords, structured entries, identities, or uploaded file contents ever reach our servers — files you upload are encrypted on your device before transmission. Documents that LifeVault generates for you (legal-template PDFs) are rendered server-side and encrypted with server-side AES-256 at rest, as is file metadata.

The security architecture has two distinct encryption layers:

Layer 1 — Account Key Hierarchy (client-side, zero-knowledge)

Each user's vault data is protected by a key hierarchy derived entirely from their master password inside the browser or mobile app. The server never receives the password or any key that can decrypt vault contents.

Layer 2 — Server-side Envelope Encryption (defense in depth)

Encrypted blobs stored in the database are additionally protected by a server-managed AES-256-GCM master key (ENCRYPTION_MASTER_KEY). This provides defense in depth: even if the database were extracted, an attacker would need both the server environment variable and the user's master password to access end-to-end-encrypted content (notes, passwords, structured vault data, and uploaded files). Files you upload are end-to-end encrypted on your device, so the server never holds their plaintext or their content key — the server-side envelope is layered on top of client ciphertext and does not grant read access to file contents. Documents that LifeVault generates for you, and file metadata, are protected by server-side AES-256 with the data-encryption key wrapped in Azure Key Vault.

Key architectural guarantee: The client derives the full key hierarchy locally from the master password. Only ciphertext, KDF salts, and wrapped keys are transmitted to or stored by the server. Plaintext notes, passwords, structured vault entries, and uploaded file contents never leave the client. Documents that LifeVault generates for you are encrypted with server-side AES-256 at rest.

Encryption flow summary

LayerWhereAlgorithmPurpose
Content encryptionClientAES-256-GCMEncrypt notes, passwords & structured entries before upload
File content encryptionClientAES-256-GCM (HKDF lifevault-files subkey)Encrypt uploaded file bytes on the device before upload (client-mode vaults — the default)
Generated-document encryptionServerAES-256-GCMEncrypt LifeVault-generated documents (legal-template PDFs) at rest; DEK wrapped in Azure Key Vault
Key wrappingClientAES-KW (RFC 3394)Wrap DEKs and ASK for server-side storage
Envelope encryptionServerAES-256-GCMAdditional server-side protection of stored blobs

2. Encryption

Symmetric cipher

All vault content is encrypted using AES-256-GCM (Advanced Encryption Standard, 256-bit key, Galois/Counter Mode).

ParameterValueNotes
AlgorithmAES-GCMAuthenticated Encryption with Associated Data (AEAD)
Key length256 bitsMaximum AES key size
IV (nonce)96 bits (12 bytes)Randomly generated per encryption operation using a CSPRNG
Authentication tag128 bitsGCM default; detects any tampering with ciphertext
IV sourcewindow.crypto.getRandomValues() / OS CSPRNGCryptographically secure

GCM mode provides both confidentiality and integrity — any modification to ciphertext or metadata will cause decryption to fail with an authentication error rather than silently producing corrupted output.

Where encryption happens

All vault content (files, notes, passwords, passkey metadata) is encrypted entirely on the client before any data is transmitted to the server. The API receives only ciphertext.

The Web Crypto API (window.crypto.subtle) is used on web and admin clients. The mobile app uses react-native-quick-crypto, which provides the same Web Crypto API surface backed by native platform cryptography primitives.

Subkey isolation

Each vault's DEK is not used directly for encryption. Instead, it is used as input material for HKDF to derive four independent, purpose-specific AES-256-GCM subkeys:

Purpose info stringUsed for
lifevault-filesFile content encryption
lifevault-notesNote content encryption
lifevault-passwordsPassword and passkey credential encryption
lifevault-verifierPassword verification token

Each subkey is non-extractable (marked extractable: false in the Web Crypto API). Compromise of one subkey does not compromise others, and none of the subkeys can be used to reconstruct the DEK.

3. Key Derivation

Master Password to Master KEK

When a user creates an account or unlocks their vault, their master password is processed through PBKDF2-HMAC-SHA256 to produce a Key Encryption Key (KEK):

ParameterValueStandard
AlgorithmPBKDF2-HMAC-SHA256NIST SP 800-132
Iterations (v2, current)600,000OWASP 2023 minimum recommendation
Iterations (v1, legacy)100,000Applied to accounts created before the v2 migration
Salt32 bytes, random per accountGenerated via CSPRNG at account creation
Output length256 bits
Key purposeAES-KW (key wrapping only — never used for data encryption)

The KDF version is stamped on each vault and user account record. Accounts on v1 are prompted to migrate on next password change.

Key hierarchy

The full key hierarchy from password to content:

Master Password
    └─ PBKDF2-SHA256 (600K iters, 32-byte salt)
        └─ Master KEK [AES-KW, 256-bit, in-memory only]
            └─ unwraps Account Symmetric Key (ASK) [stored as wrapped bytes]
                ├─ HKDF(“lifevault-vault-wrapping”) → Vault Wrapping Key [AES-KW]
                │   └─ unwraps per-vault DEK [stored as wrapped bytes]
                │       └─ HKDF subkeys: files, notes, passwords, verifier
                └─ HKDF(“lifevault-public-wrapping”) → Public Wrapping Key [for vault sharing]
                └─ HKDF(“lifevault-account-verifier”) → Account Verifier [non-extractable]

  • The master password is never stored anywhere.
  • The Master KEK exists only in memory during an active session.
  • Changing the master password only re-wraps the ASK; vault DEKs are unaffected and existing shared vault memberships remain valid.
  • Each content type uses a cryptographically isolated key.

Recovery phrase

Users can optionally generate a 12-word BIP39-compatible recovery phrase from 128 bits of cryptographic entropy (128 bits + 4-bit SHA-256 checksum = 132 bits). This phrase serves as a second way to unwrap the ASK:

  • The phrase is processed through PBKDF2-SHA256 (600,000 iterations) with a fixed, publicly-known salt (lifevault-recovery-salt-v1).
  • The resulting Recovery KEK wraps the same ASK and is stored server-side as recoveryWrappedAccountKey.
  • The full phrase itself is never stored anywhere — only the AES-KW-wrapped ASK.
  • A SHA-256 hash of the phrase is stored server-side solely to confirm that a recovery key exists; it is not used for authentication.

The security of the recovery path is entirely dependent on the secrecy of the recovery phrase. Users are advised to store it offline, physically separated from their devices.

4. Zero-Knowledge Design

What the server can see

DataServer access
User email addressYes — used for account identification and recovery email delivery
Display nameYes
KDF parameters (salt, KDF version)Yes — required to reconstruct the KEK on the client
Wrapped Account Symmetric Key (ASK)Yes — stored as encrypted bytes; cannot be decrypted without the master password
Account key verifierYes — used to confirm correct password client-side; reveals nothing about the password
Per-vault wrapped DEKsYes — stored as AES-KW-wrapped bytes; cannot be decrypted without the ASK
Vault metadata (name, icon, type)Yes
Notes, passwords, structured vault entries, and uploaded filesYes — ciphertext only; the server cannot decrypt these
Uploaded file contentsStored as client-side-encrypted ciphertext; the server cannot decrypt them
Generated documents (legal-template PDFs)Yes — encrypted at rest with server-side AES-256; the server can decrypt them to render and deliver the document
File metadata (name, type, size)Yes — stored server-side (used for listing, quota, and audit)
Subscription and billing statusYes

What the server cannot see

DataStatus
Master passwordNever transmitted or stored
Account Symmetric Key (ASK) in plaintextNever transmitted or stored
Per-vault Data Encryption Keys (DEKs) in plaintextNever transmitted or stored
Note contents, passwords, structured vault entries, passkey credentials, and uploaded file contentsNever in plaintext; only end-to-end-encrypted blobs are stored, which the server cannot decrypt

Documents that LifeVault generates for you — such as legal-template PDFs — are a separate category: because LifeVault renders them server-side, they are stored encrypted at rest with server-side AES-256, and LifeVault can decrypt them to deliver the document back to you. Generated documents and file metadata (name, type, size) are not part of the zero-knowledge set above. Files that you upload yourself ARE part of the zero-knowledge set: they are encrypted on your device before upload, and LifeVault cannot decrypt them.

Verification without server knowledge

When a user unlocks a vault, the client derives the key hierarchy locally and attempts to decrypt a small verifier token (lifevault-account-verifier) that was encrypted at account creation time. If decryption succeeds and the output matches the expected constant, the password is confirmed correct — entirely on the client, with no server round-trip and no password-equivalent data sent over the wire.

5. Data at Rest

Vault content

Vault content is stored in two places:

PostgreSQL (Supabase): Password entries, passkey metadata, notes, and vault configuration are stored as encrypted byte columns (BYTEA). The plaintext content never reaches the database layer.

Object Storage (Supabase Storage / S3-compatible): Files you upload are encrypted on your device (client-side, with a per-vault lifevault-files key derived from your master password) before they are stored; the blobs in object storage are opaque ciphertext that LifeVault cannot decrypt. Documents that LifeVault generates for you (legal-template PDFs) are encrypted server-side with AES-256-GCM, the data-encryption key envelope-wrapped in Azure Key Vault — LifeVault manages those keys and can decrypt generated documents to deliver them. Object storage may apply its own at-rest encryption to all blobs, but for uploaded files this is layered on top of client-side ciphertext and does not give LifeVault access to your file contents. File metadata (name, type, size) is stored server-side (server-readable; used for listing, quota, and audit).

Account key material

The following encrypted key material is stored in the users table:

ColumnContentsServer-decryptable
account_key_salt32-byte random KDF saltNot applicable (public parameter)
wrapped_account_keyAES-KW(Master KEK, ASK)No — requires master password
account_key_verifierAES-GCM ciphertext of known constantNo
public_wrapping_keyHKDF-derived AES-KW key bytes, for vault sharingNo — only usable to wrap, not decrypt
recovery_wrapped_account_keyAES-KW(Recovery KEK, ASK)No — requires recovery phrase

Server-side encryption layer

As an additional layer of defense, the API wraps certain sensitive fields with a server-managed AES-256-GCM master key (ENCRYPTION_MASTER_KEY), stored as a 256-bit hex environment variable. This key is never committed to source control and is managed via deployment secrets. Even if the database were exfiltrated without the server environment, the attacker would face the server-side encryption layer in addition to the user's client-side key hierarchy.

6. Data in Transit

TLS

All communication between clients (web, mobile, browser extension) and the LifeVault Secure API is encrypted using TLS 1.2 or higher, enforced by the hosting platform (Vercel). HTTP connections are automatically redirected to HTTPS.

API request security

  • All API responses that return user-specific data include Cache-Control: private, no-store, max-age=0. This prevents any intermediate proxy or CDN from caching authenticated responses.
  • ETags are globally disabled on the API to prevent Vercel's CDN from serving stale 304 responses that could omit CORS headers on cross-origin authenticated requests.
  • CORS is configured to allowlist only the production web origin and explicitly registered extension IDs. Wildcard origins are not permitted in production.
  • The API uses the helmet middleware to set security-relevant HTTP headers (including X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security, and X-XSS-Protection).

File upload proxy

File uploads are proxied through a Next.js server-side route (/upload-proxy) rather than allowing direct browser-to-storage requests. This route enforces an allowlist of permitted storage hostnames, preventing SSRF (Server-Side Request Forgery). The client transmits encrypted blobs only — the proxy has no ability to decrypt them.

7. Authentication

Password authentication

Account passwords are hashed with bcrypt (cost factor 10) before storage. The plaintext password is never logged or persisted. During login, bcrypt.compare() is used for timing-safe comparison.

Authentication responses return a JWT (JSON Web Token) signed with a secret that must be a 256-bit+ random value in production. The token includes user ID, email, subscription tier, and email verification status. Token expiry is set to 7 days.

Dedicated rate limiting applies to all authentication endpoints: 15 requests per 15-minute window per IP. This applies to login, signup, password reset, email verification, and resend flows.

The API uses constant-time responses and generic error messages (Invalid email or password) on authentication failure to prevent user enumeration.

Email verification

When email verification is enabled, accounts cannot access protected endpoints until the registered email is confirmed via a 6-digit OTP. OTPs are:

  • Generated with crypto.randomInt() (CSPRNG-backed)
  • Hashed with bcrypt before storage (the raw OTP is never stored)
  • Valid for 15 minutes
  • Subject to a 5-attempt lockout before requiring a new code
  • Subject to a 60-second resend cooldown

WebAuthn / Hardware security keys

LifeVault Secure supports FIDO2/WebAuthn for strong second-factor authentication and passwordless flows. Users can register hardware security keys or platform authenticators (Face ID, Touch ID, Windows Hello). Challenges are generated with crypto.randomBytes(32) per authentication ceremony. Sign count is tracked and incremented on each use.

Microsoft Entra ID (Azure AD)

Enterprise and Microsoft account users can authenticate via Microsoft's OAuth2 / OIDC flow. The server validates the Microsoft-issued b2cSubject identifier and does not process or store the user's Microsoft password.

Session model

Sessions are stateless JWT tokens. There is no server-side session store to compromise. Token revocation is implemented at the application layer for security events. The API enforces requireAuth middleware on all authenticated routes; high-impact admin operations use requireAuthStrict, which explicitly disallows development auth bypasses.

8. Emergency Access

Design model

Emergency access allows a vault owner to pre-authorize specific trusted contacts to request access to their vaults. This is designed for situations where the owner is incapacitated and cannot respond in time, rather than as a routine sharing mechanism.

Mechanism

Emergency access is governed by an AccessPolicy with type emergency_access, configured by the vault owner. The policy specifies:

  • Which contacts are authorized to request access
  • A mandatory waiting period (default: 2,880 minutes / 48 hours) before access can be granted

When a trusted contact submits an emergency access request, a cooldown timer starts. The vault owner receives a notification and has the full cooldown window to review and reject the request. If the owner does not act, the request can be approved after the cooldown expires.

Threat model

The waiting period is the primary protection against unauthorized emergency access requests. Even if an attacker gains access to a trusted contact's account, they cannot immediately access the target vault — the legitimate owner has a window to detect and reject the request.

The vault owner retains full control to reject any pending request at any time before it is approved.

Emergency access provides the trusted contact with access to vault content through the access control system. The cryptographic protection of end-to-end-encrypted content (notes, passwords, structured vault data, and the files you upload) still applies; emergency access operates at the authorization layer and releases content through the established key-sharing mechanism, not by bypassing encryption or granting the server plaintext access.

9. Infrastructure

Hosting

ComponentPlatform
Web application and APIVercel (serverless functions, edge CDN)
DatabaseSupabase (managed PostgreSQL with PgBouncer connection pooling)
File storageSupabase Storage (S3-compatible object storage, private bucket)
Mobile appDistributed via App Store and Google Play

Database security

  • The database is a managed Supabase PostgreSQL instance. Direct database access is restricted to application connection strings stored as deployment secrets.
  • The application uses Supabase's PgBouncer pooler (port 6543) for runtime connections and the direct connection (port 5432) only for schema migrations.
  • Row-level security and access control are enforced at the application layer via Prisma ORM with explicit user-ownership checks on every query.

Secrets management

Application secrets (JWT signing key, encryption master key, database credentials, Stripe keys) are stored as Vercel environment variables and never committed to source control. The ENCRYPTION_MASTER_KEY must be a 64-character hex string (256-bit) in production; the API will refuse to start if this requirement is not met.

Dependency management

The application is built on a TypeScript monorepo. Dependencies are managed with pnpm and locked via pnpm-lock.yaml. Security-relevant cryptographic operations use the browser's built-in Web Crypto API (window.crypto.subtle) rather than third-party cryptographic libraries, minimizing the supply chain attack surface for the core encryption path.

10. What We Do Not Do

We do not store your master password.

Your master password never leaves your device. We store only the cryptographic salt and the AES-wrapped Account Symmetric Key that your password produces.

We do not have a backdoor to your vault.

We have no technical capability to decrypt your end-to-end-encrypted content — your notes, passwords, structured entries, and the files you upload. There is no master override key and no recovery path for that content that does not require either your master password or your recovery phrase.

We do not mine or analyze your vault data.

Your notes, passwords, structured vault data, and the files you upload reach our servers only as ciphertext that we cannot decrypt. Documents that LifeVault generates for you, and file metadata, are encrypted at rest with server-side AES-256. We have no mechanism to read, analyze, categorize, or monetize your end-to-end-encrypted content (notes, passwords, structured entries, and uploaded files).

We do not use advertising-based tracking inside the application.

The application does not include advertising SDKs, behavioral analytics platforms, or any third-party integrations that receive vault content or browsing behavior within the authenticated application.

We do not sell data to third parties.

We do not share user data — including email addresses, usage patterns, or any vault metadata — with data brokers or advertising networks.

We do not transmit passwords in cleartext for breach checking.

Breach detection via Have I Been Pwned uses the k-anonymity model: only the first 5 hex characters of a SHA-1 hash of the password are sent to the HIBP API. The full password hash and the plaintext password never leave the client. This check runs client-side before encryption, because the server never sees the plaintext password at all.

Appendix A — Cryptographic Primitive Summary

PrimitiveAlgorithmParametersUsed for
Symmetric encryptionAES-GCM256-bit key, 96-bit IV, 128-bit auth tagAll vault content encryption
Key derivation (password)PBKDF2-HMAC-SHA256600,000 iterations, 32-byte saltMaster KEK derivation
Key derivation (subkeys)HKDF-SHA256Fixed zero salt, unique info string per purposeSubkey and wrapping key derivation
Key wrappingAES-KW (RFC 3394)256-bitASK and DEK wrapping
Password hashingbcryptCost factor 10Account password storage; OTP storage
Token generationcrypto.randomBytes(32)256 bitsPassword reset tokens, secret link tokens
Random number generationwindow.crypto.getRandomValues() / OS CSPRNGPer-operationIVs, salts, challenges
Recovery phraseBIP39-compatible, 12 words128-bit entropy + 4-bit checksumAccount recovery
FIDO2/WebAuthnPlatform-nativeECDSA P-256 (-7), RSA-PKCS1v1.5 (-257)Hardware authenticator support

Appendix B — KDF Version History

VersionIterationsStatusNotes
v1100,000LegacyApplied to accounts created before the OWASP hardening migration
v2600,000CurrentOWASP 2023 minimum for PBKDF2-SHA256

All new accounts and vault creations use v2. Legacy v1 accounts are prompted to migrate on next password change, at which point the ASK is re-wrapped using v2 KDF parameters.

This document describes the security architecture as implemented in the current LifeVault Secure codebase. It is updated when significant architectural changes are made. For security disclosures, contact security@lifevaultsecure.com.

← Back to Security Overview