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
mainreleasable. - 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
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 --> RPhases 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/httpxmiddleware, 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 Requestswith aRetry-Afterheader and the usual JSON error body. - The client address comes from the connection by default.
X-Forwarded-Foris trusted only when the request arrives from an address listed inGONE_TRUSTED_PROXIES. - Idle buckets are removed on a timer, so memory stays bounded under address churn.
- New
rate_limited_create_totalandrate_limited_read_totalmetrics counters. Client addresses are never logged.
Configuration
A budget of 0 turns that limit off.
Done when
- Middleware tests cover the bucket, header handling, proxy trust, and cleanup.
docs/openapi.yamldocuments429on 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), theGONE2attachment 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 theGONE2envelope.- Shared test data in
test/vectors/*.json, checked by bothgo testand thenode:testsuite. 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.mdis linked from the README and the API guide (nowdocs/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-motionsupport, 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/e2eruns 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 newmanage_hashcolumn 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 orRefererheaders. The page validates the link before making any request.API. Both calls send the token in an
X-Gone-Manageheader and share theGONE_RATE_READbudget.Method Path Purpose GET /api/secret/{id}/status200 {state: "pending",while the secret is waitingcreated_at, expires_at} POST /api/secret/{id}/revoke204; deletes the secret right awayNo 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_versionmigration runner. Each step runs in aBEGIN IMMEDIATEtransaction with its version bump. Version 2 addsmanage_hash. Rows created before it have no hash, so status and revoke on them return404.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
404is 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 (nowdocs/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:v2plus 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.jsonandfragment_v2.jsoncover 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
0x01is 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/goneis the CLI andcmd/gonedis the server (renamed fromcmd/gone; the container entrypoint follows). The CLI imports no server code. It is built frominternal/envelope, a small HTTP client ininternal/client, the shared wordlist ininternal/passgen, the standard library, andx/term. - Commands.
gone sendreads the message only from standard input, takes up to 10--fileattachments,--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, andgone help.
- Server URL.
--server, thenGONE_SERVER, thengone set server(a0600file under the user config directory), thenhttps://app.getgone.dev.get,status, andrevokeuse 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.
--jsonon 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_EXCLand mode0600inside anos.Root, so they never overwrite a file or escape the output directory. A collision becomesname (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 (--rawopts 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, andinternal/passgen. Integration tests intest/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-buildsgonefor linux, darwin, and windows on amd64 and arm64 andgonedfor linux (scripts/release.sh, alsotask release), attests build provenance withactions/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.
goneis the client andgonedthe 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.getnever 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-filegets 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
mainon its own. No pre-release tags were cut. v3.0.0was 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.
| Release | Change |
|---|---|
| v3.2.0 | Ordered write gate: SQLite contention answers 503 busy instead of 500 (#44). go-playground/validator dropped (#43). |
| v3.3.0 | Moonstone redesign and the G brand mark (#45, #46). |
| v3.4.0 | goned healthcheck and a Docker HEALTHCHECK (#49). The opened secret stays covered until Show, for screen sharing (#48). |
| v3.5.0 | HTTPS required: sending and opening are turned off on insecure pages, and the CLI refuses http:// except to localhost (#52). |
| v3.6.0 | Request 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.0 | Requests in the CLI: gone request, request list/open/cancel and gone reply (#61). |
| v3.8.0 | Opt-in QR code for the share link (#63). |
| v3.9.0 | Prometheus /metrics on the token-protected metrics listener (#64). |
| v3.10.0 | Translations (English, Spanish, French, German, Brazilian Portuguese) with a live language picker (#66). |
| v3.10.2 | Every source file split back to a Codacy A grade; no behavior change (#74). |
| v3.12.0 | Operator 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.0 | Security 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
| Phase | Status |
|---|---|
| 1. Rate limiting | Done |
| 2. Protocol spec + Go envelope | Done |
| 3. Web UI revamp | Done |
| 4. Sender status + revoke | Done |
| 5. Optional passphrase | Done |
| 6. CLI | Done |
| Request a secret | Done (v3.6.0 web, v3.7.0 CLI) |
| Browser end-to-end tests | Done (#81; over TLS with rate limits on since v3.11.0, #93) |
| Security review | Done (v3.11.0, #86–#94): 28 findings fixed; release tags restricted to the maintainer and signed |
| Operator policies | Done (v3.12.0, #96): GONE_REQUIRE_PASSPHRASE enforced by the server; GONE_MAX_FILES honoured by the web UI and CLI |
| Operator branding | Done (v3.12.0, #98): name, logo, accent colour and footer links; “Powered by Gone” stays |
| Search engines | Done (v3.12.0, #99): X-Robots-Tag: noindex on every response |
| Sign-in for private instances | Done (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 mcp | Rejected |