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.
197 lines
5.3 KiB
Go
197 lines
5.3 KiB
Go
package redis
|
|
|
|
import "time"
|
|
|
|
// NewCmdResult returns a Cmd initialised with val and err for testing.
|
|
func NewCmdResult(val interface{}, err error) *Cmd {
|
|
var cmd Cmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewSliceResult returns a SliceCmd initialised with val and err for testing.
|
|
func NewSliceResult(val []interface{}, err error) *SliceCmd {
|
|
var cmd SliceCmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewStatusResult returns a StatusCmd initialised with val and err for testing.
|
|
func NewStatusResult(val string, err error) *StatusCmd {
|
|
var cmd StatusCmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewIntResult returns an IntCmd initialised with val and err for testing.
|
|
func NewIntResult(val int64, err error) *IntCmd {
|
|
var cmd IntCmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewDurationResult returns a DurationCmd initialised with val and err for testing.
|
|
func NewDurationResult(val time.Duration, err error) *DurationCmd {
|
|
var cmd DurationCmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewBoolResult returns a BoolCmd initialised with val and err for testing.
|
|
func NewBoolResult(val bool, err error) *BoolCmd {
|
|
var cmd BoolCmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewStringResult returns a StringCmd initialised with val and err for testing.
|
|
func NewStringResult(val string, err error) *StringCmd {
|
|
var cmd StringCmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewFloatResult returns a FloatCmd initialised with val and err for testing.
|
|
func NewFloatResult(val float64, err error) *FloatCmd {
|
|
var cmd FloatCmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewStringSliceResult returns a StringSliceCmd initialised with val and err for testing.
|
|
func NewStringSliceResult(val []string, err error) *StringSliceCmd {
|
|
var cmd StringSliceCmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewBoolSliceResult returns a BoolSliceCmd initialised with val and err for testing.
|
|
func NewBoolSliceResult(val []bool, err error) *BoolSliceCmd {
|
|
var cmd BoolSliceCmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewFloatSliceResult returns a FloatSliceCmd initialised with val and err for testing.
|
|
func NewFloatSliceResult(val []float64, err error) *FloatSliceCmd {
|
|
var cmd FloatSliceCmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewMapStringStringResult returns a MapStringStringCmd initialised with val and err for testing.
|
|
func NewMapStringStringResult(val map[string]string, err error) *MapStringStringCmd {
|
|
var cmd MapStringStringCmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewMapStringIntCmdResult returns a MapStringIntCmd initialised with val and err for testing.
|
|
func NewMapStringIntCmdResult(val map[string]int64, err error) *MapStringIntCmd {
|
|
var cmd MapStringIntCmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewTimeCmdResult returns a TimeCmd initialised with val and err for testing.
|
|
func NewTimeCmdResult(val time.Time, err error) *TimeCmd {
|
|
var cmd TimeCmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewZSliceCmdResult returns a ZSliceCmd initialised with val and err for testing.
|
|
func NewZSliceCmdResult(val []Z, err error) *ZSliceCmd {
|
|
var cmd ZSliceCmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewZWithKeyCmdResult returns a ZWithKeyCmd initialised with val and err for testing.
|
|
func NewZWithKeyCmdResult(val *ZWithKey, err error) *ZWithKeyCmd {
|
|
var cmd ZWithKeyCmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewScanCmdResult returns a ScanCmd initialised with val and err for testing.
|
|
func NewScanCmdResult(keys []string, cursor uint64, err error) *ScanCmd {
|
|
var cmd ScanCmd
|
|
cmd.page = keys
|
|
cmd.cursor = cursor
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewClusterSlotsCmdResult returns a ClusterSlotsCmd initialised with val and err for testing.
|
|
func NewClusterSlotsCmdResult(val []ClusterSlot, err error) *ClusterSlotsCmd {
|
|
var cmd ClusterSlotsCmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewGeoLocationCmdResult returns a GeoLocationCmd initialised with val and err for testing.
|
|
func NewGeoLocationCmdResult(val []GeoLocation, err error) *GeoLocationCmd {
|
|
var cmd GeoLocationCmd
|
|
cmd.locations = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewGeoPosCmdResult returns a GeoPosCmd initialised with val and err for testing.
|
|
func NewGeoPosCmdResult(val []*GeoPos, err error) *GeoPosCmd {
|
|
var cmd GeoPosCmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewCommandsInfoCmdResult returns a CommandsInfoCmd initialised with val and err for testing.
|
|
func NewCommandsInfoCmdResult(val map[string]*CommandInfo, err error) *CommandsInfoCmd {
|
|
var cmd CommandsInfoCmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewXMessageSliceCmdResult returns a XMessageSliceCmd initialised with val and err for testing.
|
|
func NewXMessageSliceCmdResult(val []XMessage, err error) *XMessageSliceCmd {
|
|
var cmd XMessageSliceCmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewXStreamSliceCmdResult returns a XStreamSliceCmd initialised with val and err for testing.
|
|
func NewXStreamSliceCmdResult(val []XStream, err error) *XStreamSliceCmd {
|
|
var cmd XStreamSliceCmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|
|
|
|
// NewXPendingResult returns a XPendingCmd initialised with val and err for testing.
|
|
func NewXPendingResult(val *XPending, err error) *XPendingCmd {
|
|
var cmd XPendingCmd
|
|
cmd.val = val
|
|
cmd.SetErr(err)
|
|
return &cmd
|
|
}
|