Vault

Encryption

AES-256-GCM encryption with pluggable key providers.

The crypto package provides AES-256-GCM authenticated encryption for Vault secrets, along with pluggable key provider and key store interfaces for key management, rotation, and per-tenant key isolation (crypto-shredding).

Architecture

crypto.Encryptor          (AES-256-GCM encrypt/decrypt)
  └── cipher.AEAD         (Go standard library)

crypto.EncryptionKeyProvider  (key retrieval + rotation)
  └── crypto.EnvKeyProvider   (reads key from env var)

crypto.KeyStore              (per-entity key management)

The Encryptor is the core encryption primitive used by secret.Service. Key providers and key stores are abstractions for key management that can be implemented by plugins or external KMS systems.

Installation

import "github.com/xraph/vault/crypto"

Encryptor

The Encryptor provides AES-256-GCM authenticated encryption and decryption.

Creating an Encryptor

func NewEncryptor(key []byte) (*Encryptor, error)

The key must be exactly 32 bytes (256 bits). Returns ErrInvalidKeySize if the key length is wrong.

key := make([]byte, 32) // load from secure source
enc, err := crypto.NewEncryptor(key)
if err != nil {
    log.Fatal(err) // crypto: key must be exactly 32 bytes for AES-256
}

Encrypt

func (e *Encryptor) Encrypt(plaintext []byte) ([]byte, error)

Encrypts plaintext using AES-256-GCM. The returned ciphertext has the format:

| nonce (12 bytes) | ciphertext + GCM tag |

A random 12-byte nonce is generated for each encryption call using crypto/rand.

ciphertext, err := enc.Encrypt([]byte("my secret value"))
if err != nil {
    log.Fatal(err)
}
// ciphertext is: nonce || encrypted_data || gcm_tag

Decrypt

func (e *Encryptor) Decrypt(ciphertext []byte) ([]byte, error)

Decrypts ciphertext produced by Encrypt. Expects the nonce prepended to the ciphertext.

plaintext, err := enc.Decrypt(ciphertext)
if err != nil {
    log.Fatal(err) // crypto: decrypt: message authentication failed
}
fmt.Println(string(plaintext)) // "my secret value"

Returns an error if:

  • The ciphertext is shorter than the nonce size (12 bytes).
  • The GCM authentication tag does not match (tampered data).

ErrInvalidKeySize

var ErrInvalidKeySize = errors.New("crypto: key must be exactly 32 bytes for AES-256")

EncryptionKeyProvider

The EncryptionKeyProvider interface abstracts encryption key retrieval and rotation. Implement this interface to integrate with external KMS systems (AWS KMS, HashiCorp Vault, GCP KMS, etc.) or to provide custom key management.

type EncryptionKeyProvider interface {
    GetKey(ctx context.Context) ([]byte, error)
    RotateKey(ctx context.Context) ([]byte, error)
}
MethodDescription
GetKeyReturns the current active encryption key (32 bytes)
RotateKeyGenerates and stores a new key, returning the new key

Using with secret.Service

keyProvider := myKMSProvider() // implements EncryptionKeyProvider

key, err := keyProvider.GetKey(ctx)
if err != nil {
    log.Fatal(err)
}

enc, err := crypto.NewEncryptor(key)
if err != nil {
    log.Fatal(err)
}

svc := secret.NewService(store, enc)

EnvKeyProvider

The EnvKeyProvider reads the encryption key from an environment variable. The key can be hex-encoded (64 hex chars) or base64-encoded (auto-detected).

Creating an EnvKeyProvider

func NewEnvKeyProvider(envVar string) *EnvKeyProvider
provider := crypto.NewEnvKeyProvider("VAULT_ENCRYPTION_KEY")

GetKey

Reads and decodes the encryption key from the environment variable. Auto-detects the encoding format:

  1. Hex -- If the value is exactly 64 hex characters, decode as hex.
  2. Base64 standard -- Try base64.StdEncoding decoding.
  3. Base64 URL-safe -- Try base64.URLEncoding decoding.
  4. Base64 raw (no padding) -- Try base64.RawStdEncoding decoding.

All paths must produce exactly 32 bytes.

key, err := provider.GetKey(ctx)
if err != nil {
    log.Fatal(err)
}

RotateKey

Not supported by the environment provider. Returns an error.

_, err := provider.RotateKey(ctx)
// crypto: env provider does not support key rotation

Setting the environment variable

# Hex-encoded (64 characters)
export VAULT_ENCRYPTION_KEY="0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"

# Base64-encoded (44 characters with padding)
export VAULT_ENCRYPTION_KEY="ASNFZ4mrze8BI0VniavN7wEjRWeJq83vASNFZ4mrze8="

KeyStore

The KeyStore interface manages per-entity encryption keys, enabling GDPR-style per-tenant key isolation and crypto-shredding (deleting a key to render all data encrypted with it unrecoverable).

type KeyStore interface {
    GetOrCreate(ctx context.Context, id string) ([]byte, error)
    Get(ctx context.Context, id string) ([]byte, error)
    Delete(ctx context.Context, id string) error
}
MethodDescription
GetOrCreateRetrieves the key for the given ID, creating one if it does not exist
GetRetrieves the key for the given ID; returns an error if not found
DeleteRemoves the key for the given ID (crypto-shredding)

Crypto-shredding

To permanently destroy all data for a tenant, delete their encryption key:

// All secrets encrypted with this tenant's key become unrecoverable.
err := keyStore.Delete(ctx, "tenant-123")

This is more efficient and reliable than locating and deleting every individual record. The encrypted data remains in storage but is cryptographically unrecoverable without the key.

Per-tenant encryption example

// Get or create a key for the tenant.
tenantKey, err := keyStore.GetOrCreate(ctx, tenantID)
if err != nil {
    log.Fatal(err)
}

// Create a per-tenant encryptor.
tenantEnc, err := crypto.NewEncryptor(tenantKey)
if err != nil {
    log.Fatal(err)
}

// Create a tenant-scoped secret service.
tenantSecrets := secret.NewService(store, tenantEnc,
    secret.WithAppID("myapp"),
)

Integration with secret.Service

The secret.Service uses the Encryptor transparently:

Set flow:
  plaintext → Encryptor.Encrypt → store.SetSecret(EncryptedValue)
  EncryptionAlg set to "AES-256-GCM"

Get flow:
  store.GetSecret → EncryptedValue → Encryptor.Decrypt → Secret.Value

When no encryptor is provided to secret.NewService, the raw value bytes are stored directly. This is suitable for development but not recommended for production.

Security properties

PropertyDetail
AlgorithmAES-256-GCM (authenticated encryption with associated data)
Key size256 bits (32 bytes)
Nonce size96 bits (12 bytes), randomly generated per encryption
AuthenticationGCM tag provides integrity and authenticity
Nonce reuse protectionRandom nonce from crypto/rand for every Encrypt call

Full example

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/xraph/vault/crypto"
    "github.com/xraph/vault/secret"
)

func main() {
    ctx := context.Background()

    // Load key from environment.
    provider := crypto.NewEnvKeyProvider("VAULT_ENCRYPTION_KEY")
    key, err := provider.GetKey(ctx)
    if err != nil {
        log.Fatal(err)
    }

    // Create encryptor.
    enc, err := crypto.NewEncryptor(key)
    if err != nil {
        log.Fatal(err)
    }

    // Direct encryption/decryption.
    ciphertext, err := enc.Encrypt([]byte("hello world"))
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("ciphertext length: %d bytes\n", len(ciphertext))

    plaintext, err := enc.Decrypt(ciphertext)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("plaintext: %s\n", string(plaintext))

    // Use with secret service.
    svc := secret.NewService(secretStore, enc,
        secret.WithAppID("myapp"),
    )

    _, err = svc.Set(ctx, "api-key", []byte("sk-live-abc123"), "")
    if err != nil {
        log.Fatal(err)
    }

    sec, err := svc.Get(ctx, "api-key", "")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("decrypted: %s\n", string(sec.Value))
}

On this page