How a dove share moves.
Follow plaintext, ciphertext, keys, metadata, and access policy from the sender’s disk to the recipient’s device.
Two tiers, two security boundaries
| Simple | Full | |
|---|---|---|
| Stored object | Plaintext unless --encrypt is set | Always client-side encrypted |
| Link | S3 presigned GET, at most 7 days | Gate URL plus a fragment secret |
| Policy | Expiry only | Expiry, one/N downloads, optional PIN |
| Server can read file? | Yes, unless optional encryption is used | No—the content key never reaches it |
| Recipient | Direct download; encrypted links use dove get | Browser under 2 GB; CLI for larger files |
The full-mode data path
- Prepare locally. A directory is zipped. A random 32-byte secret is generated. If the share has a PIN, the content key is derived with PBKDF2-HMAC-SHA256 from the PIN and fragment secret.
- Encrypt locally. dove writes a versioned, chunked AES-256-GCM container to a temporary file before upload.
- Upload ciphertext. S3 receives a name-free object key. The plaintext filename, sender name, and message are encrypted separately with a domain-separated metadata key.
- Register policy. The scoped signing identity writes the expiry, download budget, ciphertext size, encrypted metadata, and optional PIN verifier to DynamoDB.
- Print the link. The gate URL carries a MAC-authenticated share ID. The high-entropy secret lives after
#. - Preview safely. Opening
/d/<id>serves static HTML. Its free/meta/<id>read does not decrement the budget. - Act explicitly. Only a recipient click reaches
/dl/<id>. The gate atomically decrements policy and redirects to a 15-minute presigned S3 GET. - Decrypt locally. The browser or CLI authenticates every chunk and writes the plaintext on the recipient’s device.
Why the fragment matters
https://share.example.com/d/4c8f…d02a#M9g…fragment-secret The browser sends the origin and path in HTTP. It does not send the fragment. JavaScript on the loaded page can read location.hash locally, which lets the recipient UI derive a key that CloudFront, API Gateway, Lambda, DynamoDB, S3, DNS, and ordinary access logs never receive.
This is a structural claim, not a policy promise. The serving infrastructure lacks the key material needed to decrypt a full-mode file.
Encrypted container format
The current implementation uses a 1 MiB default plaintext chunk size. Integers are big-endian.
header: "DOVE" | version:u8 | nonce_prefix:[u8;8] | chunk_size:u32
chunk: is_last:u8 | ct_len:u32 | ciphertext_and_tag[ct_len] A nonce is the random 8-byte file prefix plus a 32-bit chunk counter. The counter and final-chunk flag are authenticated as additional data. Reordering, truncation, an altered terminal flag, a wrong key, or changed ciphertext fails authentication.
PINs split access from confidentiality
The gate checks a PIN online and locks the share after five wrong attempts. Separately, the client derives the content key with 600,000 PBKDF2-HMAC-SHA256 iterations using the fragment secret as salt. The gate sees a submitted PIN, but never the fragment secret. The link carries the fragment secret, but not the PIN.
The pre-check endpoint is /verify/<id>?pin=…; it rate-limits without spending a download. The explicit download still supplies the PIN to /dl/<id>. Do not put PINs in application logs.
Browser versus CLI
The default recipient page checks metadata first. Files smaller than 2 GB use WebCrypto in the browser and save locally. Larger files show their size and a complete dove get <url> handoff. This is a product threshold, not a cryptographic one: browsers are a poor place to buffer multi-gigabyte payloads, while the Rust decryptor can stream authenticated chunks to disk.