Back to app

Security Architecture

This page explains exactly how VaultLink works at a cryptographic level, what it protects against, and where its boundaries are. No marketing. If you're reviewing this with a security mindset — that's intentional.

TL;DR

All encryption and decryption happen in the browser using the Web Crypto API. The server stores only ciphertext and never receives the encryption key. The key travels only in the URL fragment, which is never sent in HTTP requests and is immediately stripped from history after reading.

Encryption Flow (sender, in-browser)

  1. Browser generates a random 256-bit key via crypto.getRandomValues.
  2. A unique 128-bit salt is generated.
  3. PBKDF2 (SHA-256, 100,000 iterations) derives an AES-256-GCM key.
  4. Secret is encrypted with AES-256-GCM using a unique 96-bit IV.
  5. Only { encrypted_blob, iv, salt } are sent to the server.

The raw encryption key never leaves the browser.

Key Transport via URL Fragment

The key is appended to the access URL as a URL fragment:

/access?token=<access_token>#key=<base64_key>
  • URL fragments are never sent in HTTP requests.
  • Do not appear in server logs, reverse proxies, or APM tools.
  • Not included in the Referer header.
  • history.replaceState removes the fragment immediately after reading — it survives neither a Back press nor a copy of the address bar.
  • Key is held only in React component state — never written to localStorage, sessionStorage, cookies, or global scope.

Decryption Flow (recipient, in-browser)

  1. Recipient verifies identity via 6-digit OTP sent to their email.
  2. Server returns only encrypted_blob, iv, and salt.
  3. Browser reads the key from the URL fragment (already stripped from history).
  4. Decryption runs locally via Web Crypto API.
  5. Plaintext exists only in component memory and is cleared on navigation.

At no point does plaintext pass through the server, appear in server logs, or persist in server memory.

Data at Rest

The database stores:

  • encrypted_blobciphertext only
  • iv + saltrequired for decryption, useless without the key
  • label, note, recipient_emailplaintext — see Known Metadata below
  • OTP hashSHA-256, not reversible
  • IP addressesSHA-256(ip + IP_HASH_SALT), not reversible without the salt
Full database compromise: no plaintext secrets recoverable, no encryption keys recoverable, OTPs not reversible, IPs not reversible without IP_HASH_SALT.

Atomic View Enforcement

View limits are enforced atomically in Postgres to prevent race condition bypass:

UPDATE secrets
SET view_count = view_count + 1
WHERE id = p_id
  AND (p_max_views IS NULL OR view_count < p_max_views)
RETURNING TRUE;
  • Prevents Race condition bypass
  • Prevents Double-consume attacks
  • Prevents TOCTOU flaws

OTP & Rate Limiting

  • 6-digit OTP, SHA-256 hash stored in DB
  • Max 5 attempts, 10-minute expiry
  • Sliding-window rate limiter via Upstash Redis (distributed — shared across all server instances)
  • Prevents OTP brute-force and multi-instance rate limit bypass

Threat Boundaries

Protects against

  • Database compromiseciphertext only
  • Passive network interception
  • Server log leakage of keys
  • OTP brute force
  • View race conditions
  • Secret tamperingAES-GCM auth tag

Does not protect against

  • Malicious recipient copying the secret
  • Screenshot or recording
  • Compromised recipient device
  • XSS from future code changes
  • Full runtime server compromise

Zero-knowledge with respect to storage — not zero-trust against recipients.

Known Plaintext Metadata

For usability, the following fields are stored unencrypted: label, note, recipient_email. Secret values themselves remain encrypted.

Sender Identity

Sender identity is derived from a random token stored in localStorage. This is a convenience feature — it allows the dashboard to group secrets by the browser that created them. It is not a secure identity mechanism, not authenticated, and not tied to any account or credential. Do not treat it as proof of authorship.

Design Philosophy

VaultLink is intentionally scoped. It is not a password manager, a vault replacement, a compliance-certified platform, or a screenshot prevention tool.

It is a pragmatic bridge between systems — built to reduce accidental credential exposure in chat-based workflows. Security decisions prioritize minimal server trust, explicit threat boundaries, atomic enforcement, and distributed rate limiting.

VaultLink is built to be boringly correct.