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_tagDecrypt
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)
}| Method | Description |
|---|---|
GetKey | Returns the current active encryption key (32 bytes) |
RotateKey | Generates 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) *EnvKeyProviderprovider := crypto.NewEnvKeyProvider("VAULT_ENCRYPTION_KEY")GetKey
Reads and decodes the encryption key from the environment variable. Auto-detects the encoding format:
- Hex -- If the value is exactly 64 hex characters, decode as hex.
- Base64 standard -- Try
base64.StdEncodingdecoding. - Base64 URL-safe -- Try
base64.URLEncodingdecoding. - Base64 raw (no padding) -- Try
base64.RawStdEncodingdecoding.
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 rotationSetting 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
}| Method | Description |
|---|---|
GetOrCreate | Retrieves the key for the given ID, creating one if it does not exist |
Get | Retrieves the key for the given ID; returns an error if not found |
Delete | Removes 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.ValueWhen 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
| Property | Detail |
|---|---|
| Algorithm | AES-256-GCM (authenticated encryption with associated data) |
| Key size | 256 bits (32 bytes) |
| Nonce size | 96 bits (12 bytes), randomly generated per encryption |
| Authentication | GCM tag provides integrity and authenticity |
| Nonce reuse protection | Random 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))
}