Vault

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

OptionSignatureDescription
WithAppIDWithAppID(appID string) ServiceOptionSets the default app ID used when callers pass an empty string
WithOnAccessWithOnAccess(fn OnAccessFunc) ServiceOptionRegisters a callback invoked after Get or GetVersion
WithOnMutateWithOnMutate(fn OnMutateFunc) ServiceOptionRegisters 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 value

GetMeta

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

OptionSignatureDescription
WithMetadataWithMetadata(m map[string]string) SetOptionAttaches arbitrary key-value metadata to the secret
WithExpiresAtWithExpiresAt(t time.Time) SetOptionSets 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) error
err := 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"`
}
FieldTypeDescription
IDid.IDUnique secret identifier (TypeID)
KeystringLookup key for the secret
Value[]byteDecrypted plaintext (never serialized to JSON)
EncryptedValue[]byteCiphertext stored at rest (never serialized to JSON)
Versionint64Auto-incremented version number
EncryptionAlgstringAlgorithm label, set to "AES-256-GCM" when encrypted
EncryptionKeyIDstringIdentifier of the encryption key used
ExpiresAt*time.TimeOptional expiration timestamp
AppIDstringApplication scope identifier
Metadatamap[string]stringArbitrary key-value metadata
CreatedAttime.TimeTimestamp from embedded vault.Entity
UpdatedAttime.TimeTimestamp 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:

  1. On write (Set): The plaintext value is encrypted via Encryptor.Encrypt, producing a nonce || ciphertext byte slice. The EncryptionAlg field is set to "AES-256-GCM".
  2. On read (Get, GetVersion): The stored EncryptedValue is decrypted via Encryptor.Decrypt and the plaintext is placed in Secret.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:

  1. The store creates a new version entry with the current value.
  2. The Version field on the secret is incremented.
  3. Previous versions remain accessible via GetVersion and ListVersions.
  4. Delete removes 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)
    }
}

On this page