A tiny, self-hostable, server-blind secure intake box. One Go binary. Anonymous uploads. Post-quantum end-to-end encryption. Nothing flows back out.
The uploader's view — submit files into an unguessable page. No account, no folder, no listing, no download path. With server-blind E2E on, the browser encrypts every file before a byte leaves the device.
The admin dashboard — create intake pages, then review, decrypt, status, seal, and export evidence for what arrives. Only the authenticated admin can read what came in.
An admin creates an unguessable intake page, the uploader submits files without an account, and the admin receives one immutable submission envelope. The sender never gets a folder, listing, account, or download path.
Sprag is named after a sprag clutch: it engages in one direction and freewheels in the other. The product has the same shape for files. Uploaders push files into an unguessable upload-page URL — they can never list, download, or even see what else has arrived. Only the authenticated admin can read what came in.
With server-blind E2E intake enabled, the uploader's browser encrypts every file with post-quantum hybrid cryptography before a single byte leaves the device. The Go server and your configured storage backend only ever touch ciphertext. The admin decrypts client-side at download time. There is no plaintext for the server — or anyone who compromises it — to read.
A SecureDrop-adjacent intake capability without SecureDrop's operational weight: optional Tor onion ingress, but no hardened workstation, no source accounts, no air-gapped review workflow, and no multi-server deployment requirement.
For deployment recipes, see INSTALL.md. It covers plaintext intake, server-blind E2E intake, and onion-only Tor deployment.
| I want to... | Start here |
|---|---|
| Deploy Sprag on my own machine, VPS, S3-compatible storage, or onion service | Install and deploy |
| Use it for sensitive intake as a lawyer, journalist, HR/compliance team, doctor, researcher, or operator | Read the product and security overview |
| Check the exact security boundary before trusting browser E2E | Security model |
| Compare Sprag with file request links, portals, and whistleblower tools | Comparison |
- Lawyers receiving privileged or sensitive client documents
- Journalists receiving source material and leaks
- HR / compliance teams running whistleblower channels
- Doctors and researchers collecting sensitive records
- Anyone who needs to collect files from people who should not need an account
- One-way by construction. The uploader API surface is exactly three routes. There is no listing endpoint a sender can reach. Knowing one page's URL reveals nothing about any other page or the admin area.
- No accounts for uploaders. Ever. The unguessable URL is the capability. That same trusted channel also carries the page's public key, so server-blind encryption needs no separate PKI or key-exchange ceremony.
- Server-blind, post-quantum E2E. Optional per-deployment, optional or required per-page. ML-KEM-1024 + P-384 hybrid KEM, HKDF-SHA-512, AES-256-GCM — encrypted in the browser before upload.
- Tiny and legible. A single CGO-free Go binary with an embedded React frontend, one
.env, one SQLite file, and either local or S3-compatible file storage. You can read the whole threat model in an afternoon. - Bounded memory at any file size. Uploads and downloads stream through the selected storage backend. A 5 GB file is never buffered whole in RAM.
- Optional onion-only ingress. Sprag can run behind a Tor onion service with no public host ports, while keeping the product shape as one-way intake rather than a full whistleblower platform.
Sprag is not a Dropbox, a client portal, a ticketing system, a chat tool, or a form builder. It does not give uploaders accounts, folders, listings, comments, or download links.
That restraint is the product: a sender can push files in through a capability URL, then the path stops. The admin can review, decrypt, status, export evidence, and seal intake; the uploader cannot browse anything.
sequenceDiagram
participant U as Uploader browser
participant P as Sprag (Go server)
participant S as File storage
participant A as Admin browser
Note over A: Admin generates an ML-KEM-1024 + P-384 keypair.<br/>Public key is attached to the upload page.<br/>Private key never leaves the admin device.
U->>P: GET /u/{slug} (page metadata + public key)
Note over U: Browser encrypts file and metadata locally:<br/>ML-KEM-1024 + P-384 -> HKDF-SHA-512 -> AES-256-GCM
U->>P: POST ciphertext + opaque envelope
P->>S: store {uuid}.sprag (ciphertext only)
Note over P,S: Server and storage never see plaintext,<br/>original filename, or any private key.
A->>P: GET ciphertext + envelope (authenticated)
P-->>A: ciphertext + envelope
Note over A: Browser decrypts with the private key.<br/>Plaintext exists only on the two endpoints.
Without E2E, Sprag is still a strict one-way intake box: streaming uploads to local or S3-compatible storage, unguessable slugs, optional PINs, and admin-only listing and download. E2E mode adds the server-blindness on top.
The website has the buyer-facing comparison page; this README keeps the technical comparison compact for operators evaluating the source.
Most "send me a file" tools are outbound sharing products retrofitted for inbound use, and their servers can read your files in normal operation. Sprag is built the other way around: inbound-only intake is the only thing it does, which is exactly why server blindness fits naturally instead of being bolted on.
The category itself is not empty — self-hosted "reverse share" tools exist, and at least one already pairs anonymous upload with S3 storage. What none of them do is the structural thing: a persistent, self-hosted, S3-backed intake box where the operator's server provably cannot read what was uploaded, with post-quantum encryption of the stored file.
| Project | Intake model | Self-host / license | Client-side E2E of file | Post-quantum at rest | Footprint |
|---|---|---|---|---|---|
| Sprag | One-way anonymous, no uploader account | Yes, GPL-3.0 | Yes | Yes (ML-KEM-1024 + P-384 hybrid) | Tiny, single Go binary |
| Pingvin Share X | Reverse shares plus accounts | Yes, BSD-2-Clause | No | No | Medium (Node, Docker, optional ClamAV) |
| Sharry | Alias pages, anonymous upload to a user | Yes, GPL-3.0 | No | No | Medium (Scala/JVM) |
| Nextcloud File Drop | Public upload link into a folder, no account | Yes, AGPL-3.0 | No | No | Heavy (PHP plus database) |
| OnionShare (receive) | Anonymous inbound over Tor, no account | Yes, GPL-3.0 | Effectively, via Tor | No | Desktop/CLI, not a persistent service |
| SecureDrop | Anonymous whistleblower intake | Yes, AGPL-3.0 | No (server-side GPG; Tor plus air-gap) | No | Very heavy (dedicated hardware) |
| GlobaLeaks | Anonymous whistleblower intake | Yes, AGPL-3.0 | No (server-side; PQ only in TLS) | No (roadmap) | Light-to-medium (single server) |
| Bitwarden Send | One-way send link, sender account | Yes, AGPL/GPL | Yes | No | Medium / SaaS |
| Tresorit Send | One-way send link, no account | No (proprietary, SaaS) | Yes | Announced, not shipped | SaaS |
| Internxt Send | One-way send link, no account | Partial (MIT; self-host reportedly broken) | Yes | Partial (Kyber-512, storage only, lowest level) | SaaS |
| timvisee/send (Firefox Send fork) | One-way send link, no account | Yes, MPL-2.0 | Yes (AES-128) | No | Light (single container) |
| WeTransfer | Outbound send, no account | No (proprietary, SaaS) | No (provider holds keys) | No | SaaS |
Tresorit has publicly chosen the same ML-KEM-1024 hybrid design Sprag uses, but as of 2026 it is roadmap, not shipping, and not an anonymous-intake tool. Internxt genuinely ships some post-quantum encryption, but it is storage-only, Kyber-512 (NIST category 1, the lowest level), apparently not hybrid, and not confirmed for its Send product. GlobaLeaks runs live hybrid post-quantum TLS in transit but still stores submissions under classical encryption. The combination that is unique to Sprag is the intersection: one-way anonymous intake, no uploader account, client-side post-quantum E2E of the file, and a tiny self-hosted footprint.
- Unguessable upload pages — 24-character base62 slugs from
crypto/rand. A page has a title, optional description, optional PIN, optional expiry, optional per-page max file size, an optional allow-list of extensions, and an active flag. - Drag-and-drop uploads with multi-file support and per-file progress (bytes + ETA).
- Optional PIN per page (bcrypt-hashed, rate-limited per slug+client identifier).
- Submission envelopes — files selected or dropped together share one immutable submission ID, so the admin can see which files arrived as one package.
- Anonymous file-status receipts — every submission gets an unguessable receipt URL. The sender sees only aggregate arrival facts and status: received, reviewed, rejected, or downloaded.
- Admin dashboard — create/edit/delete pages, list uploads grouped by submission, update file status, download a single file, or download a plaintext page as a streamed
.zip. - QR codes and copy buttons for sharing capability URLs.
- Server-blind post-quantum E2E intake (see below).
- Chain-of-custody manifests — export page metadata, submission IDs, stored-object SHA-512 hashes, upload/download timestamps, and handling events as JSON. These are integrity and handling records, not legal certification: the hashes and logs document what Sprag observed, they do not by themselves establish admissibility.
- Legal hold / sealed mode — seal a page after intake closes; later administrative actions remain possible but are recorded as post-seal events. This is tamper-evident handling, not cryptographic immutability.
- Metadata-minimized ingress — store plaintext uploader IPs, deterministic HMAC identifiers, or no uploader IP at all for anonymous ingress such as Tor.
- Onion-only Tor deployment — publish Sprag as a v3 onion service with no host-published app or Caddy ports.
- Single static binary with the frontend embedded via
embed.FS. Pure-Go SQLite means CGO-free builds and trivial cross-compilation.
- Create an intake page. Set public title/instructions, optional PIN, optional expiry, file size limit, extension allow-list, and E2E policy.
- Share the capability URL. The page URL is the upload capability. QR and copy controls are built in.
- Receive submissions. Uploads are grouped into immutable submission envelopes and can include a sender receipt URL.
- Track file status. The admin can mark a submission received, reviewed, rejected, or downloaded. The public receipt page shows only that status and aggregate file counts/bytes.
- Download or decrypt. Plaintext pages support direct download and streamed ZIP export. E2E pages download ciphertext and decrypt in the admin browser with the page private key.
- Export evidence. Chain-of-custody manifests include stored-object SHA-512 hashes and handling events. In E2E mode the server-side hash is a ciphertext-object hash. Treat these manifests as integrity and handling records, not as legal certification or proof of court admissibility.
- Seal when intake closes. Sealing a page closes public intake, prevents reopening or page deletion, and marks later handling as post-seal activity.
- No uploader-reachable listing. The public surface is exactly
GET /api/u/:slug(metadata),POST /api/u/:slug/pin, andPOST /api/u/:slug. Upload responses never include other files. - Unguessable capability URLs. At least 24 chars, base62, cryptographically random.
- Admin password hashed with bcrypt. Supply it as plaintext (
ADMIN_PASSWORD) or — better — as a precomputed bcrypt hash (ADMIN_PASSWORD_HASH) so the plaintext never lives in your config. Passwords beyond bcrypt's 72-byte limit are handled via an internal SHA-256 prehash. - IP metadata policy. By default Sprag stores uploader IPs as plaintext for compatibility. Set
IP_STORAGE_MODE=hmac-sha256andIP_HASH_SECRETto store deterministic keyed HMAC-SHA-256 identifiers instead; startup rewrites existing plaintext uploader IPs in SQLite toip-hmac-sha256:v1:<digest>. - Rate limiting. Admin login 5/min/client identifier, PIN attempts 10/min per slug+client identifier, keyed on the real client IP or its HMAC identifier (see
TRUSTED_PROXY_HOPS). WithANONYMOUS_INGRESS=true, Sprag stores no uploader IP metadata and uses one global admin-login bucket plus page-scoped PIN buckets. - Sessions. Stateless HMAC-signed cookies, 7-day expiry,
HttpOnly+SameSite=Lax. Cookies use theSecureattribute for HTTPS;COOKIE_SECURE=autodisables it only for localhost, loopback, and HTTP.onionorigins where browsers would otherwise refuse to send the cookie. - CSRF. Admin mutations require the
X-Sprag-CSRFcustom header in addition to the same-site cookie. - Streaming with hard caps. The size limit is enforced by a counting reader while streaming instead of trusting
Content-Length; failed S3 multipart uploads are aborted and failed local uploads never publish their temporary file. Files are never buffered whole in memory. - Path-safe storage. Object keys use server-generated UUID paths (
S3_PREFIX/<slug>/<uuid>/<filename>for S3,STORAGE_PREFIX/<slug>/<uuid>/<filename>locally); local access is rooted beneathLOCAL_STORAGE_PATH, and the original filename is metadata only, so a malicious name can't traverse or collide. - Downloads are always
Content-Disposition: attachment, never inline, so stored objects can't be used as an XSS host. - Secrets are never logged. Startup echoes a redacted config.
cp .env.example .env
openssl rand -base64 32 # put this in SESSION_SECRET
docker compose run --rm sprag-app hash-password
# put the printed hash in ADMIN_PASSWORD_HASH, then choose local or S3 storage
docker compose up -dFor production recipes, E2E-required mode, and onion-only Tor ingress, use INSTALL.md.
Prebuilt multi-arch images (amd64 + arm64) are published to the GitHub Container Registry on every release. The bundled docker-compose.yml uses that image by default and runs Sprag behind Caddy with automatic HTTPS:
cp .env.example .env
openssl rand -base64 32 # put this in SESSION_SECRET
docker compose run --rm sprag-app hash-password 'your-admin-password'
# put the printed hash in ADMIN_PASSWORD_HASH, then choose local or S3 storage
SPRAG_DOMAIN=sprag.example.com docker compose up -dPin SPRAG_IMAGE in .env for reproducible deploys, for example ghcr.io/elcamino/sprag:1.6.1. To test a local source build with the same Compose topology:
docker build -t sprag:local .
SPRAG_IMAGE=sprag:local SPRAG_PULL_POLICY=never docker compose up -dFor a one-container trial without Caddy:
docker run -d --name sprag -p 8080:8080 \
--env-file .env -v sprag-data:/data \
ghcr.io/elcamino/sprag:latestRunning the server needs BASE_URL, SESSION_SECRET, storage settings, and one of ADMIN_PASSWORD / ADMIN_PASSWORD_HASH in your .env (see Configuration).
Every release also ships standalone binaries (no runtime dependencies, frontend embedded) for Linux, macOS, and FreeBSD on amd64/arm64/arm. Download one from the Releases page and run it:
# optional: verify the download against the release checksums
sha256sum --ignore-missing -c SHA256SUMS
tar xzf sprag_v1.6.1_linux_amd64.tar.gz
./sprag_v1.6.1_linux_amd64/sprag # loads a .env in the current directory if presentThe server still needs BASE_URL, SESSION_SECRET, storage settings, and an admin password (see Configuration).
Use INSTALL.md for full setup instructions. It includes:
- plaintext intake with normal server-readable uploads
- server-blind post-quantum E2E intake
- onion-only Tor deployment
- local development and MinIO notes
- backup, restore, and troubleshooting checklists
For source-based setup, use go run ./cmd/sprag hash-password instead of the Docker hash command.
Sprag loads .env if present and then reads environment variables. Startup fails fast if required secrets or backend-specific storage values are missing. ONION_BASE_URL is a Docker Compose helper used by docker-compose.tor.yml; it is mapped to BASE_URL inside the app container.
| Variable | Required | Default | Notes |
|---|---|---|---|
PORT |
8080 |
Listen port. | |
SPRAG_IMAGE |
ghcr.io/elcamino/sprag:latest |
Compose-only helper for the app image. Pin to a release tag for production. | |
SPRAG_PULL_POLICY |
missing |
Compose-only helper. Use never when testing a local image tag. |
|
SPRAG_DOMAIN |
localhost |
Compose-only helper for the bundled Caddy reverse proxy. | |
BASE_URL |
Yes | Used to build shareable /u/<slug> URLs. |
|
ONION_BASE_URL |
Compose-only helper for docker-compose.tor.yml; set to http://<hostname>.onion after Tor generates the hostname. |
||
COOKIE_SECURE |
auto |
Cookie Secure attribute policy: auto, true, or false. auto uses secure cookies for HTTPS and non-secure cookies only for localhost, loopback, and HTTP .onion origins. |
|
SESSION_SECRET |
Yes | Base64; must decode to at least 32 bytes. Rotating it invalidates all sessions. | |
ADMIN_USERNAME |
admin |
||
ADMIN_PASSWORD |
Yes* | Plaintext, bcrypt-hashed in memory at boot. | |
ADMIN_PASSWORD_HASH |
Yes* | Precomputed bcrypt hash (preferred). Takes precedence over ADMIN_PASSWORD. |
|
IP_STORAGE_MODE |
plain |
plain stores resolved uploader IPs. hmac-sha256 stores deterministic ip-hmac-sha256:v1: identifiers and rewrites existing plaintext uploader IPs at startup. |
|
IP_HASH_SECRET |
Yes* | Base64; required only when IP_STORAGE_MODE=hmac-sha256; must decode to at least 32 bytes. Protect it: IPs are low-entropy, so the secret is what prevents offline enumeration. |
|
MAX_FILE_SIZE |
5368709120 (5 GiB) |
Global default; per-page limits may only lower it. | |
ALLOWED_EXT |
(any) | Comma list, e.g. pdf,png,zip. A hard ceiling per-page lists may narrow but not widen. |
|
TRUSTED_PROXY_HOPS |
1 |
Number of trusted proxies appending to X-Forwarded-For. 0 = directly exposed. |
|
ANONYMOUS_INGRESS |
false |
Set true for Tor/onion ingress. Sprag stores no uploader IP and uses global/page-scoped abuse buckets instead of apparent per-IP buckets. |
|
DB_PATH |
/data/sprag.db |
SQLite path (WAL mode; back up the whole directory). | |
STORAGE_BACKEND |
s3 |
File-body backend: s3 or local. |
|
STORAGE_PREFIX |
pages/ |
Key namespace used only by the local filesystem backend. | |
LOCAL_STORAGE_PATH |
/data/uploads |
Root directory used only when STORAGE_BACKEND=local. Created at startup. |
|
S3_ENDPOINT |
Yes* | S3-compatible endpoint; required only for the S3 backend. | |
S3_REGION |
Yes* | Required only for the S3 backend. | |
S3_BUCKET |
Yes* | Required only for the S3 backend. | |
S3_ACCESS_KEY |
Yes* | Required only for the S3 backend. | |
S3_SECRET_KEY |
Yes* | Required only for the S3 backend. | |
S3_USE_PATH_STYLE |
false |
true for MinIO. |
|
S3_PREFIX |
pages/ |
Key namespace used only by the S3 backend. | |
E2E_INTAKE_ENABLED |
false |
Enables server-blind E2E intake. | |
E2E_INTAKE_REQUIRED |
false |
Rejects plaintext pages/uploads. Requires E2E_INTAKE_ENABLED=true. |
|
E2E_INTAKE_ALGORITHM |
ML-KEM-1024-P384-HKDF-SHA512-AES-256-GCM |
The only supported profile. |
* Exactly one of ADMIN_PASSWORD / ADMIN_PASSWORD_HASH is required. Storage requirements depend on STORAGE_BACKEND as noted above.
MAX_FILE_SIZE defaults to 5 GiB. Admin sessions are stateless signed cookies (7-day expiry). Rotating SESSION_SECRET invalidates every outstanding session immediately; changing only the admin password does not, so rotate the secret too if you need to force existing sessions to log out.
When E2E_INTAKE_ENABLED=true, admins can create pages whose uploads are encrypted in the uploader's browser before any bytes leave the device. Set E2E_INTAKE_REQUIRED=true to reject plaintext pages and plaintext uploads entirely.
How it works:
- The admin generates an encryption identity in the browser. The public key is attached to the upload page; the private key never touches the server.
- The public key rides on the same unguessable capability URL the admin already shares — no separate PKI or key server.
- The uploader's browser encrypts each file and its metadata locally and uploads only ciphertext.
- The server stores page public keys and encrypted upload envelopes, but never stores E2E private keys.
- The admin decrypts downloads client-side with the private key.
The cryptographic profile is ML-KEM-1024-P384-HKDF-SHA512-AES-256-GCM: the browser combines ML-KEM-1024 (post-quantum KEM, via @noble/post-quantum) with P-384 ECDH in a hybrid construction, derives AES-256-GCM keys with HKDF-SHA-512, encrypts file bytes and metadata locally, and uploads only ciphertext. The hybrid design means an attacker would have to break both a classical and a post-quantum primitive — and recorded ciphertext stays safe against future quantum "harvest now, decrypt later" attacks. Both the HKDF info and the AES-GCM additional data fold in the canonical envelope, so no envelope field can be swapped without breaking decryption. ML-KEM-1024 targets NIST security category 5, the highest level.
Back up each generated private key. If it is lost, the matching encrypted uploads cannot be recovered — not by you, not by anyone. The server is blind by design.
The admin UI can optionally store a generated private key encrypted in browser IndexedDB. The stored value is wrapped with a key derived from an admin-supplied passphrase via memory-hard Argon2id and sealed with AES-256-GCM; Sprag never saves the passphrase. This is safer than keeping an unencrypted downloaded key, but weaker than a password manager or offline backup, and still does not protect against a compromised admin-origin script after the key is unlocked.
What server-blind does and does not protect against. A trust product lives or dies on the precision of this claim, so state it plainly. Sprag's E2E mode protects you against the passive, at-rest adversary: a stolen S3 bucket or local upload directory, a compromised admin session, an honest-but-curious operator, a backup that walks out the door, a full server compromise after the fact. In every one of those, the attacker gets ciphertext and an opaque envelope and nothing else. The true line is the operator cannot read stored uploads; a breach yields only ciphertext. What it does not defend against is an actively malicious or compromised host at upload time: the encryption runs in JavaScript the server delivers, and the public key arrives over the same channel, so a host that deliberately serves modified code or swaps the key could defeat it. The admin sees the key fingerprint in the dashboard, but it is not yet surfaced on the upload page for a source to compare out of band. This is the same caveat that applies to Firefox Send and to webmail-based "E2E"; it is inherent to browser-delivered cryptography, not a flaw in the construction. There is also no forward secrecy — the recipient key is static, so a leaked private key exposes all historical uploads — and because E2E encryption happens in a single pass in the uploader's browser, keep E2E uploads to a few hundred megabytes for now. The line you must not read into Sprag is "zero-trust even against the host who runs it."
Metadata is stored in SQLite at DB_PATH. The database runs in WAL mode with a busy timeout so concurrent uploads don't fail under lock contention; this creates -wal and -shm sidecar files next to DB_PATH, so back up the whole directory. File bodies are streamed either to S3-compatible object storage under S3_PREFIX or beneath LOCAL_STORAGE_PATH under STORAGE_PREFIX. Local uploads are written to a private temporary file and atomically renamed only after the complete stream is synced. In E2E mode the stored object is opaque ciphertext (a .sprag blob), the content type is forced to application/octet-stream, and the original filename lives only inside the encrypted envelope — never in the object key.
- Backend: Go (stdlib
net/http+chi), pure-Go SQLite (modernc.org/sqlite, CGO-free), local filesystem or AWS SDK v2 for any S3-compatible endpoint,log/slogJSON logging. - Frontend: React + TypeScript + Vite + Tailwind CSS, built to
frontend/dist/and embedded into the binary. - Crypto: bcrypt for passwords/PINs;
@noble/post-quantum(ML-KEM-1024) + WebCrypto (P-384 ECDH, HKDF-SHA-512, AES-256-GCM) for E2E; memory-hard Argon2id (@noble/hashes) for the optional in-browser private-key store. - Deployment: multi-stage Dockerfile to
gcr.io/distroless/static,docker-compose.ymlwith Caddy for automatic HTTPS, anddocker-compose.tor.ymlfor onion-only Tor ingress.
go test ./... # backend tests
cd frontend && npm test # frontend tests (Vitest), including the E2E crypto round-trip- Security reports: SECURITY.md
- Contributions and scope rules: CONTRIBUTING.md
- License: GPL-3.0-or-later
Sprag is free software under the GNU General Public License v3.0 (or later). See LICENSE.md.