Documentation
¶
Overview ¶
Package secret provides wrapper types for sensitive configuration values that redact themselves everywhere a value is normally rendered - String, fmt, encoding/json, and log/slog - so a secret cannot leak through a stray log line or error message. Access the underlying value only through the explicit, greppable Reveal method.
Index ¶
- Constants
- type Bytes
- type String
- func (s String) Clone() String
- func (s String) GoString() string
- func (s String) IsZero() bool
- func (s String) LogValue() slog.Value
- func (s String) MarshalJSON() ([]byte, error)
- func (s String) Reveal() string
- func (s String) RevealBytes() []byte
- func (s String) Sensitive() bool
- func (s String) String() string
- func (s *String) Zero()
Examples ¶
Constants ¶
const Redacted = "[REDACTED]"
Redacted is the placeholder rendered in place of a secret value.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Bytes ¶
type Bytes struct {
// contains filtered or unexported fields
}
Bytes is a sensitive byte slice with the same redaction contract as String.
Example ¶
Bytes is the same guarantee for binary secrets such as a private key or a certificate, and Zero scrubs the backing memory when the value is done.
package main
import (
"fmt"
"github.com/xavidop/mamori/secret"
)
func main() {
key := secret.NewBytes([]byte("-----BEGIN PRIVATE KEY-----"))
fmt.Println(key)
fmt.Println(len(key.Reveal()), "bytes revealed")
}
Output: [REDACTED] 27 bytes revealed
func (Bytes) Clone ¶ added in v1.4.0
Clone returns a copy backed by its own bytes, so the caller can Zero it without touching any other copy. See String.Clone.
func (Bytes) LogValue ¶
LogValue implements slog.LogValuer so structured logs redact by construction.
func (Bytes) MarshalJSON ¶
MarshalJSON renders the redaction placeholder, never the value.
func (*Bytes) Zero ¶
func (b *Bytes) Zero()
Zero best-effort wipes the underlying bytes. Only call it on a secret whose bytes you own: copies share one backing array, so zeroing any copy zeroes every copy, including the live one the reconciler is serving. Use Clone to take ownership first. See String.Zero for the full rationale.
type String ¶
type String struct {
// contains filtered or unexported fields
}
String is a sensitive string. Its zero value is a valid empty secret.
String deliberately does NOT expose the value through String(), fmt verbs, JSON marshaling, or slog. Use Reveal to obtain the plaintext at the exact point it is needed - those call sites are easy to audit in code review.
Example ¶
String redacts itself everywhere a value is normally printed. Nothing short of an explicit Reveal produces the plaintext, so an accidental log line or error message cannot leak a credential.
package main
import (
"fmt"
"github.com/xavidop/mamori/secret"
)
func main() {
pw := secret.NewString("hunter2")
fmt.Println(pw) // Stringer
fmt.Printf("%v\n", pw) // default verb
fmt.Printf("%s\n", pw) // string verb
fmt.Printf("%q\n", pw) // quoted
fmt.Println(pw.Reveal()) // the one explicit, greppable escape hatch
}
Output: [REDACTED] [REDACTED] [REDACTED] "[REDACTED]" hunter2
func NewStringBytes ¶
NewStringBytes wraps raw bytes as a secret string, taking ownership of b.
func (String) Clone ¶ added in v1.4.0
Clone returns a copy backed by its own bytes, so the caller can Zero it without touching any other copy.
This is the safe way to wipe a secret that came from Watcher.Get: that returns your config by value, which shares the secret's backing array with the reconciler and with every other caller. Clone breaks the sharing, and only then is Zero yours to call.
A cloned nil or empty secret stays empty rather than allocating.
func (String) LogValue ¶
LogValue implements slog.LogValuer so structured logs redact by construction.
Example ¶
String implements slog.LogValuer, so structured logging redacts it too, even when it is passed straight to a log call.
package main
import (
"log/slog"
"os"
"github.com/xavidop/mamori/secret"
)
func main() {
// A handler with time and level stripped, so the example output is stable.
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey || a.Key == slog.LevelKey {
return slog.Attr{}
}
return a
},
}))
logger.Info("connecting", "user", "svc-api", "password", secret.NewString("hunter2"))
}
Output: msg=connecting user=svc-api password=[REDACTED]
func (String) MarshalJSON ¶
MarshalJSON renders the redaction placeholder, never the value.
Example ¶
Marshaling a struct that carries a String emits the redaction, not the secret, so a config dump or an API response is safe by construction.
package main
import (
"encoding/json"
"fmt"
"github.com/xavidop/mamori/secret"
)
func main() {
type Config struct {
User string `json:"user"`
Password secret.String `json:"password"`
}
out, err := json.Marshal(Config{User: "svc-api", Password: secret.NewString("hunter2")})
if err != nil {
fmt.Println("marshal failed:", err)
return
}
fmt.Println(string(out))
}
Output: {"user":"svc-api","password":"[REDACTED]"}
func (String) Reveal ¶
Reveal returns the plaintext value. This is the only way to read it; keep such call sites minimal and reviewable.
Example ¶
Reveal is deliberately the only way out. Keeping it explicit makes every place a secret becomes plaintext a one-line grep away in code review.
package main
import (
"fmt"
"github.com/xavidop/mamori/secret"
)
func main() {
token := secret.NewString("sk-live-abc123")
// Pass the plaintext at the exact point it is needed, not before.
authHeader := "Bearer " + token.Reveal()
fmt.Println("stored: ", token)
fmt.Println("on wire:", authHeader)
}
Output: stored: [REDACTED] on wire: Bearer sk-live-abc123
func (String) RevealBytes ¶
RevealBytes returns the underlying bytes. Callers must not mutate the result.
func (*String) Zero ¶
func (s *String) Zero()
Zero best-effort wipes the underlying bytes. This is a defense-in-depth measure only: Go's garbage collector may have already copied the value elsewhere (during string conversion, interface boxing, or GC compaction), so zeroization cannot be guaranteed.
Only call this on a secret whose bytes you own. A String is a struct holding a slice, so copying one copies the slice header and shares the backing array: every copy reads through to the same bytes, and zeroing any of them zeroes all of them.
That matters because Watcher.Get returns your config by value, which copies the struct without copying the secret's bytes. Zeroing a secret obtained that way does not wipe "your" copy, it wipes the live one the reconciler is still serving, and every other caller's too. A request in flight would authenticate with null bytes.
Use Clone to take ownership first when you need to wipe:
pw := cfg.DBPassword.Clone() defer pw.Zero() db.Connect(pw.Reveal())
mamori itself never calls Zero. It cannot know when the last caller has finished with a superseded value, so wiping one on rotation would be a use after free with extra steps.