Paste a secret, get a link, send it. The first person to open it and press
Reveal sees the secret; the link dies at that moment. The recipient needs a
browser and nothing else — no account, no client, no installed tooling.
The server cannot read what it stores. AES-256-GCM happens in the browser and
the key lives in the URL fragment, which browsers never transmit, so hushd
holds ciphertext and no key material. That is a property of where the key sits
rather than a promise about our conduct, which is why there is deliberately no
endpoint accepting a plaintext secret and no server-side-encryption fallback:
two guarantees behind one URL would be worse than one honest guarantee.
Three decisions carry the design:
* GET /s/{id} touches NO storage, not even to check existence. Slack, Teams,
WhatsApp, iMessage and Outlook Safe Links all fetch a URL before a human
sees it, so destroying on GET would destroy most secrets in transit and the
recipient's "already used" would be indistinguishable from interception.
Only POST /reveal consumes. Bot user-agent detection is an arms race;
removing the side effect from GET is not. Pinned by
TestGettingTheRevealPageNeverConsumesTheSecret.
* Destruction is one Redis GETDEL, which is atomic. GET-then-DEL has a window
where two simultaneous readers both win, and for a one-time secret that
window is the product. The store contract demands atomicity and the same
concurrency test runs against both implementations.
* Missing, already-revealed, expired and evicted are ONE indistinguishable
410. Separating them would confirm to a prober that a given link was real.
The secret id IS the capability, so secret.ID is a struct whose every
accidental path — %v, %s, String(), slog, json.Marshal — emits a redacted
handle or refuses, and the raw value needs an explicit Value(). The first
version tried to prevent leaks by implementing no String() at all; its own test
caught that Go's fmt prints unexported fields anyway, so forbidding the method
had removed the control rather than the leak.
Operationally: structured JSON on stdout in the fleet's wire format, which
Vector already collects with no annotation; six hush_* metrics on the chassis
registry with no id, IP or path in any label; five alert rules wired into
vmalert. The public Ingress enumerates /, /s/ and /api/ so /metrics, /healthz
and /readyz share the port but are unreachable from the internet — no
basic-auth middleware to maintain and get wrong.
Dependencies are vendored because go-chassis is private: the Woodpecker test
step and the in-cluster Kaniko build both run -mod=vendor with GOPROXY=off and
hold no git credential.
cmd/hush-mcp is a stdio MCP server doing the same client-side crypto locally,
so using hush from an agent preserves the same guarantee as using it from a
browser.
92 lines
3.6 KiB
Plaintext
92 lines
3.6 KiB
Plaintext
# github.com/beorn7/perks v1.0.1
|
|
## explicit; go 1.11
|
|
github.com/beorn7/perks/quantile
|
|
# github.com/cespare/xxhash/v2 v2.3.0
|
|
## explicit; go 1.11
|
|
github.com/cespare/xxhash/v2
|
|
# github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822
|
|
## explicit
|
|
github.com/munnerz/goautoneg
|
|
# github.com/orchard9/go-chassis v0.1.0
|
|
## explicit; go 1.26.0
|
|
github.com/orchard9/go-chassis/chassis
|
|
github.com/orchard9/go-chassis/config
|
|
github.com/orchard9/go-chassis/logging
|
|
# github.com/prometheus/client_golang v1.24.0
|
|
## explicit; go 1.25.0
|
|
github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil
|
|
github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil/header
|
|
github.com/prometheus/client_golang/prometheus
|
|
github.com/prometheus/client_golang/prometheus/collectors
|
|
github.com/prometheus/client_golang/prometheus/internal
|
|
github.com/prometheus/client_golang/prometheus/promhttp
|
|
github.com/prometheus/client_golang/prometheus/promhttp/internal
|
|
# github.com/prometheus/client_model v0.6.2
|
|
## explicit; go 1.22.0
|
|
github.com/prometheus/client_model/go
|
|
# github.com/prometheus/common v0.70.0
|
|
## explicit; go 1.25.0
|
|
github.com/prometheus/common/expfmt
|
|
github.com/prometheus/common/model
|
|
# github.com/prometheus/procfs v0.21.1
|
|
## explicit; go 1.25.0
|
|
github.com/prometheus/procfs
|
|
github.com/prometheus/procfs/internal/fs
|
|
github.com/prometheus/procfs/internal/util
|
|
# github.com/redis/go-redis/v9 v9.22.0
|
|
## explicit; go 1.24
|
|
github.com/redis/go-redis/v9
|
|
github.com/redis/go-redis/v9/auth
|
|
github.com/redis/go-redis/v9/internal
|
|
github.com/redis/go-redis/v9/internal/auth/streaming
|
|
github.com/redis/go-redis/v9/internal/hashtag
|
|
github.com/redis/go-redis/v9/internal/hscan
|
|
github.com/redis/go-redis/v9/internal/interfaces
|
|
github.com/redis/go-redis/v9/internal/maintnotifications/logs
|
|
github.com/redis/go-redis/v9/internal/otel
|
|
github.com/redis/go-redis/v9/internal/pool
|
|
github.com/redis/go-redis/v9/internal/proto
|
|
github.com/redis/go-redis/v9/internal/routing
|
|
github.com/redis/go-redis/v9/internal/util
|
|
github.com/redis/go-redis/v9/maintnotifications
|
|
github.com/redis/go-redis/v9/push
|
|
# go.uber.org/atomic v1.11.0
|
|
## explicit; go 1.18
|
|
go.uber.org/atomic
|
|
# golang.org/x/sys v0.47.0
|
|
## explicit; go 1.25.0
|
|
golang.org/x/sys/cpu
|
|
golang.org/x/sys/unix
|
|
golang.org/x/sys/windows
|
|
# google.golang.org/protobuf v1.36.11
|
|
## explicit; go 1.23
|
|
google.golang.org/protobuf/encoding/protodelim
|
|
google.golang.org/protobuf/encoding/prototext
|
|
google.golang.org/protobuf/encoding/protowire
|
|
google.golang.org/protobuf/internal/descfmt
|
|
google.golang.org/protobuf/internal/descopts
|
|
google.golang.org/protobuf/internal/detrand
|
|
google.golang.org/protobuf/internal/editiondefaults
|
|
google.golang.org/protobuf/internal/encoding/defval
|
|
google.golang.org/protobuf/internal/encoding/messageset
|
|
google.golang.org/protobuf/internal/encoding/tag
|
|
google.golang.org/protobuf/internal/encoding/text
|
|
google.golang.org/protobuf/internal/errors
|
|
google.golang.org/protobuf/internal/filedesc
|
|
google.golang.org/protobuf/internal/filetype
|
|
google.golang.org/protobuf/internal/flags
|
|
google.golang.org/protobuf/internal/genid
|
|
google.golang.org/protobuf/internal/impl
|
|
google.golang.org/protobuf/internal/order
|
|
google.golang.org/protobuf/internal/pragma
|
|
google.golang.org/protobuf/internal/protolazy
|
|
google.golang.org/protobuf/internal/set
|
|
google.golang.org/protobuf/internal/strs
|
|
google.golang.org/protobuf/internal/version
|
|
google.golang.org/protobuf/proto
|
|
google.golang.org/protobuf/reflect/protoreflect
|
|
google.golang.org/protobuf/reflect/protoregistry
|
|
google.golang.org/protobuf/runtime/protoiface
|
|
google.golang.org/protobuf/runtime/protoimpl
|
|
google.golang.org/protobuf/types/known/timestamppb
|