3 Lessons I Learned the Hard Way About Web Cryptography
Published on July 22, 2025
You can read all the books you want they won’t prepare you for actually deploying cryptography on the web. The math is elegant. The APIs look safe. The reality? Full of footguns. Timing leaks, bad defaults, silent failures, and APIs that hand you the rope.
These are three hard constraints I’ve internalized from building and hardening a real-world cryptosystem. They aren’t theoretical. They’re operational. If you’re building secure apps, start here.
1. Don’t Choose Primitives. Choose Protocols. (Why X25519 Beats P-256 Every Time)
The textbook says: use ECDH for key exchange. Most developers reach for NIST P-256. It’s the most common choice. It’s also a trap.
Standard ECDH over curves like P-256 is underspecified and dangerous by default:
- Timing side-channels: Scalar multiplication via double-and-add leaks execution time. That’s correlated to private key bits. If your attacker has high-resolution timing, your key is exposed.
- Manual input validation: The protocol doesn’t validate peer public keys. If you forget to do it — and most do — an attacker can feed you crafted keys that leak your private scalar.
The fix isn’t diligence. It’s better design.
Use X25519. It’s not just a curve it’s a full key exchange protocol with safety properties baked in:
- Constant-time execution via Montgomery ladder, no matter what key is used.
- Input-agnostic math: Any 32-byte input is valid. No validation step. No room for injection.
Lesson: crypto primitives give you rope. Protocols give you guardrails. X25519 removes entire categories of bugs. Use it.
2. If You’re Just Encrypting, You’re Doing It Wrong (Use AEAD or Get Burned)
Encryption alone doesn’t protect you. Attackers don’t just listen. They tamper.
AES in CBC mode still shows up in production. It’s insecure:
- No built-in integrity.
- Vulnerable to padding oracle attacks.
- Allows bit-flipping attacks that corrupt data silently.
Modern encryption must be authenticated. Use AEAD Authenticated Encryption with Associated Data.
The default answer: AES-256-GCM. It:
- Encrypts the message (confidentiality).
- Adds an authentication tag (integrity).
- Rejects any tampered message at decryption time.
One cipher, two guarantees. You never want to separate them.
Lesson: if your encryption isn’t authenticated, it’s broken. AEAD isn’t optional. It’s baseline.
3. The Browser Is the Enemy (Non-Exportable Keys Are Your Shield)
The web’s threat model is brutal. If someone gets XSS, they can run arbitrary JavaScript. That includes reading localStorage, dumping memory, and stealing anything your frontend can access.
Most apps store keys like config: as raw bytes, in localStorage, or worse, in-memory globals. That’s dead on arrival.
Here’s how to actually contain the blast radius:
- Generate keys via Web Crypto API with
extractable: false
. That means: JavaScript can use the key, but never see the key. - Store the
CryptoKey
handle in IndexedDB. Not for security, but for capability localStorage only stores strings. CryptoKey is an opaque object, and IndexedDB can persist it.
If an attacker lands XSS, they can’t export the key. They can temporarily use it, but the secret itself never leaves the browser’s cryptographic boundary.
Lesson: store capabilities, not secrets. And let the platform enforce the boundary, not your code.
Final Takeaway
Most crypto failures aren’t due to broken algorithms. They’re due to bad assumptions, unsafe defaults, and environments that make the secure thing harder than the dangerous one.
- Use X25519, not P-256.
- Use AES-GCM, not CBC.
- Use non-exportable keys in IndexedDB, not bytes in memory.
That’s not crypto hygiene. That’s survival.