secret

package
v1.12.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: MIT Imports: 1 Imported by: 0

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

Examples

Constants

View Source
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 NewBytes

func NewBytes(b []byte) Bytes

NewBytes wraps b as a secret, taking ownership of the slice.

func (Bytes) Clone added in v1.4.0

func (b Bytes) Clone() Bytes

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) GoString

func (b Bytes) GoString() string

GoString implements fmt.GoStringer so %#v also redacts.

func (Bytes) IsZero

func (b Bytes) IsZero() bool

IsZero reports whether the secret holds no bytes.

func (Bytes) LogValue

func (b Bytes) LogValue() slog.Value

LogValue implements slog.LogValuer so structured logs redact by construction.

func (Bytes) MarshalJSON

func (b Bytes) MarshalJSON() ([]byte, error)

MarshalJSON renders the redaction placeholder, never the value.

func (Bytes) Reveal

func (b Bytes) Reveal() []byte

Reveal returns the underlying bytes. Callers must not mutate the result.

func (Bytes) Sensitive

func (b Bytes) Sensitive() bool

Sensitive always reports true.

func (Bytes) String

func (b Bytes) String() string

String implements fmt.Stringer and returns the redaction placeholder.

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 NewString

func NewString(s string) String

NewString wraps s as a secret.

func NewStringBytes

func NewStringBytes(b []byte) String

NewStringBytes wraps raw bytes as a secret string, taking ownership of b.

func (String) Clone added in v1.4.0

func (s String) Clone() String

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) GoString

func (s String) GoString() string

GoString implements fmt.GoStringer so %#v also redacts.

func (String) IsZero

func (s String) IsZero() bool

IsZero reports whether the secret holds no bytes.

func (String) LogValue

func (s String) LogValue() slog.Value

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

func (s String) MarshalJSON() ([]byte, error)

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

func (s String) Reveal() string

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

func (s String) RevealBytes() []byte

RevealBytes returns the underlying bytes. Callers must not mutate the result.

func (String) Sensitive

func (s String) Sensitive() bool

Sensitive always reports true.

func (String) String

func (s String) String() string

String implements fmt.Stringer and returns the redaction placeholder.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL