Back to Croc

Encrypted stored transfers

src/docs/STORED_TRANSFERS.md

10.7.07.2 KB
Original Source

Encrypted stored transfers

Stored transfers are croc's opt-in asynchronous mode. A sender encrypts one or more regular files locally, uploads only ciphertext, and shares a capability link. The first receiver to finish authenticating and verifying every file commits the download, which immediately removes the ciphertext. An unfinished transfer expires 24 hours after upload finalization.

This is deliberately separate from the normal croc relay protocol. Direct transfers remain the default and continue to use PAKE between two live peers.

User workflow

Upload with the public service:

bash
croc send --store photo.jpg document.pdf

Use another service origin when self-hosting:

bash
croc send --store --store-url https://files.example.com photo.jpg

The sender receives:

  • a browser link, https://files.example.com/s/<id>#v1.<key>;
  • a croc-store-v1... CLI token;
  • the precise expiration time; and
  • a transfer ID that can be passed to croc --revoke.

On the receiving CLI, run croc and paste the token or link at the prompt. This avoids exposing its decryption key in the UNIX process list. Noninteractive use can supply the same value through the environment:

bash
CROC_STORE_TOKEN='croc-store-v1....' croc --out ./received

The CLI decrypts the manifest before showing file names and sizes. It asks for confirmation, downloads to partial files, authenticates every chunk, verifies each complete file with SHA-256, renames it into place, and only then commits the one download. Its partial state allows completed chunks to resume.

The web client performs the same inspection before asking the user to accept. It writes through the File System Access API where available. Otherwise a service worker streams the download without keeping the complete file in page memory. Blob fallback is limited to 256 MiB; for larger files on browsers that support neither streaming path, the UI directs the recipient to the CLI.

The sender can revoke an unconsumed transfer:

bash
croc --revoke <transfer-id>

CLI revoke capabilities are stored with mode 0600 in the croc configuration directory. Browser upload and claim capabilities are limited to the current tab session.

Why the browser key follows #

The URL fragment is interpreted by the browser and is not sent in HTTP request targets or Referer headers. The service therefore sees /s/<id> while the web client reads #v1.<key> locally and uses it to decrypt. This keeps the decryption key out of server access logs and reverse-proxy logs.

The complete link is still a bearer secret. Anyone or any application that can read the text can decrypt the files, claim the transfer, or deliberately consume it without saving the files. Share it over an appropriately private channel. The fragment protects against normal HTTP transport and logging; it does not protect a compromised browser, recipient, sender, or messaging account.

Cryptographic format

The versioned protocol name is croc-store-v1.

  • Each transfer uses a random 256-bit master key and a random 128-bit public transfer ID.
  • HKDF-SHA-256 derives independent manifest, data, and redeem keys or capabilities from the master key.
  • The manifest and every non-empty 4 MiB file chunk use AES-256-GCM with a fresh random 96-bit nonce.
  • Associated data binds the protocol version and transfer ID. Chunk associated data additionally binds the global object index, file index, per-file chunk index, and expected plaintext length, preventing substitution or reordering.
  • The encrypted manifest contains safe base file names, byte lengths, modification times, SHA-256 file digests, and the exact chunk map.
  • The service stores only SHA-256 verifiers for upload, redeem, and active claim capabilities. Capability values travel only in Authorization: Bearer headers and API responses marked Cache-Control: no-store.

The service necessarily learns connection metadata, timing, ciphertext byte lengths, the total plaintext byte count, file and chunk counts declared for quota enforcement, and the opaque transfer ID. It cannot learn individual file names, contents, hashes, modification times, or the master key from stored data. It can deny service, delete data early, or serve stale/corrupt ciphertext, but official clients authenticate ciphertext and verify the final plaintext before committing consumption.

One-download state machine

New objects have one of these persistent states:

  1. uploading — reserved for one hour while ciphertext objects arrive;
  2. available — complete and redeemable for 24 hours;
  3. claimed — locked to one receiver for up to 30 minutes, renewed as chunks are read;
  4. consumed, revoked, or expired — ciphertext is deleted immediately and a small tombstone remains for 24 hours so callers receive a stable terminal result.

Reading the encrypted manifest does not consume or lock a transfer. A receiver claims immediately before reading chunks. Only the claim capability can read chunks or commit. If the receiver disappears, the claim expires and another receiver may try. Official clients commit only after client-side authentication and whole-file SHA-256 verification. The service cannot prove that a custom client actually performed those checks.

Running a service

Storage is disabled by default. Enable it on the unified web server with a private persistent directory:

bash
croc serve \
  --bind 127.0.0.1:9014 \
  --store-dir /var/lib/croc/store \
  files.example.com

Put the complete origin behind HTTPS and proxy every path, including /api/v1/store, /ws, the web assets, and stored share routes, to the same process. Do not cache the API. The generated browser links use the origin from which the browser app is loaded.

The storage controls are:

FlagDefaultPurpose
--store-max-transfer1GiBMaximum plaintext bytes per transfer (up to 2GiB)
--store-quota5GiBMaximum reserved ciphertext across transfers
--store-min-free512MiBDisk space that must remain available
--store-max-files100Maximum regular files per transfer
--store-create-rate5Creates allowed per client IP per hour
--store-active-uploads2Concurrent incomplete uploads per client IP
--store-trusted-proxynoneRepeatable trusted reverse-proxy CIDR

Byte flags accept integer B, KB, MB, GB, TB, KiB, MiB, GiB, and TiB suffixes. A service holds an exclusive lock on its configured root; do not point multiple processes at the same directory. On startup it rebuilds quota and active-upload accounting from metadata, then sweeps incomplete, expired, and terminal records every minute.

Run the service as an unprivileged account. Give only that account access to the storage directory, monitor free space and HTTP 429/507 responses, and exclude ciphertext from backups so deletion semantics are not undermined. Only configure --store-trusted-proxy for infrastructure that overwrites client forwarding headers; otherwise rate limiting deliberately uses the socket peer address.

The HTTP API is versioned at /api/v1/store/transfers. It is an implementation boundary for the official croc web and CLI clients, not a promise that unversioned internals will remain compatible.