DEV Community

Cover image for How I built a zero-knowledge encrypted cloud storage app: the complete crypto architecture
Rajath R
Rajath R

Posted on

How I built a zero-knowledge encrypted cloud storage app: the complete crypto architecture

For the past two years I've been building Silvora - a zero-knowledge
encrypted cloud storage app - entirely solo, alongside a full-time job
as a programming instructor.

This post is about the cryptographic architecture. Not the marketing
version. The actual decisions, why I made them, and what they mean for
the security guarantees.

The core guarantee

The server stores only ciphertext. It has zero decryption code.
Mathematically, even if someone compromised the server completely,
they could not read a single user's file.

Here is how that guarantee is actually implemented.

The key hierarchy

Everything starts with your password. But your password never travels
to the server - not even as a hash.

Instead, on your device, Argon2id derives your master key:

  • Memory: 64MB
  • Iterations: 3
  • Parallelism: 2

These parameters are above OWASP minimums. The server enforces floors
server-side - if a client tries to submit with weaker parameters,
the request is rejected.

The master key lives in memory only. Never touches disk. Never touches
logs. When you lock the vault or background the app, it is zeroed with
fillRange(0) before being released.

Per-file key derivation

One master key encrypting everything would be a design failure. If the
master key was ever exposed, every file would be compromised simultaneously.

Instead, each file gets its own key derived via HKDF-SHA256:
per_file_key = HKDF(
master_key,
salt=file_id,
info="silvora-file-encryption-v1",
length=32
)
The info parameter is domain separation - it ensures a key derived
for file encryption cannot be used for any other purpose, even with
the same inputs.

Per-chunk keys are derived the same way with their own domain separator.

Encryption

XChaCha20-Poly1305 AEAD encrypts every chunk.

Why XChaCha20 over AES?

  • 192-bit nonce - nonce reuse is catastrophically unlikely even at scale
  • No hardware acceleration dependency - constant-time on all devices
  • No padding oracle attacks (stream cipher, not block cipher)
  • Poly1305 provides authentication - tampering is detectable

Every encryption call uses a fresh random nonce. Nonce reuse is
checked server-side as an additional layer.

Recovery without server knowledge

If you forget your password, a traditional system resets it server-side
because the server knows something.

Silvora can't do that. The server knows nothing.

Instead, during registration, your device generates a BIP39 24-word
recovery phrase (256 bits of entropy). The master key is encrypted
under a key derived from this phrase and stored - still encrypted -
on the server.

Recovery works entirely client-side. The server hands back the
encrypted blob. Your phrase decrypts it locally. The server never
sees the phrase or the master key.

During onboarding, you must correctly identify 3 random words from
your phrase before continuing. You cannot skip this. For a
zero-knowledge app, a user who loses their phrase loses their data
permanently. That consequence deserved a real confirmation step.

Integrity verification

Each file upload generates a manifest:

  • SHA-256 hash of every plaintext chunk
  • Total chunk count (detects truncation)
  • The manifest itself encrypted with XChaCha20-Poly1305 under an HKDF-derived per-file integrity key

On every download, the manifest is fetched, decrypted, and every
chunk hash is verified before the file is returned to the user.

The server cannot tamper with file contents without detection. It
cannot reorder chunks. It cannot truncate files silently.

What the server has

The server stores:

  • Encrypted file chunks (ciphertext)
  • Encrypted filenames (ciphertext)
  • Encrypted integrity manifests (ciphertext)
  • Argon2id parameters (public - needed for key derivation)
  • File metadata: size, upload timestamp, chunk count

That is all it has ever had.

The backend

Django REST Framework on Render, PostgreSQL on Neon, file storage
on Cloudflare R2.

Key security decisions:

  • Every file endpoint scoped by owner AND tenant - double isolation
  • Rate limiting on all auth and file endpoints
  • SELECT FOR UPDATE on upload commits - idempotent, race-condition safe
  • Quota reserved at upload start, not commit - parallel uploads cannot blow past limits
  • Soft-deleted files cannot accept new chunk writes
  • No raw SQL anywhere - full ORM

103 tests passing across backend and Flutter client.

The Flutter client

On-device Argon2 key derivation. Encrypted chunk streaming. JWT
stored in FlutterSecureStorage - never in SharedPreferences.

Additional hardening:

  • Root detection blocks vault access on rooted devices
  • Developer mode / USB debugging gate - vault inaccessible while debugging active
  • FLAG_SECURE - blocks screenshots, screen recording, and recent-apps thumbnail on release builds
  • Clipboard auto-clear - recovery phrase wiped from clipboard after 60 seconds
  • 1-minute auto-lock on background

Current status

Silvora is in Google Play closed testing. I'm looking for Android
users who care about privacy to test it.

If you want early access: leave a comment or email me at
rajathramesh2002@gmail.com with your Gmail address and I'll
add you as a tester.

If you want to understand the full architecture in more depth -
I've been writing a technical book using Silvora as the running
example, covering the cryptography from first principles.
Happy to share chapters with anyone interested.

Live at: silvora.cloud

Ask me anything about the implementation.

Top comments (0)