secrets API

secrets

package

API reference for the secrets package.

S
struct
Implements: Store

CipherStore

CipherStore wraps a Store and encrypts values at rest.

core/secrets/extras.go:13-16
type CipherStore struct

Methods

Set
Method

Parameters

key string
value []byte

Returns

error
func (*CipherStore) Set(key string, value []byte) error
{
	block, err := aes.NewCipher(c.key)
	if err != nil {
		return err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return err
	}
	nonce := make([]byte, gcm.NonceSize())
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return err
	}
	ciphertext := gcm.Seal(nil, nonce, value, []byte(key))
	return c.store.Set(key, append(nonce, ciphertext...))
}
Get
Method

Parameters

key string

Returns

[]byte
error
func (*CipherStore) Get(key string) ([]byte, error)
{
	data, err := c.store.Get(key)
	if err != nil {
		return nil, err
	}
	block, err := aes.NewCipher(c.key)
	if err != nil {
		return nil, err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	nonceSize := gcm.NonceSize()
	if len(data) < nonceSize {
		return nil, errors.New("secrets: ciphertext too short")
	}
	plaintext, err := gcm.Open(nil, data[:nonceSize], data[nonceSize:], []byte(key))
	if err != nil {
		return nil, fmt.Errorf("secrets: decrypt failed: %w", err)
	}
	return plaintext, nil
}
Delete
Method

Parameters

key string

Returns

error
func (*CipherStore) Delete(key string) error
{
	return c.store.Delete(key)
}

Fields

Name Type Description
store Store
key []byte
F
function

NewCipherStore

NewCipherStore creates a store that encrypts values with AES-GCM.

Parameters

store
key
[]byte

Returns

error
core/secrets/extras.go:19-24
func NewCipherStore(store Store, key []byte) (*CipherStore, error)

{
	if len(key) != 32 {
		return nil, errors.New("secrets: cipher key must be 32 bytes")
	}
	return &CipherStore{store: store, key: append([]byte(nil), key...)}, nil
}
S
struct
Implements: Store

PrefixStore

PrefixStore adds a namespace prefix to keys.

core/secrets/extras.go:72-75
type PrefixStore struct

Methods

Set
Method

Parameters

key string
value []byte

Returns

error
func (*PrefixStore) Set(key string, value []byte) error
{
	return p.store.Set(p.prefix+key, value)
}
Get
Method

Parameters

key string

Returns

[]byte
error
func (*PrefixStore) Get(key string) ([]byte, error)
{
	return p.store.Get(p.prefix + key)
}
Delete
Method

Parameters

key string

Returns

error
func (*PrefixStore) Delete(key string) error
{
	return p.store.Delete(p.prefix + key)
}

Fields

Name Type Description
store Store
prefix string
F
function

NewPrefixStore

NewPrefixStore wraps a Store and prepends the given prefix to all keys.

Parameters

store
prefix
string

Returns

core/secrets/extras.go:78-80
func NewPrefixStore(store Store, prefix string) *PrefixStore

{
	return &PrefixStore{store: store, prefix: prefix}
}
S
struct
Implements: Store

FallbackStore

FallbackStore tries the primary store first, then falls back to secondary.

core/secrets/extras.go:95-98
type FallbackStore struct

Methods

Set
Method

Parameters

key string
value []byte

Returns

error
func (*FallbackStore) Set(key string, value []byte) error
{
	err := f.primary.Set(key, value)
	if errors.Is(err, ErrReadOnly) || errors.Is(err, ErrNotConfigured) {
		return f.secondary.Set(key, value)
	}
	return err
}
Get
Method

Parameters

key string

Returns

[]byte
error
func (*FallbackStore) Get(key string) ([]byte, error)
{
	v, err := f.primary.Get(key)
	if err == nil {
		return v, nil
	}
	if errors.Is(err, ErrNotFound) {
		return f.secondary.Get(key)
	}
	return nil, err
}
Delete
Method

Parameters

key string

Returns

error
func (*FallbackStore) Delete(key string) error
{
	err1 := f.primary.Delete(key)
	if err1 != nil && !errors.Is(err1, ErrNotFound) {
		return err1
	}
	err2 := f.secondary.Delete(key)
	if err2 != nil && !errors.Is(err2, ErrNotFound) {
		return err2
	}
	return nil
}

Fields

Name Type Description
primary Store
secondary Store
F
function

NewFallbackStore

NewFallbackStore creates a store that tries primary first, then falls back to secondary.

Parameters

primary
secondary

Returns

core/secrets/extras.go:101-103
func NewFallbackStore(primary, secondary Store) *FallbackStore

{
	return &FallbackStore{primary: primary, secondary: secondary}
}
F
function

TestCipherStoreBindsCiphertextToKey

Parameters

core/secrets/extras_test.go:9-31
func TestCipherStoreBindsCiphertextToKey(t *testing.T)

{
	raw := NewMemoryStore()
	store, err := NewCipherStore(raw, bytes.Repeat([]byte{1}, 32))
	if err != nil {
		t.Fatal(err)
	}
	if err := store.Set("one", []byte("first")); err != nil {
		t.Fatal(err)
	}
	if err := store.Set("two", []byte("second")); err != nil {
		t.Fatal(err)
	}
	second, err := raw.Get("two")
	if err != nil {
		t.Fatal(err)
	}
	if err := raw.Set("one", second); err != nil {
		t.Fatal(err)
	}
	if _, err := store.Get("one"); err == nil {
		t.Fatal("Get() accepted ciphertext copied from another key")
	}
}
F
function

TestCipherStoreClonesEncryptionKey

Parameters

core/secrets/extras_test.go:33-50
func TestCipherStoreClonesEncryptionKey(t *testing.T)

{
	key := bytes.Repeat([]byte{1}, 32)
	store, err := NewCipherStore(NewMemoryStore(), key)
	if err != nil {
		t.Fatal(err)
	}
	key[0] = 2
	if err := store.Set("key", []byte("value")); err != nil {
		t.Fatal(err)
	}
	value, err := store.Get("key")
	if err != nil {
		t.Fatal(err)
	}
	if string(value) != "value" {
		t.Fatalf("Get() = %q", value)
	}
}
S
struct

failingStore

core/secrets/extras_test.go:52-54
type failingStore struct

Methods

Set
Method

Parameters

string
[]byte

Returns

error
func (failingStore) Set(string, []byte) error
{ return s.err }
Get
Method

Parameters

string

Returns

[]byte
error
func (failingStore) Get(string) ([]byte, error)
{ return nil, s.err }
Delete
Method

Parameters

string

Returns

error
func (failingStore) Delete(string) error
{ return s.err }

Fields

Name Type Description
err error
F
function

TestFallbackStoreDoesNotHidePrimaryFailures

Parameters

core/secrets/extras_test.go:60-72
func TestFallbackStoreDoesNotHidePrimaryFailures(t *testing.T)

{
	primaryErr := errors.New("authentication failed")
	secondary := NewMemoryStore()
	_ = secondary.Set("key", []byte("stale"))
	store := NewFallbackStore(failingStore{err: primaryErr}, secondary)

	if _, err := store.Get("key"); !errors.Is(err, primaryErr) {
		t.Fatalf("Get() error = %v, want primary error", err)
	}
	if err := store.Set("key", []byte("secret")); !errors.Is(err, primaryErr) {
		t.Fatalf("Set() error = %v, want primary error", err)
	}
}
F
function

TestFallbackStoreUsesSecondaryForExpectedConditions

Parameters

core/secrets/extras_test.go:74-90
func TestFallbackStoreUsesSecondaryForExpectedConditions(t *testing.T)

{
	secondary := NewMemoryStore()
	store := NewFallbackStore(failingStore{err: ErrNotFound}, secondary)
	_ = secondary.Set("key", []byte("value"))
	value, err := store.Get("key")
	if err != nil {
		t.Fatal(err)
	}
	if string(value) != "value" {
		t.Fatalf("Get() = %q", value)
	}

	store = NewFallbackStore(failingStore{err: ErrReadOnly}, secondary)
	if err := store.Set("other", []byte("stored")); err != nil {
		t.Fatal(err)
	}
}
I
interface

Store

Store is the shared contract for secret backends.

core/secrets/secrets.go:42-46
type Store interface

Methods

Set
Method

Parameters

key string
value []byte

Returns

error
func Set(...)
Get
Method

Parameters

key string

Returns

[]byte
error
func Get(...)
Delete
Method

Parameters

key string

Returns

error
func Delete(...)
S
struct
Implements: Store

MemoryStore

MemoryStore is a thread-safe in-memory store intended for tests and ephemeral use.

core/secrets/secrets.go:49-52
type MemoryStore struct

Methods

Set
Method

Set stores a copy of the provided secret value.

Parameters

key string
value []byte

Returns

error
func (*MemoryStore) Set(key string, value []byte) error
{
	s.mu.Lock()
	defer s.mu.Unlock()
	s.m[key] = append([]byte(nil), value...)
	return nil
}
Get
Method

Get returns a copy of the stored secret value.

Parameters

key string

Returns

[]byte
error
func (*MemoryStore) Get(key string) ([]byte, error)
{
	s.mu.RLock()
	defer s.mu.RUnlock()

	v, ok := s.m[key]
	if !ok {
		return nil, ErrNotFound
	}

	return append([]byte(nil), v...), nil
}
Delete
Method

Delete removes a secret from the in-memory store.

Parameters

key string

Returns

error
func (*MemoryStore) Delete(key string) error
{
	s.mu.Lock()
	defer s.mu.Unlock()
	delete(s.m, key)
	return nil
}

Fields

Name Type Description
mu sync.RWMutex
m map[string][]byte
F
function

NewMemoryStore

NewMemoryStore creates a new in-memory store.

Returns

core/secrets/secrets.go:55-57
func NewMemoryStore() *MemoryStore

{
	return &MemoryStore{m: make(map[string][]byte)}
}
S
struct
Implements: Store

EnvStore

EnvStore provides read-only access to environment variables.

core/secrets/secrets.go:89-89
type EnvStore struct

Methods

Set
Method

Set reports that environment-backed stores are read-only.

Parameters

key string
value []byte

Returns

error
func (*EnvStore) Set(key string, value []byte) error
{
	return ErrReadOnly
}
Get
Method

Get returns the environment variable value for the provided key.

Parameters

key string

Returns

[]byte
error
func (*EnvStore) Get(key string) ([]byte, error)
{
	v, ok := os.LookupEnv(key)
	if !ok {
		return nil, ErrNotFound
	}
	return []byte(v), nil
}
Delete
Method

Delete reports that environment-backed stores are read-only.

Parameters

key string

Returns

error
func (*EnvStore) Delete(key string) error
{
	return ErrReadOnly
}
F
function

NewEnvStore

NewEnvStore creates a new environment-backed secret store.

Returns

core/secrets/secrets.go:92-94
func NewEnvStore() *EnvStore

{
	return &EnvStore{}
}
S
struct
Implements: Store

VaultStore

VaultStore stores secrets in a HashiCorp Vault KV v2 mount.

core/secrets/secrets.go:116-121
type VaultStore struct

Methods

Set
Method

Set stores a secret value in Vault.

Parameters

key string
value []byte

Returns

error
func (*VaultStore) Set(key string, value []byte) error
{
	if err := v.ready(); err != nil {
		return err
	}
	if len(value) > maxVaultSecretSize {
		return errors.New("secrets: vault value exceeds size limit")
	}
	body := map[string]any{
		"data": map[string]string{
			"value": base64.StdEncoding.EncodeToString(value),
		},
	}
	data, err := json.Marshal(body)
	if err != nil {
		return err
	}
	req, err := v.request(http.MethodPut, key, bytes.NewReader(data))
	if err != nil {
		return err
	}
	resp, err := v.client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	return vaultStatus(resp, http.StatusOK, http.StatusNoContent)
}
Get
Method

Get reads a secret value from Vault.

Parameters

key string

Returns

[]byte
error
func (*VaultStore) Get(key string) ([]byte, error)
{
	if err := v.ready(); err != nil {
		return nil, err
	}
	req, err := v.request(http.MethodGet, key, nil)
	if err != nil {
		return nil, err
	}
	resp, err := v.client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusNotFound {
		return nil, ErrNotFound
	}
	if err := vaultStatus(resp, http.StatusOK); err != nil {
		return nil, err
	}

	var out struct {
		Data struct {
			Data map[string]string `json:"data"`
		} `json:"data"`
	}
	data, err := io.ReadAll(io.LimitReader(resp.Body, maxVaultResponseSize+1))
	if err != nil {
		return nil, err
	}
	if len(data) > maxVaultResponseSize {
		return nil, errors.New("secrets: vault response exceeds size limit")
	}
	if err := json.Unmarshal(data, &out); err != nil {
		return nil, err
	}
	encoded, ok := out.Data.Data["value"]
	if !ok {
		return nil, ErrNotFound
	}
	value, err := base64.StdEncoding.DecodeString(encoded)
	if err != nil {
		return nil, fmt.Errorf("secrets: vault decode failed: %w", err)
	}
	return value, nil
}
Delete
Method

Delete removes a secret from Vault.

Parameters

key string

Returns

error
func (*VaultStore) Delete(key string) error
{
	if err := v.ready(); err != nil {
		return err
	}
	req, err := v.request(http.MethodDelete, key, nil)
	if err != nil {
		return err
	}
	resp, err := v.client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	return vaultStatus(resp, http.StatusOK, http.StatusNoContent, http.StatusNotFound)
}
ready
Method

Returns

error
func (*VaultStore) ready() error
{
	if v.addr == "" || v.token == "" || v.mount == "" || v.client == nil {
		return ErrNotConfigured
	}
	parsed, err := url.Parse(v.addr)
	if err != nil || parsed.Host == "" {
		return ErrNotConfigured
	}
	if parsed.Scheme != "https" && !isLoopbackHTTP(parsed) {
		return ErrInsecureVaultAddress
	}
	return nil
}
request
Method

Parameters

method string
key string
body io.Reader

Returns

error
func (*VaultStore) request(method, key string, body io.Reader) (*http.Request, error)
{
	escapedKey, err := escapeVaultKey(key)
	if err != nil {
		return nil, err
	}
	u := strings.TrimRight(v.addr, "/") + "/v1/" + url.PathEscape(v.mount) + "/data/" + escapedKey
	req, err := http.NewRequest(method, u, body)
	if err != nil {
		return nil, err
	}
	req.Header.Set("X-Vault-Token", v.token)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	return req, nil
}

Fields

Name Type Description
addr string
token string
mount string
client *http.Client
T
type

VaultOption

VaultOption configures a VaultStore.

core/secrets/secrets.go:124-124
type VaultOption func(*VaultStore)
F
function

NewVaultStore

NewVaultStore creates a Vault-backed store.

Parameters

opts
...VaultOption

Returns

core/secrets/secrets.go:127-139
func NewVaultStore(opts ...VaultOption) *VaultStore

{
	store := &VaultStore{
		addr:   os.Getenv("VAULT_ADDR"),
		token:  os.Getenv("VAULT_TOKEN"),
		mount:  "secret",
		client: defaultVaultClient(),
	}
	for _, opt := range opts {
		opt(store)
	}
	store.client = secureVaultClient(store.client)
	return store
}
F
function

WithVaultAddress

WithVaultAddress sets the Vault base URL.

Parameters

addr
string

Returns

core/secrets/secrets.go:142-144
func WithVaultAddress(addr string) VaultOption

{
	return func(v *VaultStore) { v.addr = strings.TrimRight(addr, "/") }
}
F
function

WithVaultToken

WithVaultToken sets the Vault token.

Parameters

token
string

Returns

core/secrets/secrets.go:147-149
func WithVaultToken(token string) VaultOption

{
	return func(v *VaultStore) { v.token = token }
}
F
function

WithVaultMount

WithVaultMount sets the KV v2 mount name.

Parameters

mount
string

Returns

core/secrets/secrets.go:152-154
func WithVaultMount(mount string) VaultOption

{
	return func(v *VaultStore) { v.mount = strings.Trim(mount, "/") }
}
F
function

WithVaultClient

WithVaultClient sets the HTTP client.

Parameters

client

Returns

core/secrets/secrets.go:157-163
func WithVaultClient(client *http.Client) VaultOption

{
	return func(v *VaultStore) {
		if client != nil {
			v.client = client
		}
	}
}
F
function

escapeVaultKey

Parameters

key
string

Returns

string
error
core/secrets/secrets.go:289-301
func escapeVaultKey(key string) (string, error)

{
	parts := strings.Split(strings.Trim(key, "/"), "/")
	if len(parts) == 0 || parts[0] == "" {
		return "", ErrInvalidKey
	}
	for i, part := range parts {
		if part == "" || part == "." || part == ".." {
			return "", ErrInvalidKey
		}
		parts[i] = url.PathEscape(part)
	}
	return strings.Join(parts, "/"), nil
}
F
function

vaultStatus

Parameters

allowed
...int

Returns

error
core/secrets/secrets.go:303-311
func vaultStatus(resp *http.Response, allowed ...int) error

{
	for _, code := range allowed {
		if resp.StatusCode == code {
			return nil
		}
	}
	body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
	return fmt.Errorf("secrets: vault status %d: %s", resp.StatusCode, string(body))
}
F
function

isLoopbackHTTP

Parameters

u

Returns

bool
core/secrets/secrets.go:313-323
func isLoopbackHTTP(u *url.URL) bool

{
	if u.Scheme != "http" {
		return false
	}
	host := u.Hostname()
	if host == "localhost" {
		return true
	}
	ip := net.ParseIP(host)
	return ip != nil && ip.IsLoopback()
}
F
function

defaultVaultClient

Returns

core/secrets/secrets.go:325-327
func defaultVaultClient() *http.Client

{
	return secureVaultClient(http.DefaultClient)
}
F
function

secureVaultClient

Parameters

source

Returns

core/secrets/secrets.go:329-341
func secureVaultClient(source *http.Client) *http.Client

{
	if source == nil {
		source = http.DefaultClient
	}
	client := *source
	if client.Timeout == 0 {
		client.Timeout = defaultVaultTimeout
	}
	client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
		return http.ErrUseLastResponse
	}
	return &client
}