secrets
packageAPI reference for the secrets
package.
Imports
(18)CipherStore
CipherStore wraps a Store and encrypts values at rest.
type CipherStore struct
Methods
Parameters
Returns
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...))
}
Parameters
Returns
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
}
Parameters
Returns
func (*CipherStore) Delete(key string) error
{
return c.store.Delete(key)
}
Fields
| Name | Type | Description |
|---|---|---|
| store | Store | |
| key | []byte |
Uses
NewCipherStore
NewCipherStore creates a store that encrypts values with AES-GCM.
Parameters
Returns
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
}
Uses
PrefixStore
PrefixStore adds a namespace prefix to keys.
type PrefixStore struct
Methods
Parameters
Returns
func (*PrefixStore) Set(key string, value []byte) error
{
return p.store.Set(p.prefix+key, value)
}
Parameters
Returns
func (*PrefixStore) Get(key string) ([]byte, error)
{
return p.store.Get(p.prefix + key)
}
Parameters
Returns
func (*PrefixStore) Delete(key string) error
{
return p.store.Delete(p.prefix + key)
}
Fields
| Name | Type | Description |
|---|---|---|
| store | Store | |
| prefix | string |
Uses
NewPrefixStore
NewPrefixStore wraps a Store and prepends the given prefix to all keys.
Parameters
Returns
func NewPrefixStore(store Store, prefix string) *PrefixStore
{
return &PrefixStore{store: store, prefix: prefix}
}
Uses
FallbackStore
FallbackStore tries the primary store first, then falls back to secondary.
type FallbackStore struct
Methods
Parameters
Returns
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
}
Parameters
Returns
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
}
Parameters
Returns
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
}
NewFallbackStore
NewFallbackStore creates a store that tries primary first, then falls back to secondary.
Returns
func NewFallbackStore(primary, secondary Store) *FallbackStore
{
return &FallbackStore{primary: primary, secondary: secondary}
}
TestCipherStoreBindsCiphertextToKey
Parameters
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")
}
}
TestCipherStoreClonesEncryptionKey
Parameters
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)
}
}
failingStore
type failingStore struct
Methods
Parameters
Returns
func (failingStore) Set(string, []byte) error
{ return s.err }
Parameters
Returns
func (failingStore) Get(string) ([]byte, error)
{ return nil, s.err }
Parameters
Returns
func (failingStore) Delete(string) error
{ return s.err }
Fields
| Name | Type | Description |
|---|---|---|
| err | error |
TestFallbackStoreDoesNotHidePrimaryFailures
Parameters
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)
}
}
TestFallbackStoreUsesSecondaryForExpectedConditions
Parameters
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)
}
}
Store
Store is the shared contract for secret backends.
type Store interface
MemoryStore
MemoryStore is a thread-safe in-memory store intended for tests and ephemeral use.
type MemoryStore struct
Methods
Set stores a copy of the provided secret value.
Parameters
Returns
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 returns a copy of the stored secret value.
Parameters
Returns
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 removes a secret from the in-memory store.
Parameters
Returns
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 |
NewMemoryStore
NewMemoryStore creates a new in-memory store.
Returns
func NewMemoryStore() *MemoryStore
{
return &MemoryStore{m: make(map[string][]byte)}
}
EnvStore
EnvStore provides read-only access to environment variables.
type EnvStore struct
Methods
Set reports that environment-backed stores are read-only.
Parameters
Returns
func (*EnvStore) Set(key string, value []byte) error
{
return ErrReadOnly
}
Get returns the environment variable value for the provided key.
Parameters
Returns
func (*EnvStore) Get(key string) ([]byte, error)
{
v, ok := os.LookupEnv(key)
if !ok {
return nil, ErrNotFound
}
return []byte(v), nil
}
Delete reports that environment-backed stores are read-only.
Parameters
Returns
func (*EnvStore) Delete(key string) error
{
return ErrReadOnly
}
NewEnvStore
NewEnvStore creates a new environment-backed secret store.
Returns
func NewEnvStore() *EnvStore
{
return &EnvStore{}
}
VaultStore
VaultStore stores secrets in a HashiCorp Vault KV v2 mount.
type VaultStore struct
Methods
Set stores a secret value in Vault.
Parameters
Returns
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 reads a secret value from Vault.
Parameters
Returns
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 removes a secret from Vault.
Parameters
Returns
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)
}
Returns
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
}
Parameters
Returns
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 |
VaultOption
VaultOption configures a VaultStore.
type VaultOption func(*VaultStore)
NewVaultStore
NewVaultStore creates a Vault-backed store.
Parameters
Returns
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
}
WithVaultAddress
WithVaultAddress sets the Vault base URL.
Parameters
Returns
func WithVaultAddress(addr string) VaultOption
{
return func(v *VaultStore) { v.addr = strings.TrimRight(addr, "/") }
}
WithVaultToken
WithVaultToken sets the Vault token.
Parameters
Returns
func WithVaultToken(token string) VaultOption
{
return func(v *VaultStore) { v.token = token }
}
WithVaultMount
WithVaultMount sets the KV v2 mount name.
Parameters
Returns
func WithVaultMount(mount string) VaultOption
{
return func(v *VaultStore) { v.mount = strings.Trim(mount, "/") }
}
WithVaultClient
WithVaultClient sets the HTTP client.
Parameters
Returns
func WithVaultClient(client *http.Client) VaultOption
{
return func(v *VaultStore) {
if client != nil {
v.client = client
}
}
}
escapeVaultKey
Parameters
Returns
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
}
vaultStatus
Parameters
Returns
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))
}
isLoopbackHTTP
Parameters
Returns
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()
}
defaultVaultClient
Returns
func defaultVaultClient() *http.Client
{
return secureVaultClient(http.DefaultClient)
}
secureVaultClient
Parameters
Returns
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
}