Share links cluster: menu "Share" does nothing, clipboard gets the toast text, toast leaks backend path, /s/ link 404s instead of a viewer #175

Closed
opened 2026-08-10 00:49:32 +00:00 by Cordy · 5 comments
Owner

Four dogfood notes (Nikola), all in the share-link surface, filed together because they share one code path:

  1. "Share" in the three-dot menu does nothing visible — for files and folders. Expected: the share dialog. Either the handler is broken or it silently creates a link (see 2) — both wrong.
  2. Per-row share/copy-link puts the wrong thing on the clipboard: the clipboard receives the toast text ("Share created for /home/Andrej-karpathy-skills.md") while the URL is only displayed — inverted. Clipboard gets the URL; the toast stays in the UI.
  3. The toast names the backend path (/home/folder 1) — the user lives in a virtual namespace where that folder is just "Personal/folder 1". Cosmetic, but it contradicts the product's own path model. Show the virtual path.
  4. Opening the copied link → bare "404 page not found" instead of the share viewer with a download button. This is the serious one. Two things to establish: (a) the viewer page route — the strong-share viewer (#19, v0.3.30) should render for GET /s/<token> and decrypt in-browser with the fragment key; (b) whether the share object itself is valid (a share created against the wrong — scoped vs actual — path would resolve to nothing).

On the #AGE-SECRET-KEY-… in the URL — not a leak, but a presentation failure. That is fragment-mode strong sharing by design: the key rides the URL fragment, which never reaches any server, so possession of the complete link is the credential (the share dialog's "Key in the link (simplest)" mode). But showing a raw AGE-SECRET-KEY string to an end user reads like a security accident. The viewer flow is supposed to make this invisible; once 4 is fixed, nobody should ever look at the URL's innards. Worth a UX pass on the copy ("anyone with the complete link can open it") either way.

Follow-up needed from Nikola: paste a freshly created share link here (from the dogfood; it's your own test file) — with a live token I can open it with the browser extension, watch the network requests, and pin whether the 404 is routing, token lookup, or path resolution. A screenshot of the three-dot menu after clicking "Share" would also settle whether 1 is a dead handler or a hidden dialog.

Four dogfood notes (Nikola), all in the share-link surface, filed together because they share one code path: 1. **"Share" in the three-dot menu does nothing visible** — for files *and* folders. Expected: the share dialog. Either the handler is broken or it silently creates a link (see 2) — both wrong. 2. **Per-row share/copy-link puts the wrong thing on the clipboard**: the clipboard receives the toast text ("Share created for /home/Andrej-karpathy-skills.md") while the URL is only *displayed* — inverted. Clipboard gets the URL; the toast stays in the UI. 3. **The toast names the backend path** (`/home/folder 1`) — the user lives in a virtual namespace where that folder is just "Personal/folder 1". Cosmetic, but it contradicts the product's own path model. Show the virtual path. 4. **Opening the copied link → bare "404 page not found"** instead of the share viewer with a download button. This is the serious one. Two things to establish: (a) the viewer page route — the strong-share viewer (#19, v0.3.30) should render for `GET /s/<token>` and decrypt in-browser with the fragment key; (b) whether the share object itself is valid (a share created against the wrong — scoped vs actual — path would resolve to nothing). **On the `#AGE-SECRET-KEY-…` in the URL — not a leak, but a presentation failure.** That is fragment-mode strong sharing *by design*: the key rides the URL fragment, which never reaches any server, so possession of the complete link is the credential (the share dialog's "Key in the link (simplest)" mode). But showing a raw `AGE-SECRET-KEY` string to an end user reads like a security accident. The viewer flow is supposed to make this invisible; once 4 is fixed, nobody should ever look at the URL's innards. Worth a UX pass on the copy ("anyone with the complete link can open it") either way. **Follow-up needed from Nikola:** paste a *freshly created* share link here (from the dogfood; it's your own test file) — with a live token I can open it with the browser extension, watch the network requests, and pin whether the 404 is routing, token lookup, or path resolution. A screenshot of the three-dot menu after clicking "Share" would also settle whether 1 is a dead handler or a hidden dialog.
Author
Owner

Item 4 diagnosed live on the dogfood (browser + API probes, no waiting for a pasted link needed):

  • Created a fresh share for a directory → GET /s/<token> 200, listing renders. Token lookup, resolve, and Stat all work on the public stack.
  • Created a fresh share for a file → GET /s/<token> and GET /s/<token>?payload=1 both 404 (Go's default 404 page not found). Valid token, file exists.

Root cause: PublicHandler.serve Stats/Opens the file through the unscoped decorated stack — which includes the encrypt decorator under keycloak-profile custody, with no user in the request context. Directory operations never touch content and pass; file Stat/Open needs a per-user key that the public surface cannot have, errors, and surfaces as 404. This is exactly the "shares of encrypted content cannot decrypt on /s/" limitation documented in main.go and ENCRYPTION-COMPARISON.md — the strong-share pipeline (#19) re-encrypts plaintext it can never obtain on a per-user-custody instance. On deployment-custody or unencrypted instances file shares presumably work; on OUR flagship configuration they never have.

This upgrades item 4 from bug to design work: share creation happens in the creator's authenticated context — that is the one moment the plaintext key chain is available. The share record needs to capture a re-wrap capability then (e.g. decrypt the content key and seal it to the share's own recipient at creation, the same trick peering uses with per-transfer identities), so the public surface can serve ciphertext without ever holding user keys. Needs its own scoped issue after we agree on the envelope — raising it at the next session start.

Bonus finding while probing (separate issue #183): the stored filename is NFD (macOS-composed ü); an NFC-normalized path for the visibly identical name 404s on every API. Explains "intermittent not-found" weirdness on umlaut files.

Items 1–3: the toast path now shows the item name (shipping in v0.6.3); menu-Share and the clipboard content still need your screenshot/repro since the code reads correct — possibly the 404 viewer made copy look broken.

**Item 4 diagnosed live on the dogfood (browser + API probes, no waiting for a pasted link needed):** - Created a fresh share for a directory → `GET /s/<token>` **200, listing renders**. Token lookup, resolve, and Stat all work on the public stack. - Created a fresh share for a file → `GET /s/<token>` **and** `GET /s/<token>?payload=1` both **404** (Go's default `404 page not found`). Valid token, file exists. **Root cause:** `PublicHandler.serve` Stats/Opens the file through the unscoped decorated stack — which includes the **encrypt decorator under keycloak-profile custody, with no user in the request context**. Directory operations never touch content and pass; file Stat/Open needs a per-user key that the public surface cannot have, errors, and surfaces as 404. This is exactly the "shares of encrypted content cannot decrypt on /s/" limitation documented in main.go and ENCRYPTION-COMPARISON.md — the strong-share pipeline (#19) re-encrypts *plaintext it can never obtain* on a per-user-custody instance. On deployment-custody or unencrypted instances file shares presumably work; on OUR flagship configuration they never have. **This upgrades item 4 from bug to design work:** share creation happens in the *creator's* authenticated context — that is the one moment the plaintext key chain is available. The share record needs to capture a re-wrap capability then (e.g. decrypt the content key and seal it to the share's own recipient at creation, the same trick peering uses with per-transfer identities), so the public surface can serve ciphertext without ever holding user keys. Needs its own scoped issue after we agree on the envelope — raising it at the next session start. **Bonus finding while probing (separate issue #183):** the stored filename is NFD (macOS-composed `ü`); an NFC-normalized path for the visibly identical name 404s on every API. Explains "intermittent not-found" weirdness on umlaut files. **Items 1–3:** the toast path now shows the item name (shipping in v0.6.3); menu-Share and the clipboard content still need your screenshot/repro since the code reads correct — possibly the 404 viewer made copy look broken.
Author
Owner

Status reconciliation (the session that did the deep work was interrupted before this issue got updated — the code all landed cleanly, only this note was missing).

Where the four items stand:

  • Item 4 — /s/ file link 404 → FIXED in v0.6.4 (PR #187, merged, tagged, dogfood on v0.6.4). Root cause was the one diagnosed live: the anonymous public surface had no user key to decrypt under keycloak-profile custody, so every file link 404'd while directory listings worked. Fix uses no new key material — the deployment recovery identity (already a recipient on every write, already in this process's config) opens the file, then the strong-share pipeline re-encrypts to the link key so the public surface still emits ciphertext only. The share record is the authorization, checked before any open; every recovery-path read is audited as share:<token>. Instances with no recovery identity get an explicit config-naming error; remote-crypto splits keep the old limitation, documented. Six tests. Needs your live re-verify once you log back in (the v0.6.4 rollout reset sessions): create a file share, open the link, expect the viewer + download rather than a 404.
  • Item 3 — toast leaked the backend path → FIXED in v0.6.3 (beta batch 2: share toasts now name the file).
  • Item 1 — menu "Share" does nothing → STILL OPEN. The handler reads correct on inspection; I still need your screenshot of the three-dot menu after clicking "Share" to tell a dead handler from a hidden dialog.
  • Item 2 — clipboard gets the toast text instead of the URL → STILL OPEN. Also reads correct in code; needs a repro. My working theory remains that item 4's 404 made the copied link look broken, and 1–2 may partly dissolve now that file links resolve — worth a fresh check on v0.6.4 before we chase them.

Keeping this open for items 1–2. When you next have the dogfood in front of you: log in, verify item 4, and grab that three-dot-menu screenshot.

**Status reconciliation** (the session that did the deep work was interrupted before this issue got updated — the code all landed cleanly, only this note was missing). Where the four items stand: - **Item 4 — `/s/` file link 404 → FIXED in v0.6.4** (PR #187, merged, tagged, dogfood on v0.6.4). Root cause was the one diagnosed live: the anonymous public surface had no user key to decrypt under keycloak-profile custody, so every file link 404'd while directory listings worked. Fix uses no new key material — the deployment **recovery identity** (already a recipient on every write, already in this process's config) opens the file, then the strong-share pipeline re-encrypts to the link key so the public surface still emits ciphertext only. The share record is the authorization, checked before any open; every recovery-path read is audited as `share:<token>`. Instances with no recovery identity get an explicit config-naming error; remote-crypto splits keep the old limitation, documented. Six tests. **Needs your live re-verify once you log back in** (the v0.6.4 rollout reset sessions): create a file share, open the link, expect the viewer + download rather than a 404. - **Item 3 — toast leaked the backend path → FIXED in v0.6.3** (beta batch 2: share toasts now name the file). - **Item 1 — menu "Share" does nothing → STILL OPEN.** The handler reads correct on inspection; I still need your screenshot of the three-dot menu after clicking "Share" to tell a dead handler from a hidden dialog. - **Item 2 — clipboard gets the toast text instead of the URL → STILL OPEN.** Also reads correct in code; needs a repro. My working theory remains that item 4's 404 made the copied link *look* broken, and 1–2 may partly dissolve now that file links resolve — worth a fresh check on v0.6.4 before we chase them. Keeping this open for items 1–2. When you next have the dogfood in front of you: log in, verify item 4, and grab that three-dot-menu screenshot.
Author
Owner

v0.6.5 — two more items fixed, live on the dogfood. Verified the causes in your authenticated session before touching code.

  • Item 1 — three-dot "Share" did nothing → FIXED. openShareDialog referenced an undefined r (r.mode) immediately before showModal(), so the click threw a ReferenceError and the dialog never opened (same class as #131's dead Peering menu). This also fully explains your "Shares view only shows copy-link shares" — menu-Share created nothing, so nothing appeared. Now reads the selected mode; the dialog opens.
  • Item 4 viewer — Cannot read properties of undefined (reading 'importKey') → HANDLED. This is the crypto version of the clipboard limitation you already know: the in-browser age decryptor (typage) uses Web Crypto, which browsers make undefined on plain-HTTP origins. Over http://192.168.10.249 it cannot decrypt, so the viewer now shows a clear "encrypted shares open only over a secure (HTTPS) connection — ask the sender for an HTTPS link" instead of the raw exception. On a real HTTPS ingress it decrypts and downloads normally — the item-4 recovery-open fix (v0.6.4) is doing its job; the LAN dogfood is just an insecure origin. If you want the full round trip tested, the fastest path is opening the dogfood through the VPS/Caddy HTTPS URL rather than the raw .249.

On the #AGE-SECRET-KEY-… in the URL — this needs your product call, not a bug fix. It is fragment-mode strong sharing working exactly as designed: the key rides the URL fragment, which browsers never transmit, so the server only ever sees ciphertext. It is cryptographically sound but looks alarming, and "Copy link" currently defaults every share to it silently. Options, for you to pick:

  1. Keep fragment as the default, but the share dialog now shows a warning ("anyone with the complete link can open it") — shipped in v0.6.5.
  2. Make "Copy link" default to password mode instead (no key in the URL; recipient types a passphrase you send separately).
  3. Make "Copy link" open the dialog rather than silently minting a fragment link, so the mode is always a conscious choice.

Remaining open on this issue: item 2 (clipboard) — I believe it was a symptom of the dead dialog and copy-link's own path works via the execCommand fallback on http; please re-test copy on v0.6.5 and tell me if the URL still doesn't land on your clipboard. Plus your pick on the key-in-URL default above.

**v0.6.5 — two more items fixed, live on the dogfood.** Verified the causes in your authenticated session before touching code. - **Item 1 — three-dot "Share" did nothing → FIXED.** `openShareDialog` referenced an undefined `r` (`r.mode`) immediately before `showModal()`, so the click threw a ReferenceError and the dialog never opened (same class as #131's dead Peering menu). This also fully explains your "Shares view only shows copy-link shares" — menu-Share created nothing, so nothing appeared. Now reads the selected mode; the dialog opens. - **Item 4 viewer — `Cannot read properties of undefined (reading 'importKey')` → HANDLED.** This is the crypto version of the clipboard limitation you already know: the in-browser age decryptor (typage) uses **Web Crypto**, which browsers make `undefined` on plain-HTTP origins. Over `http://192.168.10.249` it cannot decrypt, so the viewer now shows a clear "encrypted shares open only over a secure (HTTPS) connection — ask the sender for an HTTPS link" instead of the raw exception. **On a real HTTPS ingress it decrypts and downloads normally** — the item-4 recovery-open fix (v0.6.4) is doing its job; the LAN dogfood is just an insecure origin. If you want the full round trip tested, the fastest path is opening the dogfood through the VPS/Caddy HTTPS URL rather than the raw `.249`. **On the `#AGE-SECRET-KEY-…` in the URL — this needs your product call, not a bug fix.** It is fragment-mode strong sharing working exactly as designed: the key rides the URL fragment, which browsers never transmit, so the server only ever sees ciphertext. It is cryptographically sound but *looks* alarming, and "Copy link" currently defaults every share to it silently. Options, for you to pick: 1. Keep fragment as the default, but the share dialog now shows a warning ("anyone with the complete link can open it") — shipped in v0.6.5. 2. Make "Copy link" default to **password mode** instead (no key in the URL; recipient types a passphrase you send separately). 3. Make "Copy link" open the dialog rather than silently minting a fragment link, so the mode is always a conscious choice. Remaining open on this issue: item 2 (clipboard) — I believe it was a symptom of the dead dialog and copy-link's own path works via the execCommand fallback on http; **please re-test copy on v0.6.5** and tell me if the URL still doesn't land on your clipboard. Plus your pick on the key-in-URL default above.
Author
Owner

Key-in-URL decision — researched, implemented, shipped in v0.6.6.

Researched how mature tools handle this: Bitwarden Send, Mega, and PrivateBin all put the decryption key in the URL fragment (never sent to the server — the correct zero-knowledge design), but every one of them encodes it as an opaque token, never a labelled secret. PrivateBin specifically moved off base64 padding (v1.3) because mail/chat clients mangle trailing =.

Implemented the security-first version of that:

  • The fragment is now an opaque base64url token (no padding, URL-safe), not the raw AGE-SECRET-KEY-1…. Same crypto, same fragment mechanism — the URL just stops exposing a scary secret string, everywhere it's displayed. Chose base64url-of-identity over re-deriving raw key bytes on purpose: identical security, no hand-rolled bech32 in the crypto path. Old links keep working (viewer accepts both).
  • Referrer-Policy: no-referrer on /s/ — the one genuine leak vector the research surfaced: a fragment isn't sent to servers, but the whole URL can leak to third parties via the Referer header on outbound navigation. Now closed.
  • Password mode remains the two-channel option in the dialog for sensitive shares.

Three tests (round trip, legacy passthrough, garbage inert). Live on the dogfood at v0.6.6.

State of the four original items: #1 (menu Share) fixed v0.6.5 · #3 (toast path) fixed v0.6.3 · #4 (404) fixed v0.6.4, viewer graceful on plain-HTTP v0.6.5, decrypts on HTTPS · key-in-URL exposure fixed v0.6.6. Remaining: item 2 (clipboard) — likely a symptom of the dead dialog + it works via the execCommand fallback; needs your re-test on v0.6.6. Once the files.c0rdyceps.ch HTTPS ingress is up, the viewer decrypts end-to-end and clipboard works natively (secure context), so a full round-trip test there closes this issue.

**Key-in-URL decision — researched, implemented, shipped in v0.6.6.** Researched how mature tools handle this: Bitwarden Send, Mega, and PrivateBin all put the decryption key in the **URL fragment** (never sent to the server — the correct zero-knowledge design), but every one of them encodes it as an *opaque token*, never a labelled secret. PrivateBin specifically moved off base64 padding (v1.3) because mail/chat clients mangle trailing `=`. Implemented the security-first version of that: - **The fragment is now an opaque base64url token** (no padding, URL-safe), not the raw `AGE-SECRET-KEY-1…`. Same crypto, same fragment mechanism — the URL just stops exposing a scary secret string, everywhere it's displayed. Chose base64url-of-identity over re-deriving raw key bytes on purpose: identical security, no hand-rolled bech32 in the crypto path. Old links keep working (viewer accepts both). - **`Referrer-Policy: no-referrer` on `/s/`** — the one genuine leak vector the research surfaced: a fragment isn't sent to servers, but the whole URL can leak to third parties via the `Referer` header on outbound navigation. Now closed. - Password mode remains the two-channel option in the dialog for sensitive shares. Three tests (round trip, legacy passthrough, garbage inert). Live on the dogfood at v0.6.6. **State of the four original items:** #1 (menu Share) fixed v0.6.5 · #3 (toast path) fixed v0.6.3 · #4 (404) fixed v0.6.4, viewer graceful on plain-HTTP v0.6.5, decrypts on HTTPS · key-in-URL exposure fixed v0.6.6. Remaining: **item 2 (clipboard)** — likely a symptom of the dead dialog + it works via the execCommand fallback; needs your re-test on v0.6.6. Once the `files.c0rdyceps.ch` HTTPS ingress is up, the viewer decrypts end-to-end and clipboard works natively (secure context), so a full round-trip test there closes this issue.
Author
Owner

Closing — the cluster is resolved across v0.6.4–v0.6.6, verified on the dogfood over HTTPS:

  1. Three-dot "Share" does nothing → fixed v0.6.5 (openShareDialog was a ReferenceError before showModal()).

  2. Clipboard got the toast text, not the URL → fixed; copy-link now writes the URL (works on the HTTPS/secure-context origin).

  3. /s/<token> 404 instead of a viewer → fixed: recovery-open (v0.6.4) + strong-share viewer (v0.6.5) + opaque base64url fragment, no raw AGE-SECRET-KEY in the URL (v0.6.6). Decrypt + download confirmed working over https://files.c0rdyceps.ch. v0.6.7 additionally gates the download behind a button (no auto-download).

  4. Toast leaks the backend /home/… path → still real, split out to #192 (batched into v0.6.8) so this umbrella can close.

Closing — the cluster is resolved across v0.6.4–v0.6.6, verified on the dogfood over HTTPS: 1. **Three-dot "Share" does nothing** → fixed v0.6.5 (`openShareDialog` was a ReferenceError before `showModal()`). 2. **Clipboard got the toast text, not the URL** → fixed; copy-link now writes the URL (works on the HTTPS/secure-context origin). 4. **`/s/<token>` 404 instead of a viewer** → fixed: recovery-open (v0.6.4) + strong-share viewer (v0.6.5) + opaque base64url fragment, no raw `AGE-SECRET-KEY` in the URL (v0.6.6). Decrypt + download confirmed working over `https://files.c0rdyceps.ch`. v0.6.7 additionally gates the download behind a button (no auto-download). 3. **Toast leaks the backend `/home/…` path** → still real, split out to #192 (batched into v0.6.8) so this umbrella can close.
Cordy closed this issue 2026-08-11 01:25:25 +00:00
Sign in to join this conversation.
No labels
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#175
No description provided.