Deep Dive into Xdrop: An End-to-End Encrypted File Transfer System for Humans and Agents
File transfer is one of those categories that looks solved until you ask harder questions. The hard part is not letting users click a button and upload a file. The hard part is preserving a usable experience while refusing to treat the server as a trusted holder of plaintext, especially when the receiver may be either a human in a browser or an agent running on a remote machine, inside a container, or in an automated workflow.
Xdrop is positioned very clearly. It is not a generic cloud drive replacement, and it is not only a browser-based encrypted share page. Its scope is much sharper: an open source end-to-end encrypted file transfer system for humans and agents, with one core promise that is both restrained and meaningful: plaintext file names, plaintext contents, and decryption keys stay off the server.
This article analyzes its main design choices and engineering tradeoffs.
1. The real problem Xdrop solves: trustworthy handoffs, not generic uploads
Most file-sharing systems implicitly assume that the server both receives the file and understands the file. It knows the filename, the directory structure, and often the content as well. The differences usually come down to whether the product has accounts, expiration, or share links.
Xdrop takes a different route. It splits the system into three boundaries.
1.1 The sender understands plaintext; the server coordinates lifecycle
In Xdrop, the sender browser or local agent runtime is the only place that fully understands the file. Names, paths, MIME types, timestamps, and chunk metadata enter a browser-side encryption flow first, then move to the backend as ciphertext or opaque identifiers.
The backend is intentionally reduced to a narrower set of jobs:
- create transfer records and return a manage token plus upload limits
- store file and chunk progress metadata
- issue time-limited upload and download URLs for object storage
- enforce expiry, deletion, rate limits, and baseline access controls
That makes it a coordination plane, not a plaintext data plane.
1.2 The audience is not only human browser users, but also agents
Xdrop consistently defines itself as being “for humans and agents,” and that is not just positioning language. It is a real protocol constraint. Browser sharing and agent-driven terminal uploads are not parallel systems. They use the same transfer format, the same share-link structure, and the same decryption model.
That matters because many products split “web upload” and “automation handoff” into separate feature lines, which tends to create awkward outcomes:
- the browser flow is polished, but automation falls back to plaintext upload
- the CLI works, but its links are incompatible with the browser flow
- the crypto story differs by entry point, raising audit and maintenance cost
Xdrop instead uses one protocol. The same /t/:transferId#k=... link can be opened by a human in a browser or used by an agent for local decryption.
1.3 This is not “zero metadata,” but “the server does not hold the important plaintext”
This nuance is important. Xdrop does not pretend the server sees nothing. In this architecture, the server still sees operational metadata, such as:
- transfer creation and expiry timestamps
- file counts, chunk counts, and ciphertext sizes
- rate-limit identifiers
- which chunks have completed upload
What it deliberately keeps off the server are the most semantically revealing parts of the transfer: filenames, paths, file contents, and decryption keys. That is a more accurate claim, and a more credible one.
2. The cryptographic model: turning the share link into a capability token
The cryptographic design in Xdrop is not flashy. It is disciplined. Instead of inventing anything novel, it composes a few mature primitives through the browser’s built-in WebCrypto APIs.
2.1 Two-key design: a transfer root key and a link key
When a sender starts a transfer, the client generates two 32-byte random secrets:
rootKey: the transfer root key that actually protects the manifest and file payloadslinkKey: the share-link key that wraps therootKeyand is placed in#k=...inside the final link
The final link looks like this in principle:
/t/<transferId>#k=<base64url-link-key>
The server stores a wrappedRootKey, not the rootKey itself. When the receiver opens the link, the browser extracts the linkKey from the fragment, unwraps the rootKey locally, and only then decrypts the manifest and file data.
This structure yields two direct benefits:
- the transfer ID can remain in the URL path where the server can route on it
- the actual decryption capability stays in the fragment and does not travel as part of normal HTTP requests
The key point is simple: the link fragment stays in the browser and does not travel with normal HTTP requests.
2.2 HKDF scopes keys by purpose, and AES-256-GCM provides confidentiality and integrity
Xdrop uses HKDF-SHA-256 to derive purpose-scoped AES-256-GCM keys:
linkKeywithinfo = "wrap-root"derives the key that wraps the root keyrootKeywithinfo = "manifest"derives the manifest encryption keyrootKeywithinfo = "file:<fileId>"derives a separate key for each file
This “same source secret, different derived contexts” approach is safer than reusing a single AES key across everything. It isolates cryptographic roles and leaves room for future evolution without entangling unrelated formats.
2.3 Chunk encryption binds context, not just bytes
Xdrop uses AES-GCM for file chunks, but it does more than encrypt slices of data. In encryptChunk(), it binds the following values into authenticated data:
transferIdfileIdchunkIndexplaintextChunkSize- protocol version
That means a ciphertext chunk cannot be replayed across transfers, files, or positions without failing authentication.
The IV strategy is also worth noting. Each 12-byte GCM IV is built from:
- an 8-byte per-file
noncePrefix - a 4-byte
chunkIndex
This combines file-level randomness with deterministic chunk positioning. It reduces state complexity while still letting the receiver reconstruct decryption parameters exactly.
2.4 Metadata stripping is an extra privacy layer, not the core cryptographic story
Before encryption, the frontend optionally strips removable metadata from supported image types such as JPEG, PNG, and WebP by redrawing them and re-exporting the pixels.
This solves a different privacy problem. Even if a file is later encrypted, an untouched photo may still contain EXIF data with location or device information. Xdrop deals with that by stripping metadata before the encrypted transfer is prepared.
3. The upload pipeline: how the browser chunks, encrypts, and resumes transfers
One of the strongest parts of Xdrop is not merely that it performs end-to-end encryption. It is that it turns the browser into a recoverable upload runtime rather than a one-shot upload form.
3.1 It prepares source files and estimates ciphertext size before upload begins
During upload preparation, the client does several things for each file:
- generates an opaque
fileId - creates a per-file
noncePrefix - computes
totalChunksfrom the configured chunk size - predicts each ciphertext chunk size as
plaintext + 16bytes for the GCM tag - sums the total encrypted transfer size in advance
That lets Xdrop enforce limits before it commits to the transfer. The current defaults are shared between the frontend and backend:
- chunk size:
8 MiB - maximum file count:
100 - maximum encrypted transfer size:
256 MiB
These values are conservative, but they fit the real design target: browser-side encryption with local persistence and resume support. Xdrop is not currently pretending to be a multi-terabyte archival pipeline. It is optimizing for a trustworthy encrypted handoff that remains understandable and robust in actual browser environments.
3.2 OPFS first, IndexedDB as fallback, so refreshes do not destroy the transfer
If a browser only uploads from the live File handle, a page refresh means starting over. Xdrop treats local source persistence as a first-class part of the runtime.
It prefers OPFS (Origin Private File System) for staged source files. If the browser does not support OPFS, or if fallback is needed, it can store Blob-backed file data in IndexedDB within a configured limit.
That produces a very practical recovery flow:
- when the page is refreshed, in-flight uploads are marked as
paused - when the page is reopened, the client reloads local transfer and source records
- the client calls
/resumeto ask which chunks already exist - only missing chunks are uploaded again
That also shows that recovery is not an optional extra. It is part of the upload model itself.
3.3 Web Workers handle the heavy crypto while the main thread orchestrates
Xdrop pushes expensive operations into a dedicated worker:
- wrapping and unwrapping the root key
- encrypting and decrypting the manifest
- encrypting and decrypting file chunks
That keeps React rendering and large cryptographic operations from fighting over the main thread. For users, the immediate benefit is a UI that remains responsive during large transfers. Architecturally, it also cleanly separates “cryptographic execution” from “application state.”
3.4 Upload URLs are presigned in batches rather than all at once
Xdrop does not presign PUT URLs for the entire transfer in a single shot. The frontend asks /upload-urls for chunks in batches, then uploads those chunks in parallel and reports them back through /chunks/complete with ciphertext size and SHA-256 checksum metadata.
That design has several advantages:
- presigned URLs have a smaller exposure window
- retries affect only a batch rather than the full transfer
- the server can re-check state at each batch boundary
Once all chunks are acknowledged, the client:
- builds the manifest
- encrypts it with the
rootKey - wraps the
rootKeywith thelinkKey - uploads the manifest
- calls
/finalizeto move the transfer intoready
In other words, a transfer only becomes downloadable at the very end. Until that point, the public API treats it as incomplete.
4. The backend is intentionally narrow: it stores state, not meaning
The backend is written in Go, but the most important choice is not the language. It is the responsibility boundary. The server behaves much more like a transfer lifecycle coordinator than a traditional file server.
4.1 The API surface is centered on transfer lifecycle
The server-side API surface is intentionally tight:
- create transfer
- register files
- create upload URLs
- complete chunks
- upload manifest
- finalize transfer
- resume, manage, and delete
- public read of transfer status
- public creation of download URLs
There is no endpoint that means “upload plaintext file” or “download plaintext file.” The server only knows that a transfer exists, that it contains opaque file IDs, that chunk indexes map to object keys, and that certain lifecycle steps have completed.
4.2 PostgreSQL stores a state graph, not file semantics
The database has three core tables:
transferstransfer_filestransfer_chunks
The important fields include:
wrapped_root_keymanifest_object_keymanage_token_hashopaque_file_idchunk_indexchecksum_sha256
Two implementation details matter a lot:
- manage tokens are not stored in plaintext; the server stores a SHA-256 digest
- file IDs are opaque client-generated identifiers, not filenames
That is the core backend design philosophy in Xdrop. The database needs to express transfer progress, but it does not need to know that a file is called contract.pdf.
4.3 The manage token is a sender-side control capability issued once
When a transfer is created, the API returns a manageToken. It is only revealed once in plaintext to the sender. From that point on, the server stores only its hash and validates it with constant-time comparison.
That token gives the sender a well-defined control surface:
- resume an upload
- extend expiry
- delete a transfer
If the sender enables privacy mode, local control state is scrubbed after upload completes. The share link can still work, but local extend and delete controls disappear. This is an important nuance: it is not about making the cryptography stronger. It is about reducing residual sensitive state on the sender device.
4.4 Object storage is the ciphertext destination; the API mostly presigns
The storage layer is structured very clearly:
- the API talks to a private S3-compatible endpoint
- a different public endpoint can be used for externally visible presigned URLs
- manifest and chunk payloads are just objects
- cleanup operates by deleting the
transfers/<id>/prefix
The object layout is consistent:
transfers/<transferId>/manifest.bin
transfers/<transferId>/files/<fileId>/chunks/<chunkIndex>.bin
That means the backend is organized around object layout and lifecycle, not around interpreting file contents. For MinIO, S3, or any compatible storage system, Xdrop simply treats them as ciphertext chunk storage.
4.5 Security headers, rate limits, and cleanup jobs form the baseline defense
Xdrop does not act as though end-to-end encryption alone is the whole security model. The backend also adds operational defenses:
- Redis-backed fixed-window rate limiting
- origin allowlisting via
ALLOWED_ORIGINS - baseline response hardening with headers such as
X-Content-Type-Options,X-Frame-Options, andCross-Origin-Opener-Policy - background cleanup jobs that purge expired or deleted transfer prefixes and then mark them as purged in the database
These mechanisms are not the cryptographic core, but they determine whether the service can be exposed safely and run reliably.
5. Downloads and agent integration: one protocol for humans and automation
If the upload side shows how Xdrop implements browser-side encryption, the download side shows how seriously it takes protocol unification across user types.
5.1 Receivers fetch state first, then the manifest, and only then decrypt locally
When a receiver opens /t/:transferId#k=..., the browser first calls the public descriptor endpoint. Only when the transfer is ready does the server return:
wrappedRootKeymanifestUrl- limited download configuration
The receiver then:
- uses the fragment
linkKeyto unwrap thewrappedRootKey - fetches the encrypted manifest
- decrypts the manifest locally into a file list
- requests download URLs for the listed chunks
That means the public API only exposes ciphertext-oriented descriptors that are safe to expose. It never returns a plaintext file list or a usable decryption secret as ordinary JSON.
5.2 ZIP downloads are assembled locally, and folder structure comes from the manifest
For folder downloads, Xdrop does not ask the server to assemble a ZIP archive on demand. Instead, the browser decrypts files as streams and uses zip.js to build the ZIP locally.
That does more than remove backend complexity. It preserves a larger principle: the server delivers encrypted objects, not reconstructed plaintext artifacts. Directory paths come from the decrypted manifest, and ZIP structure is rebuilt on the client.
The server never needs to know that a certain set of chunks belongs to docs/readme.txt. It only needs to make the corresponding encrypted chunks retrievable.
5.3 Agent workflows reuse the exact same share format
One of the strongest product decisions in Xdrop is that agent support is not implemented as a second, separate API line. The companion Xdrop skill and command-line workflow revolve around the same capabilities:
- upload a local file or directory
- return the full encrypted share link
- use a full link with
#k=for local decryption download
That makes Xdrop a useful end-to-end encrypted handoff channel for remote servers, containers, and CI-adjacent environments. An agent can send logs, artifacts, diagnostic bundles, or outputs back to a local user without forcing the server to act as a holder of plaintext file semantics.
That is what “for humans and agents” means at the architecture level, not just at the homepage level.
6. Engineering tradeoffs: why Xdrop fits trustworthy temporary handoffs better than a general cloud drive
Xdrop is thoughtfully designed, but it does not try to become an everything-platform. Many of its limits are there to keep the boundaries clear.
6.1 The current defaults reveal the intended problem space
The default 8 MiB chunk size, 100 file cap, and 256 MiB encrypted transfer limit show what Xdrop is optimizing for:
- temporary sharing
- human-to-human and agent-to-human handoffs
- browser-friendly local persistence and recovery
If you need terabyte-scale archival storage, layered permissions, team workspaces, or deep audit features, that is not the core Xdrop problem domain today.
6.2 “No account” and “local history only” are simplifications, but also boundaries
Xdrop very clearly chooses:
- no account system
- no cross-device history synchronization
- sender-side management records only in the current browser
That makes the system lighter and narrows the trust surface. The tradeoff is that if you lose local state, or if privacy mode scrubs local controls after upload, manageability decreases. That is not an accident. It is part of the product’s commitment to minimal server state and short-lived trusted context.
6.3 The deployment model favors self-hosting with explicit boundaries
In the default Docker deployment, the final container combines:
- nginx for the static frontend
- the Go API
alongside:
- PostgreSQL
- Redis
- MinIO
A safer deployment pattern is:
- exposing only a reverse proxy to the public Internet
- binding the
xdropcontainer and MinIO privately when possible - rebuilding the frontend image if you want production-correct
VITE_SITE_URLand canonical metadata
This is not a flashy cloud-native demo, but it is a very practical shape for personal, small-team, and self-hosted deployments. The topology is simple, the boundaries are easy to reason about, and the operational story matches the trust model.
Closing Thoughts
What makes Xdrop worth studying is not that it uses the phrase “end-to-end encrypted.” It is that the system actually carries that promise through the implementation:
#k=fragments keep decryption capability client-side- HKDF plus AES-256-GCM create a clean, scoped cryptographic model
- Web Workers, OPFS, and IndexedDB turn the browser into a resumable transfer runtime
- a Go API, PostgreSQL, Redis, and S3-compatible storage form a backend that manages lifecycle rather than plaintext
- one share protocol serves both human browser flows and agent-driven terminal workflows
Xdrop is not trying to be a universal file platform. But for the problem of trustworthy, temporary, encrypted handoffs across human and machine boundaries, it already shows strong product judgment and solid engineering discipline. In an era of remote development, containers, and agent-assisted workflows, that is a much more interesting design target than a generic upload page.