nimir /
BioWorkContact
Get in touch

© 2026 Nimir Khan. All rights reserved.

Engineering Story

How I Built CryptoCloud

The complete story of building zero-knowledge cloud storage: from initial idea to production deployment, including design decisions, mistakes made, and lessons learned.

15 min read
Solo Project
4 months

The Motivation

I wanted to build something real — not another CRUD app or tutorial project. Something that would:

  • Teach me cryptography: I'd read theory but never implemented it in production
  • Force architectural decisions: Scaling considerations, security tradeoffs, performance bottlenecks
  • Be portfolio-worthy: Something I could actually deploy and show to employers
  • Solve a real problem: I didn't trust Google Drive or Dropbox with sensitive files

Zero-knowledge encryption felt like the perfect challenge: complex enough to be interesting, practical enough to be useful, and niche enough that I couldn't just copy-paste from tutorials.

Initial Design Goals

✅ Must Have

  • • Client-side encryption (zero-knowledge)
  • • File upload/download with progress bars
  • • Folder organization
  • • Secure file sharing between users
  • • Public link sharing with optional password
  • • User authentication (JWT)

⚪ Nice to Have

  • • File versioning
  • • Real-time sync across devices
  • • Mobile app
  • • Full-text search (encrypted)
  • • Collaborative editing
  • • End-to-end encrypted chat

In hindsight, I was way too ambitious. I cut 90% of the "nice to have" features after realizing basic file encryption was harder than I thought.

Tech Stack Decisions

Frontend: Next.js + TypeScript

Why: Needed React for complex UI state (upload queues, encryption progress). TypeScript for type safety with crypto operations.

Alternative considered: Vanilla JS + Web Components (too much boilerplate)

Backend: FastAPI (Python)

Why: Fast to prototype, async/await for concurrent uploads, Pydantic for validation, strong typing.

Alternative considered: Node.js/Express (chose Python for better cryptography libraries)

Database: MongoDB

Why: Flexible schema for file metadata, easy nested documents for sharing, fast queries with indexes.

Alternative considered: PostgreSQL (stuck with NoSQL for schema flexibility during prototyping)

Storage: AWS S3

Why: Scalable, cheap, pre-signed URLs let clients upload directly (bypass backend bottleneck).

Alternative considered: Filesystem storage (doesn't scale, hard to manage)

Task Queue: Celery + Redis

Why: Background jobs for cleanup (expired shares, deleted files), email notifications.

Alternative considered: Cron jobs (Celery more flexible for scheduled + triggered tasks)

Development Journey

Phase 1: Learning Web Crypto API (Week 1-2)

Spent two weeks just understanding crypto.subtle. The documentation is sparse, and debugging encrypted data is impossible (it's just random bytes).

Key insight: Start with test vectors from NIST/RFC specs to verify implementations are correct.

Phase 2: Building File Encryption (Week 3-4)

Implemented AES-256-GCM encryption for files. Hit immediate problem: how to handle large files (100MB+) without consuming all memory?

Solution: Chunk files into 64KB blocks, encrypt each block separately, stream to S3. Added progress bars.

Phase 3: Authentication & Key Management (Week 5-6)

The hardest part: deriving master key from password, generating RSA keys, wrapping/unwrapping keys. Made critical mistake here (static PBKDF2 salt).

Mistake: Used same salt for all users. Only realized this was wrong after reading security audit of similar projects.

Phase 4: File Sharing (Week 7-10)

Implementing secure sharing was conceptually simple (wrap file key with recipient's RSA public key) but debugging edge cases took forever.

Challenge: Handling permissions (owner vs shared user), revocation, public links with passwords.

Phase 5: Deployment & Production (Week 11-16)

Dockerized backend, set up CI/CD, configured HTTPS, added rate limiting, implemented cleanup workers. Real-world testing revealed bugs.

Surprise: Mobile browsers handle large file encryption differently. Had to add device-specific chunking logic.

Biggest Mistakes (What I'd Redesign)

❌ Static PBKDF2 Salt

Mistake: Used hardcoded salt for all users. Two users with same password derive same master key.

Impact: Enables rainbow table attacks. If attacker pre-computes hashes for common passwords, they can crack multiple accounts.

Fix: Generate random salt per user during registration, store in DB, send to client before login for key derivation.

❌ No JWT Revocation

Mistake: JWT tokens valid until expiration. No logout or revocation mechanism.

Impact: Stolen token remains valid for 7 days. User can't force logout from compromised session.

Fix: Implement token blacklist in Redis. Short-lived access tokens (15min) + refresh tokens stored in database.

⚠️ Plaintext File Metadata

Decision: Store filenames, sizes, folder structure in plaintext for server-side features.

Tradeoff: Enables search, sorting, quota management BUT server sees all metadata.

Redesign: Encrypt metadata client-side. Accept that search/sort must happen on client. Use bloom filters for server-side search hints.

⚠️ No Key Rotation

Missing: No way to rotate RSA keys or change password without losing access to files.

Impact: If RSA key is compromised (e.g., device stolen), can't revoke it. All shared files remain vulnerable.

Redesign: Support key versioning. Re-wrap all file keys with new RSA key. Allow background re-encryption jobs.

⚠️ Chunked Encryption Complexity

Overengineered: Implemented streaming encryption with 64KB chunks. Added tons of complexity.

Reality: Modern browsers handle 100MB+ in memory fine. Premature optimization.

Lesson: Start simple, optimize when you measure actual bottlenecks. Chunking added 2 weeks of debugging for marginal benefit.

What Went Well

✅ Envelope Encryption Pattern

Separating DEK (file key) from KEK (master/RSA key) made sharing elegant. Can re-wrap same DEK for multiple recipients without re-encrypting file.

✅ S3 Pre-Signed URLs

Client uploads directly to S3 (bypasses backend). Backend stays stateless, scales horizontally. Best architecture decision.

✅ TypeScript Everywhere

Type safety caught so many crypto bugs (wrong key type, missing IV, incorrect buffer encoding). Would've been nightmare in vanilla JS.

✅ Docker + CI/CD Early

Set up Docker Compose and GitHub Actions from day one. Made deployments painless, caught bugs in staging before production.

✅ Comprehensive Error Handling

Crypto operations fail silently (wrong key = random bytes). Added explicit error handling + user-friendly messages at every step.

Lessons Learned

1. Cryptography is Harder Than You Think

Implementing crypto primitives is easy. Getting the system design right is hard. Security is about the whole picture: key management, authentication, session handling, threat modeling, not just AES vs RSA.

2. Performance Matters (But Measure First)

Spent weeks optimizing chunked encryption. Real bottleneck was network speed, not crypto. Always profile before optimizing. Most of my "performance improvements" were premature.

3. Read Real Implementations

Academic papers teach theory. Open-source projects (Bitwarden, Keybase, Signal) teach practical engineering. I learned more from reading their GitHub repos than any textbook.

4. Security is About Tradeoffs

Every decision is a tradeoff. Plaintext metadata = better UX but less privacy. Long JWT expiration = better performance but bigger attack surface. Document your tradeoffs so you (and others) understand why choices were made.

5. Zero-Knowledge = Zero Recovery

Forgot password = lost data. Period. Had to add scary warnings everywhere. Users expect "reset password" to work. User education is as important as technical implementation for E2EE.

6. Write Documentation As You Go

Came back to code after 2 weeks, couldn't remember why I made certain decisions. Started writing architecture decision records (ADRs) — saved hours of reverse-engineering my own code.

If I Started Over Tomorrow...

1️⃣ Build Native Desktop App First

Web apps have inherent limitations (frontend trust problem, no secure memory). Electron or Tauri would solve many security issues and give better UX.

2️⃣ Use Proven Crypto Libraries

Instead of raw Web Crypto API, use libsodium.js or TweetNaCl. They abstract painful details (key formats, nonce management) and are battle-tested.

3️⃣ Implement Metadata Encryption

Accept the UX cost. Encrypt filenames, folder structure, timestamps. Do search and sorting client-side. True zero-knowledge requires this.

4️⃣ Add Audit Logs From Day One

Immutable append-only logs for every action (login, upload, share). Critical for security investigations and debugging production issues.

5️⃣ Design for Key Rotation

Support rotating RSA keys and master keys without losing data. Build versioning into the protocol from the start. Retrofitting is painful.

6️⃣ Get Security Audit Early

Even informal peer review would've caught the static salt issue. Don't wait until "v1.0" — get feedback on core crypto design before building features.

Final Thoughts

Building CryptoCloud taught me more about real software engineering than any course or tutorial ever did.

The mistakes I made? They're not failures — they're documented lessons that I can share with others and avoid in future projects.

The code isn't perfect. The architecture has flaws. But it works, it's deployed, and I can explain every decision (good or bad) in technical interviews.

That's worth more than 100 perfect tutorial projects.

Related Blog Posts

What is Zero-Knowledge? →

Introduction to zero-knowledge encryption for beginners

Client vs Server Encryption →

Understanding the critical difference in security models

Password Reset Challenge →

Why "Forgot Password?" breaks zero-knowledge guarantees

Want Technical Details?

Architecture

System design, MongoDB schemas, API architecture

Security Model

Threat analysis, attack vectors, security guarantees

Cryptography

AES-GCM, RSA-OAEP, PBKDF2 implementation details