Between dropping a file onto the page and seeing “upload complete,” a fair amount happens that you never see. This post follows a single file through the whole pipeline — key derivation, segmentation, placement, transfer — and points out where each design decision was made to keep plaintext away from us.
Step 0: unlocking your keys
Nothing can be encrypted until your keys exist in memory, and they only exist there after you unlock your seed vault.
The vault is a small encrypted bundle holding your Ed25519 signing seed — from which your X25519 key-agreement key is derived (how a shared secret gets established when you share a file with someone) — and an ML-KEM-768 key sitting alongside it as a post-quantum hedge. The vault itself is sealed with a key derived from your password's SHA-512 digest using Argon2id — a memory-hard function, chosen specifically because it makes offline guessing expensive in RAM as well as CPU time.
The important property: this derivation runs in your browser. The server stores the sealed vault as an opaque blob and has no way to open it. Log in on a new device and you download the same blob and unseal it locally.
The password you type never leaves your machine — the browser sends only its SHA-512 digest. That digest does two jobs: it is your login credential, and combined with a random per-account salt it is the Argon2id input that opens your vault. The vault key itself is derived in your browser and never transmitted, and at rest the server keeps only a one-way Argon2id hash of the digest — so nothing stored on our side can reproduce the key that opens your vault.
Step 1: a fresh key per transfer
Each transfer gets its own randomly generated content key, covering the files in it and their metadata. It is never derived from anything else, and it is never your account key.
That indirection is what makes sharing possible without re-uploading. To share, your browser wraps that one content key to the recipient's public keys — a hybrid of X25519 and post-quantum ML-KEM-768, so the wrap is not undone even by a future quantum computer. The ciphertext on disk is untouched. Revoking a share removes their wrapped copy of the key; the files themselves never move.
Step 2: segmentation and encryption
The file is read in 8 MiB segments and each segment is sealed independently with XSalsa20-Poly1305 (libsodium's secretbox), which gives both confidentiality and authentication — a modified byte doesn't decrypt to garbage, it fails to authenticate and is rejected outright.
Segmenting rather than encrypting one long stream buys three things:
- Bounded memory. A 40 GB file never needs 40 GB of RAM. The browser works one segment at a time.
- Resumable transfers. A dropped connection costs you the current segment, not the upload.
- Range reads. Seeking into the middle of a large file only requires fetching and decrypting the segments that cover that range.
Each segment travels with its own fresh random nonce, and its offset in the file is recorded as part of the upload, so the file reassembles in order on the way back down.
Filenames are content too
The name of a file is often as revealing as its contents —
redundancy-plan-q3.xlsx tells a story on its own. So filenames and metadata are
encrypted with the same content key before they are sent. Your file list is rendered by
decrypting those names in the browser. Server-side, they are opaque blobs we can neither
index nor search.
Step 3: asking where to put it
Only now does the network get involved, and what goes to the API is a request for permission, not for storage.
The API checks your quota and bucket permissions, then asks the placement service to pick storage nodes for each chunk. What comes back is a placement plan along with a set of Ed25519-signed operation tokens — short-lived, single-purpose credentials that say “the holder may write this specific chunk to this specific node until this timestamp.”
This is the architectural pivot of the whole system. The API's job is to decide, not to carry. It has issued a signed decision and stepped out of the data path.
Step 4: straight to the node
Your browser now uploads each encrypted chunk directly to the assigned storage node with a
plain PUT, presenting the signed token. The node verifies the signature
against the placement service's public key, writes the blob to disk, records it in a local
index, and replies.
The node has no database connection to the API and no key material of any kind. Next to each blob it keeps a small plaintext recovery record — owner, bucket and object identifiers — so chunks can be grouped, replicated and recovered without asking anyone. What it can never do is look inside them: to a node, your data is ciphertext with a routing label.
| Component | What it handles | Sees plaintext? |
|---|---|---|
| Your browser / SDK | Keys, encryption, chunking | Yes — the only place |
| Control-plane API | Accounts, quota, permissions, signed plans | No |
| Placement service | Node selection, token signing | No — never touches bytes |
| Storage node | Writing and serving encrypted chunks | No |
Step 5: durability, without decryption
Once a chunk lands, the receiving node replicates it to peers so a single disk or machine failure doesn't lose data. Replication is a byte-for-byte copy — nothing is unwrapped or re-encrypted in transit between nodes, because no node holds a key that could do so.
Redundancy is therefore a purely mechanical concern. That is a nice property to have: the people running the storage layer can reason about disks, checksums and failure domains without any of it being a privacy question.
Download is the same thing backwards
- Ask the API where the chunks are; receive signed read tokens.
- Fetch chunks directly from the nodes that hold them.
- Unwrap the content key locally — from your vault, or from the share addressed to your public key.
- Decrypt and authenticate each segment; any that fails its Poly1305 tag is refused.
- Reassemble in the browser and hand the file to the download stream.
One implementation, two languages
The browser code and the C# SDK implement the same formats, and they have to agree byte-for-byte forever — a subtle divergence in nonce derivation or segment framing would mean vaults that no longer open. So the SDK crypto is a deliberate port of the browser's, and a parity test suite encrypts with one and decrypts with the other, in both directions, across every format.
Two independent implementations that must produce identical bytes is a useful thing to have, for the same reason double-entry bookkeeping is: a mistake in one shows up as a mismatch rather than as silent corruption.
Encryption handles confidentiality. It says nothing about whether the record of who accessed what is honest — that needs a different mechanism, which is the subject of the next post.