Web Upload
DraftWeb upload lets a guest with no Capsule account — the non-registered account class, using the browser web client — send assets to a user. The guest holds no album key and no MLS membership and cannot sign, so it never writes into the library: the user provisions an upload link, the guest’s browser encrypts each asset and seals its key to the link’s Drop Key, the sealed bytes land in the user’s staging inbox, and nothing becomes a library asset until the user adopts it on a native client, which authors the provenance.
This doc owns the upload-link capability, the drop wire object, and the adoption transition. The Drop Key and its escrow are owned by Keys — Non-registered accounts; the key rewrap by Encryption — Asset Key Derivation; key_mode/wrapped_file_key by Provenance — Asset Manifest. Endpoints and error codes are defined by capsule-server/src/routes/drop.rs and the served OpenAPI contract.
Two Confidentiality Properties
Section titled “Two Confidentiality Properties”- Server-blind. The server stores only ciphertext and a KEM-encapsulated key it cannot open. The Drop Key public half reaches the guest in the link’s URL fragment, which browsers never transmit, so the server cannot substitute a key it controls.
- Contribute-only. The guest holds only the public Drop Key half — no AMK, device key, or write-tier key. It cannot read the library, other guests’ drops, or the album’s contents.
In v1: link provisioning with per-link caps (expiry, cumulative bytes, file count, per-file size, single- or multi-use) and revocation; browser drop upload of originals; a per-user inbox charged to the provisioning user’s quota; adopt-in-place; discard.
Not in v1: guest-produced derivatives, metadata or provenance (the adopter produces them); guest read-back; adoption by anyone but the provisioning user (the Drop Key is escrowed only under their hierarchy); writable share links.
Security Contract
Section titled “Security Contract”- Link format.
https://server.tld/u/{opaque_id}#{drop_pubkey}.{opaque_id}follows the share-link opaque-id rule (≥128-bit CSPRNG, no structure);{drop_pubkey}rides the fragment. - Drops never enter the library. A drop lives only in the provisioning user’s inbox, never on any sync feed, and is served only to that user’s devices. It carries no
AssetManifestand never reachesverify_asset. Library state is reachable only through adoption by a trusted client. - Per-link caps are enforced server-side at drop-session creation, decided and reserved in one store operation so concurrent guests cannot both take the last slot. A leaked link is bounded to wasted quota and inbox space.
- Quota is charged to the link owner at drop-session creation — the single enforcement point.
- Probing reveals nothing. An unknown, expired, revoked, or spent link and an unknown session all return one indistinguishable
404, never410. A cumulative cap on a live link is the distinct409 error.drop.cap_exhausted, because the guest holds a real link and needs a new one. - Rate limits. Drop-session creation is limited per source IP and per
{opaque_id}(429 error.drop.rate_limited), in that order, before the link is resolved, failing closed. The source is resolved the way Share Links resolves it. - Optional passphrase gates the write, not confidentiality. The link record stores an Argon2id verifier; the guest sends the derived proof as
passphrase_proof, compared in constant time before any cap is reserved so wrong guesses cannot burn the link down. A refusal is403 error.drop.passphrase_required— not404, since the caller already holds the id. - Home-server-only. Like a share link, an upload link is served only by the album owner’s home server; a peer refuses with a
{ home_server }pointer. - Adopted bytes are external-origin. Every client decodes them only in the sandboxed decoder.
Drop and Adoption Lifecycle
Section titled “Drop and Adoption Lifecycle”1. Provision (native client)
Section titled “1. Provision (native client)”The client mints a Drop Key, escrows the private half under the master key and the user’s OGK, and registers the link record (opaque id, caps, pinned protocol_version and crypto_suite_id, optional passphrase verifier). The server never sees {drop_pubkey}.
2. Seal and upload (web client)
Section titled “2. Seal and upload (web client)”Per asset, the browser draws a random 32-byte key K, encrypts under the unchanged STREAM construction, encapsulates K to {drop_pubkey} as kem_ct, and uploads the ciphertext with an unsigned DropDescriptor (content type, plaintext size, STREAM chunk size and nonce prefix, ciphertext_hash, kem_ct, advisory suggested_filename). Transfer uses the upload protocol’s chunk, finalization and session-lifetime rules under link-capability auth. A descriptor is not a manifest: it has no signatures, no album_id, and no provenance; it is untrusted until a trusted client decapsulates K and the STREAM tags verify.
3. Stage (server)
Section titled “3. Stage (server)”The server validates the session against the link record and the drop invariants, charges quota, and stores the ciphertext as a content-addressed blob referenced by a drop-inbox row. A finished drop session is deposited under the upload protocol’s finalization claim: the server re-hashes the ciphertext against ciphertext_hash before committing it, then moves the held descriptor into the inbox in one store operation, so the drop is filed exactly once. A claimed deposit runs detached and reaches a terminal state whatever happens to the guest’s request or the server process; the guest reads its outcome through the drop’s status probe under the link capability. A full drop session that nobody claimed, because the process died between recording its last chunk and claiming, is deposited on the next boot, or sooner when the guest resends one of its chunks. A deposit that fails before filing, or a drop session discarded under pressure or for a missing stage, gives the link its reservation back. The drop appears on the user’s native clients as a quarantine surface awaiting review.
4. Review and adopt-in-place (native client, provisioning user)
Section titled “4. Review and adopt-in-place (native client, provisioning user)”The client decapsulates K, previews in the sandboxed decoder, and on approval:
- Authors the signed sidecar with an unverified guest-origin note. The guest is never a signer.
- Wraps
Kunder the destination album’s AMK (asset-keywrap/v1) aswrapped_file_key; an externally chosenKcannot be derived, so it is carried. - Signs a
createmanifest withciphertext_hashandnonce_prefixfrom the drop,key_mode = wrapped, and the adopter as author, and appends thecreateprovenance record. - Submits it to
POST /v1/drops/{drop_id}/adopt. The server re-runs the create validation battery and requires the referenced blob to be a drop in the caller’s own inbox; only the metadata and provenance blobs are new bytes. The ciphertext hash is not recomputed — that already ran at the drop’s finalization.
Server-side adoption is claim → write asset → settle: writing first could duplicate a photo, deleting first could lose one, so a crash leaves the row visible in the owner’s inbox marked adopting, never lost or duplicated. The client drops its local inbox row only after the asset is written locally. Afterwards the asset is ordinary; later edits use the derived-key path, and only this create sets key_mode = wrapped.
5. Discard
Section titled “5. Discard”A rejected or expired drop’s inbox row is removed and its blob garbage-collected, freeing quota. No provenance is involved.
Why Adopt-in-Place
Section titled “Why Adopt-in-Place”Re-encrypting under a derived key would upload and store every asset twice. Rewrapping the 32-byte key keeps one copy, at the cost of one contained divergence: the adopted asset’s file key is carried wrapped rather than derived. It affects only key recovery — never authorization, provenance, or STREAM.
Failure Modes
Section titled “Failure Modes”- Leaked link: bounded by caps, quota, rate limits, and revocation; it cannot inject into the library.
- Server reads drops: impossible without
{drop_pubkey}, which it never receives. - Hostile file: decoded only in the sandbox, after owner review.
- Forged
wrapped_file_keyor mismatchedkey_mode: covered bydevice_sigandwrite_sig; failsverify_assetand is quarantined. - Weak guest
K: compromises only that asset.
Validation
Section titled “Validation”- Seal → decapsulate → STREAM-decrypt round-trips byte-identically;
kem_ctlength matches the suite. - Upload-link ids are ≥128-bit and non-sequential.
- An adopted manifest with
key_mode = wrappedpassesverify_asset, and a second member unwraps and decrypts the unchanged ciphertext; a forged wrapped key or mismatchedkey_modeis rejected. - A
DropDescriptorcannot reach any album-write path, and the inbox never appears on the sync feed. - Caps, quota, the per-source and per-link limiters, and the indistinguishable
404each refuse as specified. - A crash mid-adoption leaves an
adoptingrow, never a lost or duplicated asset.