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.
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.