Most "encrypted" file sharing means encrypted in transit and at rest, which is a polite way of saying the server holds the keys and can read everything whenever it likes. I wanted the other kind: the server stores my files and genuinely cannot open them. Here's how that works in share·me, including the parts the tutorials quietly skip.
The whole trick is the URL fragment
The browser generates a random key, encrypts the file client-side with AES-256-GCM, and the key goes here:
https://share.example/d/AbC123#k=<base64-key>
Everything after the # never leaves the browser. It's not in the request line, not in the server logs, not in the access logs of whatever proxy you stuck in front. The link carries the key; the server only ever sees ciphertext, filenames included. That's the entire game, and it's almost embarrassingly simple.
Don't want the key in the link at all? Password mode derives it with Argon2id (PBKDF2 fallback). A wrong password just fails server-side authorization, which is rate-limited, so there's no offline brute-force surface to attack.
The part tutorials skip: you can't buffer a 5 GB file in RAM
Every "encrypt a file with WebCrypto" tutorial calls encrypt(wholeFile). Try that with a 5 GB upload and you OOM the tab. Real files have to stream.
So the crypto package uses a segmented AES-256-GCM STREAM construction: the file is split into chunks, each chunk encrypted under an HKDF-derived sub-key with a sequence nonce. Two things that bit me and are worth calling out:
- Per-chunk nonces have to be deterministic and ordered, or you can't decrypt a stream you didn't buffer. Sequence numbers, not random nonces.
- AES-GCM is not key-committing. A ciphertext can be crafted to decrypt cleanly under two different keys. For a sharing service that's a real footgun, so there's a key-committing header that binds the ciphertext to exactly one key.
Browser ↔ API streams directly; the Rust service never holds a whole file in memory.
Architecture
Browser (client-side AES-256-GCM)
│
▼
Traefik ──/api/*──► Rust / axum API ──► blobs (disk or S3) + metadata (SQLite/Postgres)
└─────/*──────► Next.js BFF
One origin via Traefik, so no CORS. A thin Next.js BFF (Server Actions) keeps each drop's owner token in an httpOnly cookie; big blobs bypass it and stream straight to the API. Expiry, download limits, burn-after-reading and time-lock are all enforced server-side, so the client can't lie its way past them.
Running it
git clone https://github.com/onokashino/share-me.git && cd share-me
docker compose up --build # http://localhost
Set DOMAIN + ACME_EMAIL and Traefik fetches a Let's Encrypt cert itself. Prebuilt images are on ghcr.io. ~1 vCPU / 1 GB RAM runs it.
One honest caveat: there's no third-party audit yet. The crypto is deliberately small and dependency-light so it's actually readable in one sitting, and I'd love eyes on it. It's AGPL-3.0.
If you spot something wrong in the threat model, I want to hear it.
Top comments (0)