hush/vendor/github.com/redis/go-redis/v9/himport.go
jx12n 4d9a26498e hush: one-time secret links the server cannot read
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.
2026-09-03 00:08:38 -06:00

474 lines
17 KiB
Go

package redis
import (
"context"
"strings"
"sync"
"github.com/redis/go-redis/v9/internal"
"github.com/redis/go-redis/v9/internal/pool"
"github.com/redis/go-redis/v9/internal/proto"
)
// himportFieldset is the client-side record of a fieldset registered with
// HImportPrepare.
type himportFieldset struct {
fields []string
version uint64
}
// himportRegistry remembers fieldsets registered through a client so HIMPORT
// SET can lazily prepare them on whichever pooled connection it executes.
// Versions increase monotonically and start at 1; re-registering a name under
// a new version invalidates every connection's prepared flag for it, so a
// replaced fieldset is re-prepared before its next use.
//
// Discards propagate lazily as well: a discarded name is kept as a tombstone
// and the discard-all counter as an epoch, and connections whose sessions
// still hold discarded fieldsets replay HIMPORT DISCARD/DISCARDALL before
// their next HIMPORT command (see baseClient.himportInjectedCmds).
type himportRegistry struct {
mu sync.RWMutex
nextVersion uint64
fieldsets map[string]himportFieldset
// tombstones holds names discarded through this client whose server-side
// copies may survive on pooled connections that prepared them. An entry
// is removed when the name is registered again (the new version replaces
// the fieldset on the server, so no discard is needed) or by discardAll.
// Known limitation: a workload discarding many uniquely-named fieldsets
// grows this map for the client's lifetime and pays an O(tombstones)
// snapshot per HIMPORT round trip; HImportDiscardAll resets it.
tombstones map[string]struct{}
// discardAllEpoch increments on every successful HImportDiscardAll.
discardAllEpoch uint64
}
func newHImportRegistry() *himportRegistry {
return &himportRegistry{}
}
// register stores the fieldset and returns its new version together with the
// current discard-all epoch.
func (r *himportRegistry) register(name string, fields []string) (version, epoch uint64) {
r.mu.Lock()
defer r.mu.Unlock()
if r.fieldsets == nil {
r.fieldsets = make(map[string]himportFieldset)
}
delete(r.tombstones, name)
r.nextVersion++
r.fieldsets[name] = himportFieldset{
fields: append([]string(nil), fields...),
version: r.nextVersion,
}
return r.nextVersion, r.discardAllEpoch
}
func (r *himportRegistry) lookup(name string) (himportFieldset, bool) {
if r == nil {
return himportFieldset{}, false
}
r.mu.RLock()
fs, ok := r.fieldsets[name]
r.mu.RUnlock()
return fs, ok
}
// discard removes the fieldset and leaves a tombstone so connections whose
// sessions still hold it replay the DISCARD before their next HIMPORT
// command. It reports whether the fieldset was registered.
func (r *himportRegistry) discard(name string) bool {
r.mu.Lock()
defer r.mu.Unlock()
if _, ok := r.fieldsets[name]; !ok {
return false
}
delete(r.fieldsets, name)
if r.tombstones == nil {
r.tombstones = make(map[string]struct{})
}
r.tombstones[name] = struct{}{}
return true
}
// discardAll drops every fieldset and tombstone and moves to a new epoch;
// connections prepared under an older epoch replay HIMPORT DISCARDALL before
// their next HIMPORT command. It returns the new epoch and the number of
// fieldsets that were registered.
func (r *himportRegistry) discardAll() (epoch uint64, removed int) {
r.mu.Lock()
removed = len(r.fieldsets)
r.fieldsets = nil
r.tombstones = nil
r.discardAllEpoch++
epoch = r.discardAllEpoch
r.mu.Unlock()
return epoch, removed
}
// discardVersion withdraws a registration whose fan-out PREPARE was rejected
// by a server — but only while the entry is still at that version, so a
// concurrent re-registration is not clobbered. A tombstone is left: the
// fan-out may have succeeded on some masters before another rejected it
// (per-node ACLs, rolling upgrades), and those sessions hold the withdrawn
// fieldset; the tombstone makes their next HIMPORT command discard it
// instead of leaving a fieldset the client can no longer address.
func (r *himportRegistry) discardVersion(name string, version uint64) {
r.mu.Lock()
if fs, ok := r.fieldsets[name]; ok && fs.version == version {
delete(r.fieldsets, name)
if r.tombstones == nil {
r.tombstones = make(map[string]struct{})
}
r.tombstones[name] = struct{}{}
}
r.mu.Unlock()
}
// refreshVersion bumps a registered fieldset to a new version, keeping its
// fields — but only while the entry is still at the given version, so a
// concurrent re-registration is not disturbed. Every connection's prepared
// flag becomes stale, forcing a re-prepare before the fieldset's next use on
// each of them. Used when a "no such fieldset" reply signals session loss
// that may have hit more connections than the one that reported it (failover,
// cross-region switch, reset storms).
func (r *himportRegistry) refreshVersion(name string, version uint64) {
r.mu.Lock()
if fs, ok := r.fieldsets[name]; ok && fs.version == version {
r.nextVersion++
fs.version = r.nextVersion
r.fieldsets[name] = fs
}
r.mu.Unlock()
}
// idle reports whether the registry implies no injection work at all: no
// fieldsets to replay, no tombstones to discard, and no discard-all epoch a
// session could be behind.
func (r *himportRegistry) idle() bool {
if r == nil {
return true
}
r.mu.RLock()
idle := len(r.fieldsets) == 0 && len(r.tombstones) == 0 && r.discardAllEpoch == 0
r.mu.RUnlock()
return idle
}
// cleanupSnapshot returns the current epoch and the tombstoned names.
func (r *himportRegistry) cleanupSnapshot() (epoch uint64, tombstones []string) {
r.mu.RLock()
epoch = r.discardAllEpoch
if len(r.tombstones) > 0 {
tombstones = make([]string, 0, len(r.tombstones))
for name := range r.tombstones {
tombstones = append(tombstones, name)
}
}
r.mu.RUnlock()
return epoch, tombstones
}
// himportNoSuchFieldset reports whether err is the server's "no such
// fieldset" reply, i.e. an HIMPORT SET executed on a connection whose session
// does not hold the referenced fieldset.
func himportNoSuchFieldset(err error) bool {
return isRedisError(err) && strings.Contains(err.Error(), "no such fieldset")
}
// himportInjectedCmds returns the HIMPORT commands to write to cn ahead of a
// batch, in order:
//
// 1. HIMPORT DISCARDALL when cn's session was prepared under an older
// discard-all epoch;
// 2. HIMPORT DISCARD for each discarded fieldset the session still holds;
// 3. HIMPORT PREPARE for each registered fieldset referenced by an HIMPORT
// SET in the batch that the session lacks at the current version.
//
// A fieldset covered by a user-issued PREPARE earlier in the batch needs no
// injection — the server session holds it by the time the SET runs. Returns
// nil when the batch contains no HIMPORT commands: sessions holding only
// discarded fieldsets are cleaned up on their next HIMPORT use, not on
// unrelated traffic.
func (c *baseClient) himportInjectedCmds(ctx context.Context, cn *pool.Conn, cmds []Cmder) []Cmder {
if c.himport.idle() {
return nil
}
hasHImport := false
for _, cmd := range cmds {
if _, ok := cmd.(himportCmder); ok {
hasHImport = true
break
}
}
if !hasHImport {
return nil
}
var injected []Cmder
// Discards first: a session behind the discard-all epoch is wiped
// entirely; otherwise individual tombstoned fieldsets it still holds are
// discarded.
epoch, tombstones := c.himport.cleanupSnapshot()
sessionWiped := false
if cn.HasPreparedFieldsets() && cn.FieldsetEpoch() != epoch {
da := NewHImportDiscardAllCmd(ctx)
da.registryEpoch = epoch
injected = append(injected, da)
sessionWiped = true
} else {
for _, name := range tombstones {
if cn.FieldsetPreparedVersion(name) != 0 {
injected = append(injected, NewHImportDiscardCmd(ctx, name))
}
}
}
// Prepares for registered fieldsets the batch's SETs reference.
var covered map[string]struct{}
cover := func(name string) {
if covered == nil {
covered = make(map[string]struct{})
}
covered[name] = struct{}{}
}
for _, cmd := range cmds {
switch hc := cmd.(type) {
case *HImportPrepareCmd:
cover(hc.fieldsetName)
case *HImportSetCmd:
if _, ok := covered[hc.fieldsetName]; ok {
continue
}
fs, ok := c.himport.lookup(hc.fieldsetName)
if !ok {
continue
}
if !sessionWiped && cn.FieldsetPreparedVersion(hc.fieldsetName) == fs.version {
continue
}
// The session holds an older version. Discard it before the
// re-prepare: the SET behind it is already on the wire, and if
// the re-prepare fails the SET must answer "no such fieldset"
// rather than silently writing the old version's field names.
if !sessionWiped && cn.FieldsetPreparedVersion(hc.fieldsetName) != 0 {
injected = append(injected, NewHImportDiscardCmd(ctx, hc.fieldsetName))
}
prep := NewHImportPrepareCmd(ctx, hc.fieldsetName, fs.fields...)
prep.registryVersion = fs.version
prep.registryEpoch = epoch
injected = append(injected, prep)
cover(hc.fieldsetName)
}
}
return injected
}
// himportReadInjectedReplies consumes the replies of injected HIMPORT
// commands. Server errors are recorded on the command and the connection is
// left readable; transport errors are returned. Successful commands apply
// their prepared-flag bookkeeping on cn.
func (c *baseClient) himportReadInjectedReplies(ctx context.Context, cn *pool.Conn, rd *proto.Reader, injected []Cmder) error {
for _, cmd := range injected {
if err := c.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil {
internal.Logger.Printf(ctx, "push: error processing pending notifications before reading reply: %v", err)
}
err := cmd.readReply(rd)
cmd.SetErr(err)
if err != nil {
if !isRedisError(err) {
return err
}
// A failed injected PREPARE becomes the root cause of the
// dependent SETs' errors downstream; a failed injected discard
// only delays cleanup until the next HIMPORT command.
internal.Logger.Printf(ctx, "himport: injected %s failed: %v", cmd.Name(), err)
continue
}
switch hc := cmd.(type) {
case *HImportPrepareCmd:
cn.MarkFieldsetPrepared(hc.fieldsetName, hc.registryVersion, hc.registryEpoch)
case *HImportDiscardCmd:
cn.UnmarkFieldsetPrepared(hc.fieldsetName)
case *HImportDiscardAllCmd:
cn.ClearPreparedFieldsets(hc.registryEpoch)
}
}
return nil
}
// himportAfterCmd applies registry and prepared-flag updates after a
// user-issued HIMPORT command completed successfully on cn.
func (c *baseClient) himportAfterCmd(cn *pool.Conn, hc himportCmder) {
if c.himport == nil {
return
}
switch cmd := hc.(type) {
case *HImportPrepareCmd:
version, epoch := cmd.registryVersion, cmd.registryEpoch
if version == 0 {
version, epoch = c.himport.register(cmd.fieldsetName, cmd.fields)
}
// A pre-assigned version marks a fan-out copy: the fieldset was
// registered once at the cluster/ring level; only mark the
// executing connection.
cn.MarkFieldsetPrepared(cmd.fieldsetName, version, epoch)
case *HImportDiscardCmd:
registered := c.himport.discard(cmd.fieldsetName)
cn.UnmarkFieldsetPrepared(cmd.fieldsetName)
// The managed API reports the registry lifecycle: 1 when the
// fieldset was registered on this client and is now removed. The
// executing connection's session count stands only for fieldsets
// the registry never knew (raw usage).
if registered {
cmd.SetVal(1)
}
case *HImportDiscardAllCmd:
// A pre-assigned epoch marks a fan-out copy: the registry was
// already wiped at the cluster/ring level; only move the executing
// connection to that epoch.
if cmd.registryEpoch != 0 {
cn.ClearPreparedFieldsets(cmd.registryEpoch)
return
}
epoch, removed := c.himport.discardAll()
cn.ClearPreparedFieldsets(epoch)
// Same registry semantics: report how many registered fieldsets
// were removed, not how many the executing session happened to
// hold.
if removed > 0 {
cmd.SetVal(int64(removed))
}
}
}
// himportAfterBatch runs after all replies of a batch were read: it surfaces
// an injected PREPARE failure as the root cause on the HIMPORT SET commands
// that depended on it (their own reply is the secondary "no such fieldset"
// error), invalidates stale prepared flags for SETs that found their
// registered fieldset missing server-side, and applies registry updates for
// user-issued HIMPORT commands that succeeded in the batch.
// rawErr throughout: this runs on the execution path, before an async
// autopipeline batch completes (its ready channel closes only after the
// pipeline hook chain returns) — Err() on a user command would await and
// self-deadlock the dispatcher.
func (c *baseClient) himportAfterBatch(cn *pool.Conn, injected []Cmder, cmds []Cmder) {
var failed map[string]error
var refreshed map[string]struct{}
for _, cmd := range injected {
if prep, ok := cmd.(*HImportPrepareCmd); ok {
if err := prep.Err(); err != nil {
if failed == nil {
failed = make(map[string]error)
}
failed[prep.fieldsetName] = err
}
}
}
for _, cmd := range cmds {
hc, ok := cmd.(himportCmder)
if !ok {
continue
}
if set, ok := hc.(*HImportSetCmd); ok {
if rootCause, ok := failed[set.fieldsetName]; ok && himportNoSuchFieldset(set.rawErr()) {
set.SetErr(rootCause)
continue
}
// The session lost a fieldset the flags claim is prepared (e.g.
// RESET) — and the same event may have wiped other sessions
// whose flags also still look current. Bump the fieldset
// version once so the SET's re-issue, the cluster re-queue on
// whichever connection it lands, or the caller's transaction
// retry replays the PREPARE.
if himportNoSuchFieldset(set.rawErr()) {
if _, done := refreshed[set.fieldsetName]; !done {
if refreshed == nil {
refreshed = make(map[string]struct{})
}
refreshed[set.fieldsetName] = struct{}{}
if fs, registered := c.himport.lookup(set.fieldsetName); registered {
c.himport.refreshVersion(set.fieldsetName, fs.version)
}
}
}
continue
}
if hc.rawErr() == nil {
c.himportAfterCmd(cn, hc)
}
}
}
// himportRetryFailedSets re-issues, once, the HIMPORT SET commands of a
// pipeline batch that failed with "no such fieldset" while their fieldset is
// registered — the error must not surface for managed fieldsets (NF.4). Only
// the SETs are re-sent: HIMPORT SET is a full replace, so re-execution is
// idempotent, and no other command of the batch runs again. Their prepared
// flags were invalidated by himportAfterBatch, so himportInjectedCmds
// regenerates the PREPAREs for this connection. Transport errors are
// returned; server errors stay recorded on the commands.
// (The retry does not carry an ASKING prefix. A redirected [ASKING, SET]
// pair whose injected PREPARE failed is excluded by the root-cause swap in
// himportAfterBatch; one that lost its session without an injection can be
// re-issued here, and the bare SET then draws a fresh MOVED/ASK that the
// outer cluster redirect handling resolves.)
func (c *baseClient) himportRetryFailedSets(ctx context.Context, cn *pool.Conn, cmds []Cmder) error {
if c.himport.idle() {
return nil
}
var retry []Cmder
for _, cmd := range cmds {
// rawErr: execution path, same self-deadlock rule as himportAfterBatch.
if set, ok := cmd.(*HImportSetCmd); ok && himportNoSuchFieldset(set.rawErr()) {
if _, registered := c.himport.lookup(set.fieldsetName); registered {
retry = append(retry, set)
}
}
}
if len(retry) == 0 {
return nil
}
injected := c.himportInjectedCmds(ctx, cn, retry)
if err := cn.WithWriter(c.context(ctx), c.opt.WriteTimeout, func(wr *proto.Writer) error {
for _, ic := range injected {
if err := writeCmd(wr, ic); err != nil {
return err
}
}
return writeCmds(wr, retry)
}); err != nil {
return err
}
return cn.WithReader(c.context(ctx), c.opt.ReadTimeout, func(rd *proto.Reader) error {
if err := c.himportReadInjectedReplies(ctx, cn, rd, injected); err != nil {
return err
}
err := c.pipelineReadCmds(ctx, cn, rd, retry)
if err != nil && !isRedisError(err) {
return err
}
// Server errors (including a repeated failure) stay on the
// individual commands; the batch as a whole is done.
c.himportAfterBatch(cn, injected, retry)
return nil
})
}
// himportShouldRetrySet reports whether a retry of cmd may succeed after it
// failed with "no such fieldset": true when the fieldset is registered
// client-side — the executing connection lost its server session state (for
// example a RESET, or a concurrent discard). The connection's prepared flag
// was already invalidated inside _process, while that goroutine still owned
// the connection, so the retry re-prepares lazily wherever it lands.
func (c *baseClient) himportShouldRetrySet(cmd Cmder, err error) bool {
set, ok := cmd.(*HImportSetCmd)
if !ok || !himportNoSuchFieldset(err) {
return false
}
_, registered := c.himport.lookup(set.fieldsetName)
return registered
}