Skip to content

Sign upload URLs instead of putting the file token in them (GRYT-1549) - #724

Merged
sivert-io merged 2 commits into
mainfrom
claude/GRYT-1549-signed-file-urls
Sep 29, 2026
Merged

sivert-io merged 2 commits into
mainfrom
claude/GRYT-1549-signed-file-urls

Conversation

@sivert-io

@sivert-io sivert-io commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

GRYT-1549, the desktop half. getUploadsFileUrl put the file token in ?t=. Those <img> loads can't wait for the identity proof (#708), and the token was good for every upload you can read, for 12 hours. The findings and the options are written up in Gryt-chat/server#251.

What changes

  • common/src/utils/fileUrlAuth.ts holds each server's signing key in memory only and signs URLs with @noble/hashes (HMAC-SHA256). getUploadsFileUrl keeps its signature, so no caller changed. It adds u, k, e and s in place of t.
  • The key arrives with server:joined, token:refreshed and the new file:key, which a restored session gets. All three come after the proof, because the socket holds every emit until then. Before that, a URL carries nothing and gets a 401.
  • A refused server loses its key (markRefused now says whether the host counts as refused). A kick, removing a server, or signing out does too. An ordinary drop keeps it, since every reconnect gets a new key after its own proof.
  • URLs expire at the end of the next five-minute step. Anything that holds a URL longer signs it again when it's used: freshUploadsFileUrl in the download, Copy Image and avatar sync paths, and in the sidebar and profile settings, which keep your avatar's URL from the join. Attachments already re-sign on a load error through useStableFileUrl. Now it also signs a URL that was built before the key arrived.
  • Older servers. One that sends only fileToken still works through ?t=. That token is still kept on disk, but it's only read after that server proves itself (restoreServerFileToken in the guard's release). As soon as a server sends a key, its stored token is deleted. Signing out now clears fileToken_* too, which it never did.

What changes behaviour

  • A picture rendered before the key arrives gets a 401 and falls back, until it renders again. Attachments recover by themselves. Avatars recover when the member list comes in, and that happens after file:key.
  • A URL changes every five minutes instead of on every token refresh, so a picture that re-renders across a step loads again.

Checked

  • yarn lint and the comment check pass, and so does every yarn test:* step in CI.
  • New scripts/check-signed-file-urls.mjs: the server's vector, nothing before any key, 5 to 10 minutes, one file each, stable within a step, the server's clock rather than ours, the ?t= fallback, the stored token only after restoreServerFileToken, and sign-out.
  • check-http-proof-gate.mjs now runs the real fileUrlAuth and tokenStorage behind the real guard. A refused server loses its key, and a drop alone doesn't. A stored old-server token isn't used before the proof, and never for a server that fails it. Mutation-checked: without forgetServerFileAccess in refuse, or with the restore moved ahead of the proof, the check fails.
  • check-stable-attachment-urls.mjs: an attachment built with nothing gets signed once the key arrives, and a later key refresh doesn't reload it.
  • By hand against Sign file URLs per file instead of putting the file token in them (GRYT-1549) server#251 on :5003, in headless Chrome: after a reload (session restore) the avatar and thumbnail loaded from signed URLs, with no t=, no 401 and no fileToken_ left in storage.

Review-required: none. Nothing under src/packages/common/src/auth/** changed. fileUrlAuth.ts does hold a signing key, so I.d read it anyway.

The e2e job runs against server:latest, which sends no key yet, so it covers the ?t= fallback. Phone: Gryt-chat/mobile#281. Follow-up: GRYT-1586 drops the fallback.

🤖 Generated with Claude Code

Upload URLs are signed per file with a key the server hands over after
its identity proof, and the key is only held in memory. A refused server
loses it. An older server's file token still works, but it's only read
from disk once that server has proved itself.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@sivert-io
sivert-io merged commit 67763cb into main Sep 29, 2026
6 checks passed
@sivert-io
sivert-io deleted the claude/GRYT-1549-signed-file-urls branch September 29, 2026 11:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant