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.
173 lines
5.3 KiB
Go
173 lines
5.3 KiB
Go
// Copyright 2024 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package filedesc
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"google.golang.org/protobuf/encoding/protowire"
|
|
"google.golang.org/protobuf/internal/editiondefaults"
|
|
"google.golang.org/protobuf/internal/genid"
|
|
"google.golang.org/protobuf/reflect/protoreflect"
|
|
)
|
|
|
|
var (
|
|
defaultsCache = make(map[Edition]EditionFeatures)
|
|
defaultsKeys = []Edition{}
|
|
)
|
|
|
|
func init() {
|
|
unmarshalEditionDefaults(editiondefaults.Defaults)
|
|
SurrogateProto2.L1.EditionFeatures = getFeaturesFor(EditionProto2)
|
|
SurrogateProto3.L1.EditionFeatures = getFeaturesFor(EditionProto3)
|
|
SurrogateEdition2023.L1.EditionFeatures = getFeaturesFor(Edition2023)
|
|
}
|
|
|
|
func unmarshalGoFeature(b []byte, parent EditionFeatures) EditionFeatures {
|
|
for len(b) > 0 {
|
|
num, _, n := protowire.ConsumeTag(b)
|
|
b = b[n:]
|
|
switch num {
|
|
case genid.GoFeatures_LegacyUnmarshalJsonEnum_field_number:
|
|
v, m := protowire.ConsumeVarint(b)
|
|
b = b[m:]
|
|
parent.GenerateLegacyUnmarshalJSON = protowire.DecodeBool(v)
|
|
case genid.GoFeatures_ApiLevel_field_number:
|
|
v, m := protowire.ConsumeVarint(b)
|
|
b = b[m:]
|
|
parent.APILevel = int(v)
|
|
case genid.GoFeatures_StripEnumPrefix_field_number:
|
|
v, m := protowire.ConsumeVarint(b)
|
|
b = b[m:]
|
|
parent.StripEnumPrefix = int(v)
|
|
default:
|
|
panic(fmt.Sprintf("unknown field number %d while unmarshalling GoFeatures", num))
|
|
}
|
|
}
|
|
return parent
|
|
}
|
|
|
|
func unmarshalFeatureSet(b []byte, parent EditionFeatures) EditionFeatures {
|
|
for len(b) > 0 {
|
|
num, typ, n := protowire.ConsumeTag(b)
|
|
b = b[n:]
|
|
switch typ {
|
|
case protowire.VarintType:
|
|
v, m := protowire.ConsumeVarint(b)
|
|
b = b[m:]
|
|
switch num {
|
|
case genid.FeatureSet_FieldPresence_field_number:
|
|
parent.IsFieldPresence = v == genid.FeatureSet_EXPLICIT_enum_value || v == genid.FeatureSet_LEGACY_REQUIRED_enum_value
|
|
parent.IsLegacyRequired = v == genid.FeatureSet_LEGACY_REQUIRED_enum_value
|
|
case genid.FeatureSet_EnumType_field_number:
|
|
parent.IsOpenEnum = v == genid.FeatureSet_OPEN_enum_value
|
|
case genid.FeatureSet_RepeatedFieldEncoding_field_number:
|
|
parent.IsPacked = v == genid.FeatureSet_PACKED_enum_value
|
|
case genid.FeatureSet_Utf8Validation_field_number:
|
|
parent.IsUTF8Validated = v == genid.FeatureSet_VERIFY_enum_value
|
|
case genid.FeatureSet_MessageEncoding_field_number:
|
|
parent.IsDelimitedEncoded = v == genid.FeatureSet_DELIMITED_enum_value
|
|
case genid.FeatureSet_JsonFormat_field_number:
|
|
parent.IsJSONCompliant = v == genid.FeatureSet_ALLOW_enum_value
|
|
case genid.FeatureSet_EnforceNamingStyle_field_number:
|
|
// EnforceNamingStyle is enforced in protoc, languages other than C++
|
|
// are not supposed to do anything with this feature.
|
|
case genid.FeatureSet_DefaultSymbolVisibility_field_number:
|
|
// DefaultSymbolVisibility is enforced in protoc, runtimes should not
|
|
// inspect this value.
|
|
default:
|
|
panic(fmt.Sprintf("unknown field number %d while unmarshalling FeatureSet", num))
|
|
}
|
|
case protowire.BytesType:
|
|
v, m := protowire.ConsumeBytes(b)
|
|
b = b[m:]
|
|
switch num {
|
|
case genid.FeatureSet_Go_ext_number:
|
|
parent = unmarshalGoFeature(v, parent)
|
|
}
|
|
}
|
|
}
|
|
|
|
return parent
|
|
}
|
|
|
|
func featuresFromParentDesc(parentDesc protoreflect.Descriptor) EditionFeatures {
|
|
var parentFS EditionFeatures
|
|
switch p := parentDesc.(type) {
|
|
case *File:
|
|
parentFS = p.L1.EditionFeatures
|
|
case *Message:
|
|
parentFS = p.L1.EditionFeatures
|
|
default:
|
|
panic(fmt.Sprintf("unknown parent type %T", parentDesc))
|
|
}
|
|
return parentFS
|
|
}
|
|
|
|
func unmarshalEditionDefault(b []byte) {
|
|
var ed Edition
|
|
var fs EditionFeatures
|
|
for len(b) > 0 {
|
|
num, typ, n := protowire.ConsumeTag(b)
|
|
b = b[n:]
|
|
switch typ {
|
|
case protowire.VarintType:
|
|
v, m := protowire.ConsumeVarint(b)
|
|
b = b[m:]
|
|
switch num {
|
|
case genid.FeatureSetDefaults_FeatureSetEditionDefault_Edition_field_number:
|
|
ed = Edition(v)
|
|
}
|
|
case protowire.BytesType:
|
|
v, m := protowire.ConsumeBytes(b)
|
|
b = b[m:]
|
|
switch num {
|
|
case genid.FeatureSetDefaults_FeatureSetEditionDefault_FixedFeatures_field_number:
|
|
fs = unmarshalFeatureSet(v, fs)
|
|
case genid.FeatureSetDefaults_FeatureSetEditionDefault_OverridableFeatures_field_number:
|
|
fs = unmarshalFeatureSet(v, fs)
|
|
}
|
|
}
|
|
}
|
|
defaultsCache[ed] = fs
|
|
defaultsKeys = append(defaultsKeys, ed)
|
|
}
|
|
|
|
func unmarshalEditionDefaults(b []byte) {
|
|
for len(b) > 0 {
|
|
num, _, n := protowire.ConsumeTag(b)
|
|
b = b[n:]
|
|
switch num {
|
|
case genid.FeatureSetDefaults_Defaults_field_number:
|
|
def, m := protowire.ConsumeBytes(b)
|
|
b = b[m:]
|
|
unmarshalEditionDefault(def)
|
|
case genid.FeatureSetDefaults_MinimumEdition_field_number,
|
|
genid.FeatureSetDefaults_MaximumEdition_field_number:
|
|
// We don't care about the minimum and maximum editions. If the
|
|
// edition we are looking for later on is not in the cache we know
|
|
// it is outside of the range between minimum and maximum edition.
|
|
_, m := protowire.ConsumeVarint(b)
|
|
b = b[m:]
|
|
default:
|
|
panic(fmt.Sprintf("unknown field number %d while unmarshalling EditionDefault", num))
|
|
}
|
|
}
|
|
}
|
|
|
|
func getFeaturesFor(ed Edition) EditionFeatures {
|
|
match := EditionUnknown
|
|
for _, key := range defaultsKeys {
|
|
if key > ed {
|
|
break
|
|
}
|
|
match = key
|
|
}
|
|
if match == EditionUnknown {
|
|
panic(fmt.Sprintf("unsupported edition: %v", ed))
|
|
}
|
|
return defaultsCache[match]
|
|
}
|