Secrets Management
Encrypted secret storage with auto-versioning.
The secret package provides encrypted secret storage with automatic versioning, expiration support, and lifecycle callbacks. Every value written through the service is transparently encrypted via AES-256-GCM before it reaches the store, and decrypted on read.
Architecture
secret.Service
|-- crypto.Encryptor (AES-256-GCM encrypt/decrypt)
|-- secret.Store (pluggable persistence)
|-- OnAccessFunc (post-read callback)
|-- OnMutateFunc (post-write callback)The Service sits between your application code and the backing store. It handles encryption, versioning, and event callbacks so the store implementation only deals with opaque ciphertext.
Installation
import (
"github.com/xraph/vault/secret"
"github.com/xraph/vault/crypto"
)Creating a Service
enc, err := crypto.NewEncryptor(key) // 32-byte AES-256 key
if err != nil {
log.Fatal(err)
}
svc := secret.NewService(store, enc,
secret.WithAppID("myapp"),
secret.WithOnAccess(func(ctx context.Context, key, appID string) {
log.Printf("accessed secret %s in app %s", key, appID)
}),
secret.WithOnMutate(func(ctx context.Context, action, key, appID string) {
log.Printf("%s secret %s in app %s", action, key, appID)
}),
)ServiceOption
| Option | Signature | Description |
|---|---|---|
WithAppID | WithAppID(appID string) ServiceOption | Sets the default app ID used when callers pass an empty string |
WithOnAccess | WithOnAccess(fn OnAccessFunc) ServiceOption | Registers a callback invoked after Get or GetVersion |
WithOnMutate | WithOnMutate(fn OnMutateFunc) ServiceOption | Registers a callback invoked after Set or Delete |
Callback types
// OnAccessFunc is called after a secret is accessed.
type OnAccessFunc func(ctx context.Context, key, appID string)
// OnMutateFunc is called after a secret is set or deleted.
type OnMutateFunc func(ctx context.Context, action, key, appID string)The action parameter on mutate callbacks is either "secret.set" or "secret.delete".
Service API
Get
Retrieves and decrypts a secret by key. Fires the OnAccess callback after a successful read.
func (s *Service) Get(ctx context.Context, key, appID string) (*Secret, error)sec, err := svc.Get(ctx, "db-password", "myapp")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(sec.Value)) // plaintext valueGetMeta
Retrieves secret metadata without the value. Useful for listing or inspecting secrets without triggering decryption.
func (s *Service) GetMeta(ctx context.Context, key, appID string) (*Meta, error)meta, err := svc.GetMeta(ctx, "db-password", "myapp")
if err != nil {
log.Fatal(err)
}
fmt.Printf("version=%d created=%s\n", meta.Version, meta.CreatedAt)Set
Creates or updates a secret. The value is encrypted before storage and the store auto-increments the version. Fires the OnMutate callback with action "secret.set".
func (s *Service) Set(
ctx context.Context,
key string,
value []byte,
appID string,
opts ...SetOption,
) (*Meta, error)meta, err := svc.Set(ctx, "db-password", []byte("s3cret!"), "myapp",
secret.WithMetadata(map[string]string{"env": "production"}),
secret.WithExpiresAt(time.Now().Add(90*24*time.Hour)),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("stored version %d\n", meta.Version)SetOption
| Option | Signature | Description |
|---|---|---|
WithMetadata | WithMetadata(m map[string]string) SetOption | Attaches arbitrary key-value metadata to the secret |
WithExpiresAt | WithExpiresAt(t time.Time) SetOption | Sets an expiration timestamp on the secret |
Delete
Removes a secret and all its versions. Fires the OnMutate callback with action "secret.delete".
func (s *Service) Delete(ctx context.Context, key, appID string) errorerr := svc.Delete(ctx, "db-password", "myapp")List
Returns secret metadata for an app. Values are never returned in list results.
func (s *Service) List(ctx context.Context, appID string, opts ListOpts) ([]*Meta, error)metas, err := svc.List(ctx, "myapp", secret.ListOpts{
Limit: 50,
Offset: 0,
})
for _, m := range metas {
fmt.Printf("%-30s v%d\n", m.Key, m.Version)
}GetVersion
Retrieves and decrypts a specific version of a secret. Fires the OnAccess callback.
func (s *Service) GetVersion(
ctx context.Context,
key, appID string,
version int64,
) (*Secret, error)sec, err := svc.GetVersion(ctx, "db-password", "myapp", 2)
if err != nil {
log.Fatal(err)
}
fmt.Printf("v%d value: %s\n", sec.Version, string(sec.Value))ListVersions
Returns all versions of a secret (without decrypted values).
func (s *Service) ListVersions(ctx context.Context, key, appID string) ([]*Version, error)versions, err := svc.ListVersions(ctx, "db-password", "myapp")
for _, v := range versions {
fmt.Printf("v%d created=%s by=%s\n", v.Version, v.CreatedAt, v.CreatedBy)
}Entity types
Secret
The full secret entity, including both encrypted and decrypted values. The Value field is populated only after decryption by the service; it is never serialized.
type Secret struct {
vault.Entity
ID id.ID `json:"id"`
Key string `json:"key"`
Value []byte `json:"-"` // decrypted value -- never serialized
EncryptedValue []byte `json:"-"` // encrypted at rest
Version int64 `json:"version"`
EncryptionAlg string `json:"encryption_alg"`
EncryptionKeyID string `json:"encryption_key_id"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
AppID string `json:"app_id"`
Metadata map[string]string `json:"metadata,omitempty"`
}| Field | Type | Description |
|---|---|---|
ID | id.ID | Unique secret identifier (TypeID) |
Key | string | Lookup key for the secret |
Value | []byte | Decrypted plaintext (never serialized to JSON) |
EncryptedValue | []byte | Ciphertext stored at rest (never serialized to JSON) |
Version | int64 | Auto-incremented version number |
EncryptionAlg | string | Algorithm label, set to "AES-256-GCM" when encrypted |
EncryptionKeyID | string | Identifier of the encryption key used |
ExpiresAt | *time.Time | Optional expiration timestamp |
AppID | string | Application scope identifier |
Metadata | map[string]string | Arbitrary key-value metadata |
CreatedAt | time.Time | Timestamp from embedded vault.Entity |
UpdatedAt | time.Time | Timestamp from embedded vault.Entity |
Meta
Public metadata for a secret. Never includes the value. Returned by Set, GetMeta, and List.
type Meta struct {
ID id.ID `json:"id"`
Key string `json:"key"`
Version int64 `json:"version"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
AppID string `json:"app_id"`
Metadata map[string]string `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}Version
A historical version record for a secret.
type Version struct {
ID id.ID `json:"id"`
SecretKey string `json:"secret_key"`
AppID string `json:"app_id"`
Version int64 `json:"version"`
EncryptedValue []byte `json:"-"`
CreatedBy string `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
}ListOpts
type ListOpts struct {
Limit int
Offset int
AppID string
}Store interface
The store handles persistence. Vault ships with in-memory and PostgreSQL implementations (see Stores).
type Store interface {
GetSecret(ctx context.Context, key, appID string) (*Secret, error)
SetSecret(ctx context.Context, s *Secret) error
DeleteSecret(ctx context.Context, key, appID string) error
ListSecrets(ctx context.Context, appID string, opts ListOpts) ([]*Meta, error)
GetSecretVersion(ctx context.Context, key, appID string, version int64) (*Secret, error)
ListSecretVersions(ctx context.Context, key, appID string) ([]*Version, error)
}The store is responsible for auto-incrementing the Version field on SetSecret. When a key already exists, a new version is created and the previous version is preserved.
Encryption behavior
When an Encryptor is provided to NewService:
- On write (
Set): The plaintext value is encrypted viaEncryptor.Encrypt, producing anonce || ciphertextbyte slice. TheEncryptionAlgfield is set to"AES-256-GCM". - On read (
Get,GetVersion): The storedEncryptedValueis decrypted viaEncryptor.Decryptand the plaintext is placed inSecret.Value.
When no encryptor is provided, the raw value bytes are stored directly in EncryptedValue as a fallback. This is suitable for development but not recommended for production.
Versioning behavior
Every call to Set creates a new version of the secret:
- The store creates a new version entry with the current value.
- The
Versionfield on the secret is incremented. - Previous versions remain accessible via
GetVersionandListVersions. Deleteremoves the secret and all its versions.
Full example
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/xraph/vault/crypto"
"github.com/xraph/vault/secret"
)
func main() {
ctx := context.Background()
// Create encryptor with a 32-byte key.
key := make([]byte, 32) // In production, load from a key provider
enc, err := crypto.NewEncryptor(key)
if err != nil {
log.Fatal(err)
}
// Create the service with an in-memory store.
svc := secret.NewService(memStore, enc,
secret.WithAppID("myapp"),
secret.WithOnAccess(func(ctx context.Context, key, appID string) {
fmt.Printf("[audit] accessed %s\n", key)
}),
)
// Store a secret.
meta, err := svc.Set(ctx, "api-key", []byte("sk-live-abc123"), "",
secret.WithMetadata(map[string]string{"provider": "stripe"}),
secret.WithExpiresAt(time.Now().Add(365*24*time.Hour)),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("stored: version=%d\n", meta.Version)
// Retrieve it.
sec, err := svc.Get(ctx, "api-key", "")
if err != nil {
log.Fatal(err)
}
fmt.Printf("value: %s\n", string(sec.Value))
// Update it (creates version 2).
meta, err = svc.Set(ctx, "api-key", []byte("sk-live-xyz789"), "")
if err != nil {
log.Fatal(err)
}
fmt.Printf("updated: version=%d\n", meta.Version)
// List all versions.
versions, err := svc.ListVersions(ctx, "api-key", "")
if err != nil {
log.Fatal(err)
}
for _, v := range versions {
fmt.Printf(" v%d created=%s\n", v.Version, v.CreatedAt)
}
}