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.
421 lines
15 KiB
Go
421 lines
15 KiB
Go
package redis
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/redis/go-redis/v9/internal/hashtag"
|
|
)
|
|
|
|
// SetCmdable is an interface for Redis set commands.
|
|
// Sets are unordered collections of unique strings.
|
|
type SetCmdable interface {
|
|
SAdd(ctx context.Context, key string, members ...interface{}) *IntCmd
|
|
SCard(ctx context.Context, key string) *IntCmd
|
|
SDiff(ctx context.Context, keys ...string) *StringSliceCmd
|
|
SDiffCard(ctx context.Context, opts *SDiffCardOptions, keys ...string) *IntCmd
|
|
SDiffStore(ctx context.Context, destination string, keys ...string) *IntCmd
|
|
SInter(ctx context.Context, keys ...string) *StringSliceCmd
|
|
SInterCard(ctx context.Context, limit int64, keys ...string) *IntCmd
|
|
SInterStore(ctx context.Context, destination string, keys ...string) *IntCmd
|
|
SIsMember(ctx context.Context, key string, member interface{}) *BoolCmd
|
|
SMIsMember(ctx context.Context, key string, members ...interface{}) *BoolSliceCmd
|
|
SMembers(ctx context.Context, key string) *StringSliceCmd
|
|
SMembersMap(ctx context.Context, key string) *StringStructMapCmd
|
|
SMove(ctx context.Context, source, destination string, member interface{}) *BoolCmd
|
|
SPop(ctx context.Context, key string) *StringCmd
|
|
SPopN(ctx context.Context, key string, count int64) *StringSliceCmd
|
|
SRandMember(ctx context.Context, key string) *StringCmd
|
|
SRandMemberN(ctx context.Context, key string, count int64) *StringSliceCmd
|
|
SRem(ctx context.Context, key string, members ...interface{}) *IntCmd
|
|
SScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd
|
|
SUnion(ctx context.Context, keys ...string) *StringSliceCmd
|
|
SUnionCard(ctx context.Context, opts *SUnionCardOptions, keys ...string) *IntCmd
|
|
SUnionStore(ctx context.Context, destination string, keys ...string) *IntCmd
|
|
}
|
|
|
|
// SUnionCardOptions are the options for SUnionCard.
|
|
type SUnionCardOptions struct {
|
|
Approx bool // use an approximate (HyperLogLog) count.
|
|
Limit int64 // cap the result; 0 means no limit.
|
|
}
|
|
|
|
// SDiffCardOptions are the options for SDiffCard.
|
|
type SDiffCardOptions struct {
|
|
Limit int64 // cap the result; 0 means no limit.
|
|
}
|
|
|
|
// Returns the number of elements that were added to the set, not including all
|
|
// the elements already present in the set.
|
|
//
|
|
// For more information about the command please refer to [SADD].
|
|
//
|
|
// [SADD]: (https://redis.io/docs/latest/commands/sadd/)
|
|
func (c cmdable) SAdd(ctx context.Context, key string, members ...interface{}) *IntCmd {
|
|
args := make([]interface{}, 2, 2+len(members))
|
|
args[0] = "sadd"
|
|
args[1] = key
|
|
args = appendArgs(args, members)
|
|
cmd := NewIntCmd(ctx, args...)
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Returns the set cardinality (number of elements) of the set stored at key.
|
|
// Returns 0 if key does not exist.
|
|
//
|
|
// For more information about the command please refer to [SCARD].
|
|
//
|
|
// [SCARD]: (https://redis.io/docs/latest/commands/scard/)
|
|
func (c cmdable) SCard(ctx context.Context, key string) *IntCmd {
|
|
cmd := NewIntCmd(ctx, "scard", key)
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Returns the members of the set resulting from the difference between the first set
|
|
// and all the successive sets.
|
|
// Keys that do not exist are considered to be empty sets.
|
|
//
|
|
// For more information about the command please refer to [SDIFF].
|
|
//
|
|
// [SDIFF]: (https://redis.io/docs/latest/commands/sdiff/)
|
|
func (c cmdable) SDiff(ctx context.Context, keys ...string) *StringSliceCmd {
|
|
args := make([]interface{}, 1+len(keys))
|
|
args[0] = "sdiff"
|
|
for i, key := range keys {
|
|
args[1+i] = key
|
|
}
|
|
cmd := NewStringSliceCmd(ctx, args...)
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Stores the members of the set resulting from the difference between the first set
|
|
// and all the successive sets into destination.
|
|
// If destination already exists, it is overwritten.
|
|
//
|
|
// For more information about the command please refer to [SDIFFSTORE].
|
|
//
|
|
// [SDIFFSTORE]: (https://redis.io/docs/latest/commands/sdiffstore/)
|
|
func (c cmdable) SDiffStore(ctx context.Context, destination string, keys ...string) *IntCmd {
|
|
args := make([]interface{}, 2+len(keys))
|
|
args[0] = "sdiffstore"
|
|
args[1] = destination
|
|
for i, key := range keys {
|
|
args[2+i] = key
|
|
}
|
|
cmd := NewIntCmd(ctx, args...)
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Returns the cardinality of the difference of the first set and the rest.
|
|
// Missing keys are treated as empty sets.
|
|
//
|
|
// For more information about the command please refer to [SDIFFCARD].
|
|
//
|
|
// [SDIFFCARD]: (https://redis.io/docs/latest/commands/sdiffcard/)
|
|
func (c cmdable) SDiffCard(ctx context.Context, opts *SDiffCardOptions, keys ...string) *IntCmd {
|
|
if opts == nil {
|
|
opts = &SDiffCardOptions{}
|
|
}
|
|
numKeys := len(keys)
|
|
args := make([]interface{}, 0, 4+numKeys)
|
|
args = append(args, "sdiffcard", numKeys)
|
|
for _, key := range keys {
|
|
args = append(args, key)
|
|
}
|
|
args = append(args, "limit", opts.Limit)
|
|
cmd := NewIntCmd(ctx, args...)
|
|
// Keys start after the numkeys arg: ["sdiffcard", numKeys, key1, ...].
|
|
cmd.SetFirstKeyPos(2)
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Returns the members of the set resulting from the intersection of all the given sets.
|
|
// Keys that do not exist are considered to be empty sets.
|
|
// With one of the keys being an empty set, the resulting set is also empty.
|
|
//
|
|
// For more information about the command please refer to [SINTER].
|
|
//
|
|
// [SINTER]: (https://redis.io/docs/latest/commands/sinter/)
|
|
func (c cmdable) SInter(ctx context.Context, keys ...string) *StringSliceCmd {
|
|
args := make([]interface{}, 1+len(keys))
|
|
args[0] = "sinter"
|
|
for i, key := range keys {
|
|
args[1+i] = key
|
|
}
|
|
cmd := NewStringSliceCmd(ctx, args...)
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Returns the cardinality of the set resulting from the intersection of all the given sets.
|
|
// Keys that do not exist are considered to be empty sets.
|
|
// With one of the keys being an empty set, the resulting set is also empty.
|
|
//
|
|
// The limit parameter sets an upper bound on the number of results returned.
|
|
// If limit is 0, no limit is applied.
|
|
//
|
|
// For more information about the command please refer to [SINTERCARD].
|
|
//
|
|
// [SINTERCARD]: (https://redis.io/docs/latest/commands/sintercard/)
|
|
func (c cmdable) SInterCard(ctx context.Context, limit int64, keys ...string) *IntCmd {
|
|
numKeys := len(keys)
|
|
args := make([]interface{}, 4+numKeys)
|
|
args[0] = "sintercard"
|
|
args[1] = numKeys
|
|
for i, key := range keys {
|
|
args[2+i] = key
|
|
}
|
|
args[2+numKeys] = "limit"
|
|
args[3+numKeys] = limit
|
|
cmd := NewIntCmd(ctx, args...)
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Stores the members of the set resulting from the intersection of all the given sets
|
|
// into destination.
|
|
// If destination already exists, it is overwritten.
|
|
//
|
|
// For more information about the command please refer to [SINTERSTORE].
|
|
//
|
|
// [SINTERSTORE]: (https://redis.io/docs/latest/commands/sinterstore/)
|
|
func (c cmdable) SInterStore(ctx context.Context, destination string, keys ...string) *IntCmd {
|
|
args := make([]interface{}, 2+len(keys))
|
|
args[0] = "sinterstore"
|
|
args[1] = destination
|
|
for i, key := range keys {
|
|
args[2+i] = key
|
|
}
|
|
cmd := NewIntCmd(ctx, args...)
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Returns if member is a member of the set stored at key.
|
|
// Returns true if the element is a member of the set, false if it is not a member
|
|
// or if key does not exist.
|
|
//
|
|
// For more information about the command please refer to [SISMEMBER].
|
|
//
|
|
// [SISMEMBER]: (https://redis.io/docs/latest/commands/sismember/)
|
|
func (c cmdable) SIsMember(ctx context.Context, key string, member interface{}) *BoolCmd {
|
|
cmd := NewBoolCmd(ctx, "sismember", key, member)
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Returns whether each member is a member of the set stored at key.
|
|
// For each member, returns true if the element is a member of the set, false if it is not
|
|
// a member or if key does not exist.
|
|
//
|
|
// For more information about the command please refer to [SMISMEMBER].
|
|
//
|
|
// [SMISMEMBER]: (https://redis.io/docs/latest/commands/smismember/)
|
|
func (c cmdable) SMIsMember(ctx context.Context, key string, members ...interface{}) *BoolSliceCmd {
|
|
args := make([]interface{}, 2, 2+len(members))
|
|
args[0] = "smismember"
|
|
args[1] = key
|
|
args = appendArgs(args, members)
|
|
cmd := NewBoolSliceCmd(ctx, args...)
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Returns all the members of the set value stored at key.
|
|
// Returns an empty slice if key does not exist.
|
|
//
|
|
// For more information about the command please refer to [SMEMBERS].
|
|
//
|
|
// [SMEMBERS]: (https://redis.io/docs/latest/commands/smembers/)
|
|
func (c cmdable) SMembers(ctx context.Context, key string) *StringSliceCmd {
|
|
cmd := NewStringSliceCmd(ctx, "smembers", key)
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Returns all the members of the set value stored at key as a map.
|
|
// Returns an empty map if key does not exist.
|
|
//
|
|
// For more information about the command please refer to [SMEMBERS].
|
|
//
|
|
// [SMEMBERS]: (https://redis.io/docs/latest/commands/smembers/)
|
|
func (c cmdable) SMembersMap(ctx context.Context, key string) *StringStructMapCmd {
|
|
cmd := NewStringStructMapCmd(ctx, "smembers", key)
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Moves member from the set at source to the set at destination.
|
|
// This operation is atomic. In every given moment the element will appear to be a member
|
|
// of source or destination for other clients.
|
|
//
|
|
// For more information about the command please refer to [SMOVE].
|
|
//
|
|
// [SMOVE]: (https://redis.io/docs/latest/commands/smove/)
|
|
func (c cmdable) SMove(ctx context.Context, source, destination string, member interface{}) *BoolCmd {
|
|
cmd := NewBoolCmd(ctx, "smove", source, destination, member)
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Removes and returns one or more random members from the set value stored at key.
|
|
// This version returns a single random member.
|
|
//
|
|
// For more information about the command please refer to [SPOP].
|
|
//
|
|
// [SPOP]: (https://redis.io/docs/latest/commands/spop/)
|
|
func (c cmdable) SPop(ctx context.Context, key string) *StringCmd {
|
|
cmd := NewStringCmd(ctx, "spop", key)
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Removes and returns one or more random members from the set value stored at key.
|
|
// This version returns up to count random members.
|
|
//
|
|
// For more information about the command please refer to [SPOP].
|
|
//
|
|
// [SPOP]: (https://redis.io/docs/latest/commands/spop/)
|
|
func (c cmdable) SPopN(ctx context.Context, key string, count int64) *StringSliceCmd {
|
|
cmd := NewStringSliceCmd(ctx, "spop", key, count)
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Returns a random member from the set value stored at key.
|
|
// This version returns a single random member without removing it.
|
|
//
|
|
// For more information about the command please refer to [SRANDMEMBER].
|
|
//
|
|
// [SRANDMEMBER]: (https://redis.io/docs/latest/commands/srandmember/)
|
|
func (c cmdable) SRandMember(ctx context.Context, key string) *StringCmd {
|
|
cmd := NewStringCmd(ctx, "srandmember", key)
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Returns an array of random members from the set value stored at key.
|
|
// This version returns up to count random members without removing them.
|
|
// When called with a positive count, returns distinct elements.
|
|
// When called with a negative count, allows for repeated elements.
|
|
//
|
|
// For more information about the command please refer to [SRANDMEMBER].
|
|
//
|
|
// [SRANDMEMBER]: (https://redis.io/docs/latest/commands/srandmember/)
|
|
func (c cmdable) SRandMemberN(ctx context.Context, key string, count int64) *StringSliceCmd {
|
|
cmd := NewStringSliceCmd(ctx, "srandmember", key, count)
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Removes the specified members from the set stored at key.
|
|
// Specified members that are not a member of this set are ignored.
|
|
// If key does not exist, it is treated as an empty set and this command returns 0.
|
|
//
|
|
// For more information about the command please refer to [SREM].
|
|
//
|
|
// [SREM]: (https://redis.io/docs/latest/commands/srem/)
|
|
func (c cmdable) SRem(ctx context.Context, key string, members ...interface{}) *IntCmd {
|
|
args := make([]interface{}, 2, 2+len(members))
|
|
args[0] = "srem"
|
|
args[1] = key
|
|
args = appendArgs(args, members)
|
|
cmd := NewIntCmd(ctx, args...)
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Returns the members of the set resulting from the union of all the given sets.
|
|
// Keys that do not exist are considered to be empty sets.
|
|
//
|
|
// For more information about the command please refer to [SUNION].
|
|
//
|
|
// [SUNION]: (https://redis.io/docs/latest/commands/sunion/)
|
|
func (c cmdable) SUnion(ctx context.Context, keys ...string) *StringSliceCmd {
|
|
args := make([]interface{}, 1+len(keys))
|
|
args[0] = "sunion"
|
|
for i, key := range keys {
|
|
args[1+i] = key
|
|
}
|
|
cmd := NewStringSliceCmd(ctx, args...)
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Stores the members of the set resulting from the union of all the given sets
|
|
// into destination.
|
|
// If destination already exists, it is overwritten.
|
|
//
|
|
// For more information about the command please refer to [SUNIONSTORE].
|
|
//
|
|
// [SUNIONSTORE]: (https://redis.io/docs/latest/commands/sunionstore/)
|
|
func (c cmdable) SUnionStore(ctx context.Context, destination string, keys ...string) *IntCmd {
|
|
args := make([]interface{}, 2+len(keys))
|
|
args[0] = "sunionstore"
|
|
args[1] = destination
|
|
for i, key := range keys {
|
|
args[2+i] = key
|
|
}
|
|
cmd := NewIntCmd(ctx, args...)
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Returns the cardinality of the union of all the given sets.
|
|
// Missing keys are treated as empty sets.
|
|
//
|
|
// For more information about the command please refer to [SUNIONCARD].
|
|
//
|
|
// [SUNIONCARD]: (https://redis.io/docs/latest/commands/sunioncard/)
|
|
func (c cmdable) SUnionCard(ctx context.Context, opts *SUnionCardOptions, keys ...string) *IntCmd {
|
|
if opts == nil {
|
|
opts = &SUnionCardOptions{}
|
|
}
|
|
numKeys := len(keys)
|
|
args := make([]interface{}, 0, 4+numKeys+1)
|
|
args = append(args, "sunioncard", numKeys)
|
|
for _, key := range keys {
|
|
args = append(args, key)
|
|
}
|
|
if opts.Approx {
|
|
args = append(args, "approx")
|
|
}
|
|
args = append(args, "limit", opts.Limit)
|
|
cmd := NewIntCmd(ctx, args...)
|
|
// Keys start after the numkeys arg: ["sunioncard", numKeys, key1, ...].
|
|
cmd.SetFirstKeyPos(2)
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Incrementally iterates the set elements stored at key.
|
|
// This is a cursor-based iterator that allows scanning large sets efficiently.
|
|
//
|
|
// Parameters:
|
|
// - cursor: The cursor value for the iteration (use 0 to start a new scan)
|
|
// - match: Optional pattern to match elements (empty string means no pattern)
|
|
// - count: Optional hint about how many elements to return per iteration
|
|
//
|
|
// For more information about the command please refer to [SSCAN].
|
|
//
|
|
// [SSCAN]: (https://redis.io/docs/latest/commands/sscan/)
|
|
func (c cmdable) SScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd {
|
|
args := []interface{}{"sscan", key, cursor}
|
|
if match != "" {
|
|
args = append(args, "match", match)
|
|
}
|
|
if count > 0 {
|
|
args = append(args, "count", count)
|
|
}
|
|
cmd := NewScanCmd(ctx, c, args...)
|
|
if hashtag.Present(match) {
|
|
cmd.SetFirstKeyPos(4)
|
|
}
|
|
_ = c(ctx, cmd)
|
|
return cmd
|
|
}
|