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.
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:
- When you create or edit a resume, your browser encrypts it with a key only you control
- The encrypted data is sent to our servers for storage
- 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.
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.
| 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.
Beyond client-side encryption, we have standard infrastructure protections in place:
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.
We set the usual security headers via next.config.ts:
Content-Security-Policy– restricts what can run on the pageStrict-Transport-Security– forces HTTPSX-Content-Type-Options: nosniffX-Frame-Options: SAMEORIGINReferrer-Policy: strict-origin-when-cross-originPermissions-Policy– disables camera, mic, geolocation
A few recommendations to keep your data safe:
- Export and back up your encryption keys – Store them in a password manager or encrypted storage
- Use a strong device password – Your keys are only as secure as your device
- Keep your browser updated – Web Crypto security depends on browser security
- Clear browser data on shared devices – IndexedDB persists between sessions
- Don't share your encryption key via email or chat – Treat it like a password
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.
Here's the stuff you actually want to know:
| Parameter | Value |
|---|---|
| Algorithm | AES-256-GCM |
| Key length | 256 bits |
| IV length | 96 bits (12 bytes), random per encryption |
| Authentication | GCM tag (128 bits) |
| 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.
Master keys and per-resume keys are wrapped (encrypted) before storage:
- Generate random 16-byte salt
- Derive KEK (Key Encryption Key) via PBKDF2 from device secret + salt
- Wrap the data encryption key using AES-GCM with random 12-byte IV
- 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.
When you generate a shareable resume link:
- A new AES-256-GCM key is generated specifically for that resume
- Resume content is re-encrypted with this per-resume key
- The key is base64-encoded and appended to the URL as a fragment (
#key=...) - 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.
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.
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 limitingsrc/lib/api-middleware.ts– Auth & rate-limit wrappers for API routesnext.config.ts– HTTP security headers