Documentation
¶
Overview ¶
Package secrets provides secure secret storage operations using AES-256-GCM encryption with OS keychain integration for master key management.
The package uses a hybrid approach for secret storage:
- Master encryption key is stored in the OS keychain (or env var fallback)
- Secrets are encrypted and stored as individual files
This allows for storing large secrets (e.g., auth tokens) that wouldn't fit in the OS keychain directly, while still leveraging secure key storage.
Basic usage:
store, err := secrets.New()
if err != nil {
log.Fatal(err)
}
// Store a secret
err = store.Set(ctx, "my-api-key", []byte("secret-value"))
// Retrieve a secret
value, err := store.Get(ctx, "my-api-key")
// List all secrets
names, err := store.List(ctx)
// Delete a secret
err = store.Delete(ctx, "my-api-key")
Index ¶
- Constants
- Variables
- func DeleteMasterKeyFromKeyring(kr Keyring) error
- func GetMasterKeyFromKeyring(kr Keyring) ([]byte, error)
- func IsValidName(name string) bool
- func SetMasterKeyInKeyring(kr Keyring, key []byte) error
- func ValidateName(name string) error
- type CorruptedSecretError
- type InvalidNameError
- type Keyring
- type KeyringCall
- type KeyringError
- type KeyringSetCall
- type MockKeyring
- type MockStore
- func (m *MockStore) Delete(_ context.Context, name string) error
- func (m *MockStore) Exists(_ context.Context, name string) (bool, error)
- func (m *MockStore) Get(_ context.Context, name string) ([]byte, error)
- func (m *MockStore) KeyringBackend() string
- func (m *MockStore) List(_ context.Context) ([]string, error)
- func (m *MockStore) Reset()
- func (m *MockStore) Rotate(_ context.Context) error
- func (m *MockStore) Set(_ context.Context, name string, value []byte) error
- type OSKeyring
- type Option
- type SetCall
- type Store
Constants ¶
const ( // KeyringBackendOS indicates the OS keyring was used. KeyringBackendOS = "os" // KeyringBackendEnv indicates the environment variable keyring was used. KeyringBackendEnv = "env" // KeyringBackendFile indicates the file-based keyring was used. KeyringBackendFile = "file" )
const ( // KeyringService is the service name used in the OS keyring. KeyringService = "scafctl" // KeyringMasterKeyAccount is the account name for the master encryption key. KeyringMasterKeyAccount = "master-key" // EnvSecretKey is the environment variable name for the fallback secret key. EnvSecretKey = "SCAFCTL_SECRET_KEY" //nolint:gosec // This is the env var name, not a credential )
const ( // MinNameLength is the minimum length for a secret name. MinNameLength = 1 // MaxNameLength is the maximum length for a secret name. MaxNameLength = 255 )
Variables ¶
var ( // ErrNotFound is returned when a secret does not exist. ErrNotFound = errors.New("secret not found") // ErrInvalidName is returned when a secret name is invalid. ErrInvalidName = errors.New("invalid secret name") // ErrCorrupted is returned when a secret file is corrupted and cannot be decrypted. ErrCorrupted = errors.New("secret is corrupted") // ErrKeyringAccess is returned when the OS keyring cannot be accessed. ErrKeyringAccess = errors.New("cannot access keyring") )
Sentinel errors for the secrets package.
var ErrKeyNotFound = errors.New("key not found in keyring")
ErrKeyNotFound is returned when a key is not found in the keyring.
Functions ¶
func DeleteMasterKeyFromKeyring ¶
DeleteMasterKeyFromKeyring removes the master encryption key from the keyring.
func GetMasterKeyFromKeyring ¶
GetMasterKeyFromKeyring retrieves the master encryption key from the keyring. The key is stored as base64-encoded string in the keyring.
func IsValidName ¶
IsValidName returns true if the secret name is valid.
func SetMasterKeyInKeyring ¶
SetMasterKeyInKeyring stores the master encryption key in the keyring. The key is stored as base64-encoded string.
func ValidateName ¶
ValidateName checks if a secret name is valid according to the naming rules. Returns nil if valid, or an InvalidNameError with details if invalid.
Types ¶
type CorruptedSecretError ¶
type CorruptedSecretError struct {
Name string `json:"name" yaml:"name" doc:"The name of the corrupted secret" example:"my-secret"`
Reason string `json:"reason" yaml:"reason" doc:"The reason the secret is corrupted" example:"invalid version byte"`
Cause error `json:"-" yaml:"-" doc:"The underlying error"`
}
CorruptedSecretError provides details about a corrupted secret.
func NewCorruptedSecretError ¶
func NewCorruptedSecretError(name, reason string, cause error) *CorruptedSecretError
NewCorruptedSecretError creates a new CorruptedSecretError with the given name, reason, and cause.
func (*CorruptedSecretError) Error ¶
func (e *CorruptedSecretError) Error() string
Error implements the error interface.
func (*CorruptedSecretError) Is ¶
func (e *CorruptedSecretError) Is(target error) bool
Is reports whether the target error is ErrCorrupted.
func (*CorruptedSecretError) Unwrap ¶
func (e *CorruptedSecretError) Unwrap() error
Unwrap returns the underlying cause for use with errors.Is and errors.As.
type InvalidNameError ¶
type InvalidNameError struct {
Name string `json:"name" yaml:"name" doc:"The invalid secret name" example:".invalid-name"`
Reason string `json:"reason" yaml:"reason" doc:"The reason the name is invalid" example:"cannot start with '.'"`
}
InvalidNameError provides details about an invalid secret name.
func NewInvalidNameError ¶
func NewInvalidNameError(name, reason string) *InvalidNameError
NewInvalidNameError creates a new InvalidNameError with the given name and reason.
func (*InvalidNameError) Error ¶
func (e *InvalidNameError) Error() string
Error implements the error interface.
func (*InvalidNameError) Is ¶
func (e *InvalidNameError) Is(target error) bool
Is reports whether the target error is ErrInvalidName.
type Keyring ¶
type Keyring interface {
// Get retrieves a value from the keyring.
// Returns an error if the key does not exist or cannot be accessed.
Get(service, account string) (string, error)
// Set stores a value in the keyring.
// Creates or updates the existing value.
Set(service, account, value string) error
// Delete removes a value from the keyring.
// Returns an error if the key does not exist or cannot be deleted.
Delete(service, account string) error
}
Keyring defines the interface for keyring operations. This interface abstracts the OS keychain to allow for testing and alternative implementations.
func NewDefaultKeyring ¶
func NewDefaultKeyring() Keyring
NewDefaultKeyring creates the default keyring with the following fallback order:
- OS keyring (most secure - uses OS keychain)
- Environment variable SCAFCTL_SECRET_KEY (explicit user intent, e.g. CI)
- File-based key storage (auto-fallback, less secure but "just works")
type KeyringCall ¶
KeyringCall records a call to Get or Delete.
type KeyringError ¶
type KeyringError struct {
Operation string `json:"operation" yaml:"operation" doc:"The keyring operation that failed" example:"get"`
Cause error `json:"-" yaml:"-" doc:"The underlying error"`
}
KeyringError wraps a keyring access error with additional context.
func NewKeyringError ¶
func NewKeyringError(operation string, cause error) *KeyringError
NewKeyringError creates a new KeyringError with the given operation and cause.
func (*KeyringError) Error ¶
func (e *KeyringError) Error() string
Error implements the error interface.
func (*KeyringError) Is ¶
func (e *KeyringError) Is(target error) bool
Is reports whether the target error is ErrKeyringAccess.
func (*KeyringError) Unwrap ¶
func (e *KeyringError) Unwrap() error
Unwrap returns the underlying cause for use with errors.Is and errors.As.
type KeyringSetCall ¶
KeyringSetCall records a call to Set.
type MockKeyring ¶
type MockKeyring struct {
// Data holds the mock keyring data
Data map[string]string
// Error injection fields
GetErr error
SetErr error
DeleteErr error
// Call tracking
GetCalls []KeyringCall
SetCalls []KeyringSetCall
DeleteCalls []KeyringCall
// contains filtered or unexported fields
}
MockKeyring is a mock implementation of the Keyring interface for testing. It provides configurable behavior including error injection.
func NewMockKeyring ¶
func NewMockKeyring() *MockKeyring
NewMockKeyring creates a new MockKeyring with an empty data map.
func (*MockKeyring) Delete ¶
func (m *MockKeyring) Delete(service, account string) error
Delete removes a value from the mock keyring.
func (*MockKeyring) Get ¶
func (m *MockKeyring) Get(service, account string) (string, error)
Get retrieves a value from the mock keyring.
func (*MockKeyring) Set ¶
func (m *MockKeyring) Set(service, account, value string) error
Set stores a value in the mock keyring.
type MockStore ¶
type MockStore struct {
// Data holds the mock secret data
Data map[string][]byte
// Error injection fields - set these to make operations return errors
GetErr error
SetErr error
DeleteErr error
ListErr error
ExistsErr error
RotateErr error
// Call tracking
GetCalls []string
SetCalls []SetCall
DeleteCalls []string
ListCalls int
ExistsCalls []string
RotateCalls int
// contains filtered or unexported fields
}
MockStore is a mock implementation of the Store interface for testing. It provides configurable behavior including error injection and call tracking.
func NewMockStore ¶
func NewMockStore() *MockStore
NewMockStore creates a new MockStore with an empty data map.
func (*MockStore) KeyringBackend ¶ added in v0.2.0
KeyringBackend returns an empty string for mock stores.
type OSKeyring ¶
type OSKeyring struct{}
OSKeyring implements Keyring using the OS keychain via go-keyring.
type Option ¶
type Option func(*config)
Option configures the Store.
func WithKeyring ¶
WithKeyring sets a custom keyring implementation. This is primarily useful for testing or for environments where the OS keyring is not available.
func WithLogger ¶
WithLogger sets the logger for the Store. If not set, a discard logger is used.
func WithSecretsDir ¶
WithSecretsDir overrides the default secrets directory. If empty, the XDG-compliant default will be used:
- Linux: ~/.local/share/scafctl/secrets/ (XDG_DATA_HOME)
- macOS: ~/.local/share/scafctl/secrets/
- Windows: %LOCALAPPDATA%\scafctl\secrets\
This can also be overridden by the SCAFCTL_SECRETS_DIR environment variable.
type Store ¶
type Store interface {
// Get retrieves a secret by name.
// Returns ErrNotFound if the secret does not exist.
// Returns ErrCorrupted if the secret file is corrupted and cannot be decrypted.
// Returns ErrInvalidName if the name is invalid.
Get(ctx context.Context, name string) ([]byte, error)
// Set stores a secret. Creates or overwrites existing.
// Returns ErrInvalidName if the name is invalid.
Set(ctx context.Context, name string, value []byte) error
// Delete removes a secret.
// No error is returned if the secret does not exist.
// Returns ErrInvalidName if the name is invalid.
Delete(ctx context.Context, name string) error
// List returns the names of all stored secrets.
List(ctx context.Context) ([]string, error)
// Exists checks if a secret exists.
// Returns ErrInvalidName if the name is invalid.
Exists(ctx context.Context, name string) (bool, error)
// Rotate rotates the master encryption key by re-encrypting all secrets.
// This operation is atomic - if any step fails, all secrets are preserved
// with the original key. The new key is generated, all secrets are
// re-encrypted, and the keyring is updated.
Rotate(ctx context.Context) error
// KeyringBackend returns the identifier of the keyring backend used for
// master key storage. Possible values: "os", "env", "file", or "" if unknown.
KeyringBackend() string
}
Store provides secure secret storage operations. All operations are safe for concurrent use.
func New ¶
New creates a new Store with the given options. If no keyring is provided, the default keyring (OS keychain with env var fallback) is used. If no secrets directory is provided, the platform-specific default is used.
The master encryption key is retrieved from the keyring on initialization. If no master key exists:
- If there are existing secrets, they are deleted (orphaned) and a new key is generated
- If there are no existing secrets, a new key is generated
Options:
- WithSecretsDir: Override the secrets directory
- WithKeyring: Provide a custom keyring implementation
- WithLogger: Set a logger for diagnostic output