Encrypted-content share links #19
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: Cordy/Cairn#19
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Public share links (
/s/<token>) currently assume plaintext-readable content. When the backing object is age-encrypted, the share handler must decrypt on serve (server-side, using the owner's or a share-scoped key).Deferred from the v0.3 encryption probe.
Decision — deferred (Manuel's review, relayed by Nikola 2026-07-31):
Stays open but re-gated: the blocker is no longer the encryption review, it's a product decision on the sharing model itself. Until how-we-share is settled, encrypted-content share links have no target to build against. Current shipped behaviour stands documented (main.go note): with per-user custody there is no user context on public
/s/, so shares of encrypted content cannot decrypt.Design decided (Nikola, 2026-08-04) — this unblocks #19. Sharing splits into two lanes: peering (recipient has a Cairn instance — v0.5 track, delivery re-encrypts to the recipient's own key, #104) and links (recipient has NO instance — this issue). What follows is the links design.
Strong shares: ciphertext-only share links
The claim (marketing-honest, agreed): the public share surface stores and serves only ciphertext; compromising the storage backend, the share database, or sniffing the download yields nothing without the key material in your hand. Explicitly NOT "zero-knowledge": a compromised cairnd sees content (it always does — transparent server-side decryption is the product), and the decryption JS is served by the instance itself. We say so in the docs.
Mechanism — on-the-fly age re-encryption (no stored copy): at download, the server decrypts the object internally (owner/space key, exactly like any read) and re-encrypts the stream to a share-specific age recipient. Nothing extra is stored, revocation stays instant (the mint happens per fetch), and the crypto cost rides the existing scaling answer — the
--target=cryptorole (#33), per Manuel's compute-scaling design. Concurrency-capped like everything else.Two modes:
#fragment(never sent to the server, server keeps only the public recipient). One self-sufficient artifact — convenience default.Recipient experience: link opens a lightweight viewer page served by the instance; decryption happens in-browser via
typage(Filippo's official TypeScript age — the one new UI dependency). v1 caps link-share file size (browser-memory decrypt); streaming decrypt for very large files is a later lift.This REPLACES the existing plain
/s/share links — not a parallel feature. The new pipeline works uniformly on encrypted AND unencrypted instances (on unencrypted ones the "internal decrypt" is a plain read; the share surface still serves only ciphertext). Everything the current shares have carries over: expiry, revocation, admin list/revoke-all, and the org policy panel applies unchanged and instance-wide —shareMaxExpiry(enforced ceiling),shareRequirePassword(forces password mode, fragment mode refused),shareUploadsForbidden. Upload-drop links survive too and get stronger on encrypted instances: anonymous uploads are encrypted to the owner's public recipient + recovery — no secrets involved, the public surface never holds plaintext at rest.Known and accepted: a recipient who has downloaded ciphertext and holds the key keeps that file forever — revocation stops future fetches, nothing un-rings the bell (true of every sharing system; documented). Wrong password in password mode fails at the gate (server) — the in-browser decrypt failure path exists only if someone bypasses the viewer.
age-CLI compatibility (payload is a genuine age file, so
curl | age -dwould work): deliberately NOT surfaced or documented as a feature — a very small user subset would care. Filed for market review in v0.6 (#113).Build split:
/s/payload path).Shipped in v0.3.30 (PRs #114 phase 1, #115 phase 2, #116 phase 3) — live on .249.
The links lane is complete: every share download is decrypted internally and re-encrypted on the fly to that link's own age recipient, so the public
/s/surface serves ciphertext only; the recipient opens the link and gets their file, decrypted in-browser, no account or tooling. Fragment mode (key after the#, never transmitted) and password mode (age scrypt recipient; the password also unlocks the gate so ciphertext can't be harvested for offline cracking) are both selectable in the share dialog, with the org policy forcing password mode when configured. Legacy plain links keep working until they expire. Upload links now encrypt anonymous uploads to the owner's key.Docs:
docs/handbook/sharing.mdcarries the claim boundary verbatim — ciphertext-only surface, explicitly not zero-knowledge, and revocation's limit. The README's "four third-party dependencies" line was corrected to name the one vendored browser asset (pinned typage build, embedded, never CDN).Dogfood checklist for .249 (share links are enabled there):
#…part → viewer refuses with the "missing its key" message.curl -sL '<link>?payload=1' | head -c 60→age-encryption.org/v1, and the plaintext is nowhere in the response.mode:"fragment"is refused.?payload=1404.Remaining threads tracked elsewhere: peering lane in v0.5 (#100–#106), CLI-compat marketing review in v0.6 (#113), streaming decrypt for very large files (noted in the handbook, no issue yet — file one if the beta hits it).
Closing #19; reopen or file follow-ups from the dogfood.