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
| Layer | Where | Algorithm | Purpose |
|---|---|---|---|
| Content encryption | Client | AES-256-GCM | Encrypt notes, passwords & structured entries before upload |
| File content encryption | Client | AES-256-GCM (HKDF lifevault-files subkey) | Encrypt uploaded file bytes on the device before upload (client-mode vaults — the default) |
| Generated-document encryption | Server | AES-256-GCM | Encrypt LifeVault-generated documents (legal-template PDFs) at rest; DEK wrapped in Azure Key Vault |
| Key wrapping | Client | AES-KW (RFC 3394) | Wrap DEKs and ASK for server-side storage |
| Envelope encryption | Server | AES-256-GCM | Additional 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).
| Parameter | Value | Notes |
|---|---|---|
| Algorithm | AES-GCM | Authenticated Encryption with Associated Data (AEAD) |
| Key length | 256 bits | Maximum AES key size |
| IV (nonce) | 96 bits (12 bytes) | Randomly generated per encryption operation using a CSPRNG |
| Authentication tag | 128 bits | GCM default; detects any tampering with ciphertext |
| IV source | window.crypto.getRandomValues() / OS CSPRNG | Cryptographically 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 string | Used for |
|---|---|
lifevault-files | File content encryption |
lifevault-notes | Note content encryption |
lifevault-passwords | Password and passkey credential encryption |
lifevault-verifier | Password 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):
| Parameter | Value | Standard |
|---|---|---|
| Algorithm | PBKDF2-HMAC-SHA256 | NIST SP 800-132 |
| Iterations (v2, current) | 600,000 | OWASP 2023 minimum recommendation |
| Iterations (v1, legacy) | 100,000 | Applied to accounts created before the v2 migration |
| Salt | 32 bytes, random per account | Generated via CSPRNG at account creation |
| Output length | 256 bits | |
| Key purpose | AES-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
| Data | Server access |
|---|---|
| User email address | Yes — used for account identification and recovery email delivery |
| Display name | Yes |
| 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 verifier | Yes — used to confirm correct password client-side; reveals nothing about the password |
| Per-vault wrapped DEKs | Yes — 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 files | Yes — ciphertext only; the server cannot decrypt these |
| Uploaded file contents | Stored 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 status | Yes |
What the server cannot see
| Data | Status |
|---|---|
| Master password | Never transmitted or stored |
| Account Symmetric Key (ASK) in plaintext | Never transmitted or stored |
| Per-vault Data Encryption Keys (DEKs) in plaintext | Never transmitted or stored |
| Note contents, passwords, structured vault entries, passkey credentials, and uploaded file contents | Never 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:
| Column | Contents | Server-decryptable |
|---|---|---|
account_key_salt | 32-byte random KDF salt | Not applicable (public parameter) |
wrapped_account_key | AES-KW(Master KEK, ASK) | No — requires master password |
account_key_verifier | AES-GCM ciphertext of known constant | No |
public_wrapping_key | HKDF-derived AES-KW key bytes, for vault sharing | No — only usable to wrap, not decrypt |
recovery_wrapped_account_key | AES-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
helmetmiddleware to set security-relevant HTTP headers (includingX-Content-Type-Options,X-Frame-Options,Strict-Transport-Security, andX-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
| Component | Platform |
|---|---|
| Web application and API | Vercel (serverless functions, edge CDN) |
| Database | Supabase (managed PostgreSQL with PgBouncer connection pooling) |
| File storage | Supabase Storage (S3-compatible object storage, private bucket) |
| Mobile app | Distributed 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
| Primitive | Algorithm | Parameters | Used for |
|---|---|---|---|
| Symmetric encryption | AES-GCM | 256-bit key, 96-bit IV, 128-bit auth tag | All vault content encryption |
| Key derivation (password) | PBKDF2-HMAC-SHA256 | 600,000 iterations, 32-byte salt | Master KEK derivation |
| Key derivation (subkeys) | HKDF-SHA256 | Fixed zero salt, unique info string per purpose | Subkey and wrapping key derivation |
| Key wrapping | AES-KW (RFC 3394) | 256-bit | ASK and DEK wrapping |
| Password hashing | bcrypt | Cost factor 10 | Account password storage; OTP storage |
| Token generation | crypto.randomBytes(32) | 256 bits | Password reset tokens, secret link tokens |
| Random number generation | window.crypto.getRandomValues() / OS CSPRNG | Per-operation | IVs, salts, challenges |
| Recovery phrase | BIP39-compatible, 12 words | 128-bit entropy + 4-bit checksum | Account recovery |
| FIDO2/WebAuthn | Platform-native | ECDSA P-256 (-7), RSA-PKCS1v1.5 (-257) | Hardware authenticator support |
Appendix B — KDF Version History
| Version | Iterations | Status | Notes |
|---|---|---|---|
| v1 | 100,000 | Legacy | Applied to accounts created before the OWASP hardening migration |
| v2 | 600,000 | Current | OWASP 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.