hush/vendor/google.golang.org/protobuf/internal/impl/extension.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

157 lines
4.6 KiB
Go

// Copyright 2019 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 impl
import (
"reflect"
"sync"
"sync/atomic"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/runtime/protoiface"
)
// ExtensionInfo implements ExtensionType.
//
// This type contains a number of exported fields for legacy compatibility.
// The only non-deprecated use of this type is through the methods of the
// ExtensionType interface.
type ExtensionInfo struct {
// An ExtensionInfo may exist in several stages of initialization.
//
// extensionInfoUninitialized: Some or all of the legacy exported
// fields may be set, but none of the unexported fields have been
// initialized. This is the starting state for an ExtensionInfo
// in legacy generated code.
//
// extensionInfoDescInit: The desc field is set, but other unexported fields
// may not be initialized. Legacy exported fields may or may not be set.
// This is the starting state for an ExtensionInfo in newly generated code.
//
// extensionInfoFullInit: The ExtensionInfo is fully initialized.
// This state is only entered after lazy initialization is complete.
init uint32
mu sync.Mutex
goType reflect.Type
desc extensionTypeDescriptor
conv Converter
info *extensionFieldInfo // for fast-path method implementations
// ExtendedType is a typed nil-pointer to the parent message type that
// is being extended. It is possible for this to be unpopulated in v2
// since the message may no longer implement the MessageV1 interface.
//
// Deprecated: Use the ExtendedType method instead.
ExtendedType protoiface.MessageV1
// ExtensionType is the zero value of the extension type.
//
// For historical reasons, reflect.TypeOf(ExtensionType) and the
// type returned by InterfaceOf may not be identical.
//
// Deprecated: Use InterfaceOf(xt.Zero()) instead.
ExtensionType any
// Field is the field number of the extension.
//
// Deprecated: Use the Descriptor().Number method instead.
Field int32
// Name is the fully qualified name of extension.
//
// Deprecated: Use the Descriptor().FullName method instead.
Name string
// Tag is the protobuf struct tag used in the v1 API.
//
// Deprecated: Do not use.
Tag string
// Filename is the proto filename in which the extension is defined.
//
// Deprecated: Use Descriptor().ParentFile().Path() instead.
Filename string
}
// Stages of initialization: See the ExtensionInfo.init field.
const (
extensionInfoUninitialized = 0
extensionInfoDescInit = 1
extensionInfoFullInit = 2
)
func InitExtensionInfo(xi *ExtensionInfo, xd protoreflect.ExtensionDescriptor, goType reflect.Type) {
xi.goType = goType
xi.desc = extensionTypeDescriptor{xd, xi}
xi.init = extensionInfoDescInit
}
func (xi *ExtensionInfo) New() protoreflect.Value {
return xi.lazyInit().New()
}
func (xi *ExtensionInfo) Zero() protoreflect.Value {
return xi.lazyInit().Zero()
}
func (xi *ExtensionInfo) ValueOf(v any) protoreflect.Value {
return xi.lazyInit().PBValueOf(reflect.ValueOf(v))
}
func (xi *ExtensionInfo) InterfaceOf(v protoreflect.Value) any {
return xi.lazyInit().GoValueOf(v).Interface()
}
func (xi *ExtensionInfo) IsValidValue(v protoreflect.Value) bool {
return xi.lazyInit().IsValidPB(v)
}
func (xi *ExtensionInfo) IsValidInterface(v any) bool {
return xi.lazyInit().IsValidGo(reflect.ValueOf(v))
}
func (xi *ExtensionInfo) TypeDescriptor() protoreflect.ExtensionTypeDescriptor {
if atomic.LoadUint32(&xi.init) < extensionInfoDescInit {
xi.lazyInitSlow()
}
return &xi.desc
}
func (xi *ExtensionInfo) lazyInit() Converter {
if atomic.LoadUint32(&xi.init) < extensionInfoFullInit {
xi.lazyInitSlow()
}
return xi.conv
}
func (xi *ExtensionInfo) lazyInitSlow() {
xi.mu.Lock()
defer xi.mu.Unlock()
if xi.init == extensionInfoFullInit {
return
}
defer atomic.StoreUint32(&xi.init, extensionInfoFullInit)
if xi.desc.ExtensionDescriptor == nil {
xi.initFromLegacy()
}
if !xi.desc.ExtensionDescriptor.IsPlaceholder() {
if xi.ExtensionType == nil {
xi.initToLegacy()
}
xi.conv = NewConverter(xi.goType, xi.desc.ExtensionDescriptor)
xi.info = makeExtensionFieldInfo(xi.desc.ExtensionDescriptor)
xi.info.validation = newValidationInfo(xi.desc.ExtensionDescriptor, xi.goType)
}
}
type extensionTypeDescriptor struct {
protoreflect.ExtensionDescriptor
xi *ExtensionInfo
}
func (xtd *extensionTypeDescriptor) Type() protoreflect.ExtensionType {
return xtd.xi
}
func (xtd *extensionTypeDescriptor) Descriptor() protoreflect.ExtensionDescriptor {
return xtd.ExtensionDescriptor
}