Skip to content

Security: hdcodedev/resume256

Security

SECURITY.md

Security

Resume256 is built around the idea that a resume builder shouldn't need access to your actual resume data. This document explains how we achieve that and what you should know to keep your data secure.


How Encryption Works

Your resume is encrypted directly in your browser before it leaves your device. We use AES-256-GCM, a widely trusted encryption standard, through the browser's built-in Web Crypto API.

Here's the short version:

  1. When you create or edit a resume, your browser encrypts it with a key only you control
  2. The encrypted data is sent to our servers for storage
  3. When you need your resume, your browser downloads the encrypted blob and decrypts it locally

We never see your resume content. What we store is ciphertext—scrambled data that's meaningless without your key.

One Exception: When you export your resume as a PDF, the /api/pdf-render/[token] endpoint temporarily receives your key to decrypt the resume for rendering. This process happens entirely in memory. We explicitly do not log the key or decrypted content, and all data is discarded immediately after the PDF is generated.


Key Management

Your encryption key is stored in your browser's IndexedDB, protected by a wrapping key derived from your user ID and a device-specific identifier. This means your keys persist across sessions on the same device—logging out and back in won't lock you out of your data. A few things to keep in mind:

  • Back up your keys. If you lose access to your encryption key, we cannot recover your data. This is intentional—it's the tradeoff for zero-knowledge encryption. Export your keys from the Security settings page and store them somewhere safe (like a password manager).

  • Keys stay local. Your encryption keys never leave your device (except transiently for PDF export as noted above). Even if our servers were compromised, attackers would only get encrypted data they can't read.

  • Per-resume sharing keys. When you create a shareable link, a unique decryption key for that resume is embedded in the URL fragment (the part after #). This fragment never hits our servers—it goes directly to the recipient's browser.


What We Store

Data Type Stored As
Resume title Encrypted ciphertext
Resume content Encrypted ciphertext
Resume metadata User ID, timestamps, template ID
Encryption keys Never stored server-side

Metadata like template choice and timestamps are stored in plaintext since they're needed for the app to function. Your actual resume content—work history, contact info, skills—is always encrypted.


Server-Side Protections

Beyond client-side encryption, we have standard infrastructure protections in place:

Rate Limiting

Public-facing and resource-intensive endpoints are rate-limited using Upstash Redis:

  • Resume operations: 30 requests/minute
  • PDF generation: 5 requests/minute (it's expensive)
  • Public endpoints: 500 requests/hour
  • Share link creation: 10/hour
  • AI enhancements: 30/hour

If you hit a limit, the API returns a 429 status with a Retry-After header.

HTTP Security Headers

We set the usual security headers via next.config.ts:

  • Content-Security-Policy – restricts what can run on the page
  • Strict-Transport-Security – forces HTTPS
  • X-Content-Type-Options: nosniff
  • X-Frame-Options: SAMEORIGIN
  • Referrer-Policy: strict-origin-when-cross-origin
  • Permissions-Policy – disables camera, mic, geolocation

Best Practices for Users

A few recommendations to keep your data safe:

  1. Export and back up your encryption keys – Store them in a password manager or encrypted storage
  2. Use a strong device password – Your keys are only as secure as your device
  3. Keep your browser updated – Web Crypto security depends on browser security
  4. Clear browser data on shared devices – IndexedDB persists between sessions
  5. Don't share your encryption key via email or chat – Treat it like a password

Reporting Security Issues

If you find a security vulnerability, please don't open a public issue. Instead, email us directly so we can address it before it becomes public knowledge.

We take security reports seriously and will work with you to understand and fix issues quickly.


For the Nerds 🤓

Here's the stuff you actually want to know:

Encryption Parameters

Parameter Value
Algorithm AES-256-GCM
Key length 256 bits
IV length 96 bits (12 bytes), random per encryption
Authentication GCM tag (128 bits)

Key Derivation (PBKDF2)

Parameter Value
Iterations 310,000
Hash function SHA-256
Salt length 128 bits (16 bytes), random per key
Output 256-bit AES-GCM key

The 310k iteration count follows OWASP recommendations for PBKDF2-SHA256. It's offloaded to a Web Worker so your UI doesn't freeze while we crunch those hashes.

Key Wrapping

Master keys and per-resume keys are wrapped (encrypted) before storage:

  1. Generate random 16-byte salt
  2. Derive KEK (Key Encryption Key) via PBKDF2 from device secret + salt
  3. Wrap the data encryption key using AES-GCM with random 12-byte IV
  4. Store wrapped key + salt + IV in IndexedDB

The "device secret" is userId:deviceId where deviceId is a UUID persisted in localStorage. This provides device binding—your keys won't work on another device without export/import.

Share Links

When you generate a shareable resume link:

  1. A new AES-256-GCM key is generated specifically for that resume
  2. Resume content is re-encrypted with this per-resume key
  3. The key is base64-encoded and appended to the URL as a fragment (#key=...)
  4. URL fragments are never sent to the server—browsers strip them from HTTP requests

The recipient's browser extracts the key from the fragment and decrypts locally.

Worker Threads

CPU-intensive operations run in Web Workers to keep the UI responsive:

  • PBKDF2 derivation – 310k iterations can take 1-3 seconds on slower devices
  • Encryption/decryption – handled in worker when processing large resumes

Workers have a 30-second timeout. If a worker fails, we fall back to main thread execution.

Source Files

src/lib/crypto/
├── encryption.ts        # AES-256-GCM encrypt/decrypt
├── key-derivation.ts    # PBKDF2, key wrapping
├── key-storage.ts       # IndexedDB operations
├── encryption-context.tsx  # React context for key state
└── workers/
    └── pbkdf2-manager.ts   # Web Worker management

Other security-related files:

  • src/lib/rate-limit.ts – Upstash Redis rate limiting
  • src/lib/api-middleware.ts – Auth & rate-limit wrappers for API routes
  • next.config.ts – HTTP security headers

There aren’t any published security advisories