Gone

Gone v3 Roadmap

Gone v3 adds abuse protection, a second factor for secrets, sender control, a command-line client, and a refreshed web interface. The core promise does not change: the server never sees plaintext or keys, and each secret is delivered to one recipient at most.

Goals

  • Rate limiting to make floods and ID guessing expensive.
  • Optional passphrase so a leaked link alone cannot open a secret.
  • Sender status and revoke: the sender can check whether a secret has been opened and destroy it before it is (manual, no webhooks).
  • CLI to send and receive secrets from a terminal and in scripts.
  • Web UI revamp so the new features feel like one product rather than add-ons.

Non-goals

  • Accounts, login, teams, or admin dashboards.
  • Webhooks, email, or other push notifications.
  • Secrets that can be opened more than once.
  • Front-end frameworks or a build-time JS toolchain. The UI stays vanilla JS under a strict CSP.
  • Running WebAssembly in the browser. Encryption stays on WebCrypto (see Phase 2).

Features we have decided against are in Rejected features; technical alternatives we turned down are in Technical decisions. Each lists the reason and what would make us look again.

Guiding principles

  • One phase, one pull request. Each phase is reviewable, passes Codacy on its own, and leaves main releasable.
  • Zero knowledge stays intact. No phase may give the server a key, a passphrase, or plaintext.
  • Compatible upgrades. A v3 server still opens every v1 link created before the upgrade. Schema changes migrate automatically on startup.
  • Test coverage stays high. Go and JS stay at or near 100% line coverage, and each phase adds tests for its new code.
  • Minimal dependencies. Prefer the Go standard library and WebCrypto. Any new dependency needs a written reason in its PR.

Phase order

Mermaid
graph LR
  P1[1. Rate limiting]
  P2[2. Protocol spec + Go envelope]
  P3[3. Web UI revamp]
  P4[4. Sender status + revoke]
  P5[5. Optional passphrase]
  P6[6. CLI]
  R[v3.0.0]
  P2 --> P5
  P3 --> P4
  P3 --> P5
  P2 --> P6
  P4 --> P6
  P5 --> P6
  P1 --> R
  P6 --> R

Phases 1 and 2 are independent and can be worked on at the same time. The UI revamp comes before the two features with new screens, so those screens are built once, in the new design. The CLI comes last because it uses the API and encryption format the earlier phases settle.


Phase 1: Rate limiting

Status: done (merged in #26).

Server-side only; the browser already shows a message on 429.

Scope

  • Per-client token buckets in internal/httpx middleware, written with the standard library.
  • Separate budgets for creating secrets and for reading them. Reads include claims, acknowledgements, and Phase 4’s status and revoke calls. Health probes and static assets are not limited.
  • Requests over the limit get 429 Too Many Requests with a Retry-After header and the usual JSON error body.
  • The client address comes from the connection by default. X-Forwarded-For is trusted only when the request arrives from an address listed in GONE_TRUSTED_PROXIES.
  • Idle buckets are removed on a timer, so memory stays bounded under address churn.
  • New rate_limited_create_total and rate_limited_read_total metrics counters. Client addresses are never logged.

Configuration

VariablePurpose
GONE_RATE_CREATECreate budget (default 10/m)
GONE_RATE_READClaim, acknowledge, status, and revoke budget (default 30/m)
GONE_RATE_BURSTBurst allowance per bucket (default 10)
GONE_TRUSTED_PROXIESCIDRs whose X-Forwarded-For header is trusted

A budget of 0 turns that limit off.

Done when

  • Middleware tests cover the bucket, header handling, proxy trust, and cleanup.
  • docs/openapi.yaml documents 429 on the limited endpoints.
  • The README configuration table and threat model are updated (“Brute force ID enumeration” and “DoS floods” move from out of scope to partially defended).

Phase 2: Protocol spec and Go envelope

Status: done (merged in #27).

No user-visible change. This phase writes down the encryption format and adds a Go copy of it, so the passphrase and CLI phases have a fixed reference to build on.

Scope

  • docs/protocol.md: a precise v1 specification. It covers key and nonce generation, the AAD (gone:v1), the GONE2 attachment envelope byte layout, the fragment format (#v1:<base64url-key>), and the claim/acknowledge exchange.
  • internal/envelope (Go standard library only): encrypt, decrypt, and pack/unpack the GONE2 envelope.
  • Shared test data in test/vectors/*.json, checked by both go test and the node:test suite. If the browser and Go code disagree on a single byte, a test fails.
  • Decide now how format versions are numbered, so Phase 5 does not have to.

Why not WASM: compiling the Go code to WASM for the browser would add about 2 MB to page loads, require loosening the CSP with 'wasm-unsafe-eval', give up WebCrypto’s hardware-accelerated AES-GCM, and leave secrets in a garbage-collected heap that cannot be wiped. Two small implementations checked against the same test data are safer and lighter.

Done when

  • Go and JS both pass every shared test case.
  • docs/protocol.md is linked from the README and the API guide (now docs/api.md).

Phase 3: Web UI revamp

Status: done (merged in #29). The visual system is documented in Design system and the product context in PRODUCT.md.

A full redesign that keeps today’s constraints: vanilla JS, no innerHTML, strict CSP, and no third-party assets.

Scope

  • Design tokens. One set of CSS custom properties for color, spacing, type, radius, and motion, for both light and dark themes. The stylesheet was split around them into web/css/ (tokens, base, layout, components, pages, passphrase).
  • Reusable building blocks: form fields, buttons, a copyable-link card, status badges, and alert/notice styles. Phases 4 and 5 use these without adding new patterns.
  • Rethought flows:
    • Send: message, attachments, expiry, and a slot for the passphrase option.
    • Result: the share link plus a slot for the management link.
    • Receive: reveal, a slot for the passphrase prompt, and the error states (expired, already opened, wrong passphrase).
  • Accessibility: target WCAG 2.2 AA. That means full keyboard operation, visible focus, correct labels and live regions, prefers-reduced-motion support, and sufficient contrast in both themes.
  • Responsive layout that works from small phones to wide desktops.
  • Browser smoke tests: a Playwright check of the send→receive round trip in CI. Done later (#81): test/e2e runs every flow in Chromium, Firefox and WebKit against the production build and its real CSP (task e2e).

Done when

  • Every existing flow works with the same or fewer steps.
  • JS line coverage stays at 100%, and stylelint and eslint pass.
  • Before/after screenshots are attached to the PR.

Phase 4: Sender status and revoke

Status: done (merged in #30).

The sender can check whether a secret is still waiting and delete it before it is opened.

Scope

  • Manage token. Creating a secret also returns a random 256-bit manage_token (43-character base64url). The server stores only its SHA-256 hash in a new manage_hash column and compares it in constant time.

  • Manage link: /manage/{id}#<manage_token>. The token lives in the URL fragment, so it never reaches server logs or Referer headers. The page validates the link before making any request.

  • API. Both calls send the token in an X-Gone-Manage header and share the GONE_RATE_READ budget.

    MethodPathPurpose
    GET/api/secret/{id}/status200 {state: "pending", created_at, expires_at} while the secret is waiting
    POST/api/secret/{id}/revoke204; deletes the secret right away
  • No tombstones. Opened, revoked, expired, unknown, and wrong-token secrets all return the same 404. Nothing about a secret is kept once it is gone, so status can only ever say “still waiting” or “gone”.

  • Claim leases. A secret under an active claim lease still reports pending. Revoke always wins: it deletes the secret mid-lease, and the recipient’s acknowledgement then fails with 404.

  • Schema migrations. A PRAGMA user_version migration runner. Each step runs in a BEGIN IMMEDIATE transaction with its version bump. Version 2 adds manage_hash. Rows created before it have no hash, so status and revoke on them return 404.

  • UI. A “Manage this secret” disclosure on the result card holds the manage link. The /manage/{id} page checks status once on load, offers “Check again”, and asks for confirmation before deleting.

Decisions (formerly open questions)

  • Tombstones were rejected. Keeping a row after a secret is gone would leak “this ID existed” and when it was opened. A uniform 404 is simpler and leaks nothing.
  • Status does not expose the claim lease. “Pending” means not yet acknowledged.

Done when

  • Store, service, and handler tests cover each state change, including a revoke that races with a claim.
  • docs/openapi.yaml, docs/protocol.md, and the API guide (now docs/api.md) are updated, and rate limits apply to the new endpoints.

Phase 5: Optional passphrase

Status: done (merged in #31). The byte-level format is Protocol specification §4.2.

A second factor: opening the secret needs both the link and a passphrase that the sender shares separately.

Scope (as built)

  • Key derivation. The browser derives a key from the passphrase with PBKDF2-SHA-256 (built into WebCrypto), using a random 16-byte salt and 600,000 iterations, the current OWASP guidance. HKDF combines it with the link key to make the AES-GCM key, so the link alone and the passphrase alone are each useless. Passphrases are normalized to NFC before hashing, so the same words typed on different devices give the same key.
  • Protocol v2. A 21-byte authenticated header (KDF ID, iteration count, salt) sits in front of the ciphertext, and the AAD becomes gone:v2 plus that header. Readers accept 600,000 to 5,000,000 iterations, which caps the work a hostile link can force on a recipient. The server stores v2 bodies as opaque bytes and never parses the header. v1 links keep working.
  • Shared test data. aead_v2.json and fragment_v2.json cover NFC, the KDF chain, a wrong passphrase, a wrong link key, tampered headers, out-of-range iterations, and invalid passphrases. Go (SealV2/OpenV2) and JS both pass them.
  • Sender flow. “Add a passphrase” is an optional disclosure on the send form. It requires at least 8 characters, shows a strength hint, and offers a Generate button that makes five random words from the EFF short wordlist (about 51 bits). The result page reminds the sender to share the passphrase through a different channel than the link and never shows the passphrase again.
  • Recipient flow. A v2 link shows a passphrase field before anything is fetched. Opening claims and downloads the ciphertext once; a wrong passphrase can be retried against that download as many times as needed, with no second request. The secret is acknowledged (and deleted) only after a successful decrypt. Leaving the page erases the download.

Decisions

  • PBKDF2, not Argon2id. Argon2id resists guessing better, but would need WASM and a CSP exception. KDF ID 0x01 is PBKDF2-SHA-256; a later ID can add Argon2id within v2 (see protocol.md §10).
  • No retry limit. Anyone holding the downloaded ciphertext can guess offline anyway, so a client-side cap would add friction without adding security.
  • One error for both factors. A wrong link key and a wrong passphrase look the same; the format cannot tell them apart.

Threat model note. The passphrase protects against a link that leaks before the real recipient opens it. Someone who claims the secret with a leaked link can still guess the passphrase offline, limited only by its strength and PBKDF2’s cost. They also burn the secret, so the real recipient finds it already opened, which is itself a warning. The README says this plainly.


Phase 6: CLI

Status: done (merged in #32). The user guide is The gone CLI.

A single static binary, gone, that sends and receives secrets and talks to the same API as the browser. The server binary is now goned.

Scope (as built)

  • Binaries. cmd/gone is the CLI and cmd/goned is the server (renamed from cmd/gone; the container entrypoint follows). The CLI imports no server code. It is built from internal/envelope, a small HTTP client in internal/client, the shared wordlist in internal/passgen, the standard library, and x/term.
  • Commands.
    • gone send reads the message only from standard input, takes up to 10 --file attachments, --ttl, and one of --passphrase-prompt, --passphrase-file, or --passphrase-generate. It prints the share link, the manage link, and the expiry.
    • gone get <link> writes the message to standard output and attachments to --out. It acknowledges the secret only after everything is decrypted and written. A passphrase prompt allows three tries against a single download.
    • gone status <manage-link>, gone revoke <manage-link>, gone set server <url>, gone version, and gone help.
  • Server URL. --server, then GONE_SERVER, then gone set server (a 0600 file under the user config directory), then https://app.getgone.dev. get, status, and revoke use the link’s origin. Plain HTTP is refused except for a localhost server with --insecure; redirects are refused and responses are size-capped.
  • Script-friendly output. --json on every command, with errors as JSON on standard error. Exit codes are fixed per outcome: usage 2, not found 3, wrong passphrase 4, rate limited 5, integrity 6, network or server 7, local I/O 8.
  • Safe saving. Attachments are created with O_EXCL and mode 0600 inside an os.Root, so they never overwrite a file or escape the output directory. A collision becomes name (1).ext. Names get the protocol’s sanitization plus a Windows-aware save policy. On a terminal, control and bidi characters in messages and printed names are escaped (--raw opts out).
  • Secret hygiene. Messages and passphrases never come from arguments. Keys, passphrases, and plaintext buffers are wiped where Go allows it.
  • Tests. Unit tests for internal/cli, internal/client, and internal/passgen. Integration tests in test/ run the real CLI against an in-process server. Interop tests run the browser’s crypto modules in Node (test/interop/interop.js) to exchange v1 and v2 secrets (and, since v3.7.0, v3 request replies) with the CLI in both directions; CI requires Node for them (GONE_REQUIRE_NODE=1).
  • Releases. On a v* tag, the build workflow runs the tests, cross-builds gone for linux, darwin, and windows on amd64 and arm64 and goned for linux (scripts/release.sh, also task release), attests build provenance with actions/attest, and publishes the archives, SHA256SUMS, and install notes to the GitHub release. The container image is still published by the release workflow.

Decisions (formerly open questions)

  • A separate binary. gone is the client and goned the server, so the client has no server code or web assets, and the user-facing name is the short one.
  • No message argument. Reading only from standard input keeps messages out of shell history and the process list. Links are still arguments; The gone CLI explains the trade-off.
  • No --force. get never overwrites; it picks a free name instead, which is safer and just as scriptable.
  • A three-try prompt. The web UI allows unlimited retries because guessing is offline anyway. The CLI caps prompts at three for usability; --passphrase-file gets one.

Done when

  • CLI and browser can exchange secrets in both directions, covered by integration tests in test/.
  • The README has a CLI section, and the release notes include install instructions.

Release plan

  • Each phase merged to main on its own. No pre-release tags were cut.
  • v3.0.0 was released on 2026-10-04, after all six phases landed.
  • Upgrade notes should cover:
    • the new environment variables;
    • the automatic schema migration;
    • the guarantee that v1 links created before the upgrade still open.

After v3.0.0

Work after the six phases. Each item is still one PR, under the same guiding principles.

ReleaseChange
v3.2.0Ordered write gate: SQLite contention answers 503 busy instead of 500 (#44). go-playground/validator dropped (#43).
v3.3.0Moonstone redesign and the G brand mark (#45, #46).
v3.4.0goned healthcheck and a Docker HEALTHCHECK (#49). The opened secret stays covered until Show, for screen sharing (#48).
v3.5.0HTTPS required: sending and opening are turned off on insecure pages, and the CLI refuses http:// except to localhost (#52).
v3.6.0Request a secret (protocol v3) in the web UI (#60). Agent-friendly CLI: send --message-file, get --message-out and - for links on standard input, so scripts and AI agents move secrets by reference. Error output never contains plaintext, keys, tokens, passphrases or secret IDs, and a test enforces it (#59).
v3.7.0Requests in the CLI: gone request, request list/open/cancel and gone reply (#61).
v3.8.0Opt-in QR code for the share link (#63).
v3.9.0Prometheus /metrics on the token-protected metrics listener (#64).
v3.10.0Translations (English, Spanish, French, German, Brazilian Portuguese) with a live language picker (#66).
v3.10.2Every source file split back to a Codacy A grade; no behavior change (#74).
v3.12.0Operator controls: a passphrase can be required on every secret and attachments limited or turned off (#96); an instance can carry its operator’s name, logo, accent colour and contact, imprint and privacy links, with “Powered by Gone” kept (#98); every page tells search engines not to index it (#99); and a private instance can require sign-in through the operator’s proxy to create secrets and requests, while opening and replying stay open (#100).
v3.11.0Security review release: 28 findings fixed across the store, web client, CLI, server and CI (#86–#93); browser end-to-end tests in CI, now over TLS (#81, #93); documentation split into topic pages under docs/.

Decided against since v3.0.0:

  • gone mcp, a stdio MCP server over the CLI. Coding assistants have a shell and already use the agent-friendly CLI. Listed in Rejected features.
  • Sending from Slack or Teams and emailing the link for the sender. The first passes the secret through the chat service before it is encrypted; the second needs Gone to keep email addresses. Both listed in Rejected features.
  • Argon2id, reconfirmed on 2026-10-16. It still needs WebAssembly in the browser; see Phase 5’s decisions and Technical decisions.

Tracking

PhaseStatus
1. Rate limitingDone
2. Protocol spec + Go envelopeDone
3. Web UI revampDone
4. Sender status + revokeDone
5. Optional passphraseDone
6. CLIDone
Request a secretDone (v3.6.0 web, v3.7.0 CLI)
Browser end-to-end testsDone (#81; over TLS with rate limits on since v3.11.0, #93)
Security reviewDone (v3.11.0, #86–#94): 28 findings fixed; release tags restricted to the maintainer and signed
Operator policiesDone (v3.12.0, #96): GONE_REQUIRE_PASSPHRASE enforced by the server; GONE_MAX_FILES honoured by the web UI and CLI
Operator brandingDone (v3.12.0, #98): name, logo, accent colour and footer links; “Powered by Gone” stays
Search enginesDone (v3.12.0, #99): X-Robots-Tag: noindex on every response
Sign-in for private instancesDone (v3.12.0, #100): signed tokens from Cloudflare Access, Google IAP, Pomerium, Teleport and OIDC; trusted headers from Authelia, authentik, Tailscale and custom; no identity kept. OIDC, Authelia and custom are tested end to end; the rest are built from vendor docs and not yet tested live
gone mcpRejected