Encryption
Cereal provides three built-in encryptors and four hashers.
Encryption Boundary Tags
Encryption uses two boundaries:
type User struct {
// Encrypt when storing to database
Email string `store.encrypt:"aes"`
// Decrypt when loading from database
Email string `load.decrypt:"aes"`
// Usually paired together
Email string `store.encrypt:"aes" load.decrypt:"aes"`
}
AES-GCM
Symmetric encryption using AES-256 in GCM mode:
key := make([]byte, 32) // 256-bit key
rand.Read(key)
enc, err := cereal.AES(key)
if err != nil {
// Key must be 16, 24, or 32 bytes
}
proc, _ := cereal.NewProcessor[User]()
proc.SetEncryptor(cereal.EncryptAES, enc)
- Requires 16, 24, or 32 byte key (AES-128, AES-192, AES-256)
- Random nonce prepended to ciphertext
- Authenticated encryption prevents tampering
- Base64 encoded in output
RSA-OAEP
Asymmetric encryption for scenarios where encrypt and decrypt happen in different contexts:
// Generate key pair
priv, _ := rsa.GenerateKey(rand.Reader, 2048)
pub := &priv.PublicKey
// Full encryptor (can encrypt and decrypt)
enc := cereal.RSA(pub, priv)
// Encrypt-only encryptor (for client-side)
encryptOnly := cereal.RSA(pub, nil)
proc.SetEncryptor(cereal.EncryptRSA, enc)
- Uses SHA-256 for OAEP padding
- Public key encrypts, private key decrypts
- Pass
nilfor private key if decrypt not needed
Envelope Encryption
For encrypting large fields or when you need key rotation:
masterKey := make([]byte, 32) // 16, 24, or 32 bytes
rand.Read(masterKey)
enc, err := cereal.Envelope(masterKey)
proc.SetEncryptor(cereal.EncryptEnvelope, enc)
How it works:
- Generate a random data encryption key (DEK) per operation
- Encrypt the field with the DEK (AES-GCM)
- Encrypt the DEK with the master key
- Prepend encrypted DEK to ciphertext
Benefits:
- Master key never touches plaintext directly
- Key rotation: re-encrypt DEKs, data unchanged
- Large fields don't stress the master key
Multiple Encryptors
Register different encryptors for different algorithms:
aesEnc, _ := cereal.AES(aesKey)
rsaEnc := cereal.RSA(pub, priv)
envEnc, _ := cereal.Envelope(masterKey)
proc.SetEncryptor(cereal.EncryptAES, aesEnc)
proc.SetEncryptor(cereal.EncryptRSA, rsaEnc)
proc.SetEncryptor(cereal.EncryptEnvelope, envEnc)
type Record struct {
Email string `store.encrypt:"aes" load.decrypt:"aes"` // Uses AES
Secret string `store.encrypt:"rsa" load.decrypt:"rsa"` // Uses RSA
Data string `store.encrypt:"envelope" load.decrypt:"envelope"` // Uses Envelope
}
Hashing
One-way hashing for fields on the receive boundary:
type User struct {
Password string `receive.hash:"sha256"`
Token string `receive.hash:"argon2"`
}
Hashing applies when receiving external input. Hashed values cannot be recovered.
Built-in Hashers
| Algorithm | Constant | Output |
|---|---|---|
| SHA-256 | HashSHA256 | 64 hex chars |
| SHA-512 | HashSHA512 | 128 hex chars |
| Argon2id | HashArgon2 | PHC format string |
| bcrypt | HashBcrypt | bcrypt format string |
SHA-256 and SHA-512 are registered by default. Argon2 and bcrypt have configurable parameters.
Argon2 Configuration
// Default parameters
hasher := cereal.Argon2()
// Custom parameters
params := cereal.Argon2Params{
Time: 3, // iterations
Memory: 64 * 1024, // 64MB
Threads: 4,
KeyLen: 32,
SaltLen: 16,
}
hasher := cereal.Argon2WithParams(params)
proc.SetHasher(cereal.HashArgon2, hasher)
bcrypt Configuration
// Default cost (12, per OWASP recommendations)
hasher := cereal.Bcrypt()
// Custom cost
hasher := cereal.BcryptWithCost(10) // Lower cost for faster tests
proc.SetHasher(cereal.HashBcrypt, hasher)
When to Hash vs Encrypt
| Use Case | Approach |
|---|---|
| Need original value back | store.encrypt + load.decrypt |
| Verification only | receive.hash |
| Audit trail | receive.hash |
| Password storage | receive.hash:"argon2" or receive.hash:"bcrypt" |
Custom Encryptors
Implement the Encryptor interface:
type Encryptor interface {
Encrypt(plaintext []byte) ([]byte, error)
Decrypt(ciphertext []byte) ([]byte, error)
}
Example:
type vaultEncryptor struct {
client *vault.Client
path string
}
func (e *vaultEncryptor) Encrypt(plaintext []byte) ([]byte, error) {
return e.client.Transit.Encrypt(e.path, plaintext)
}
func (e *vaultEncryptor) Decrypt(ciphertext []byte) ([]byte, error) {
return e.client.Transit.Decrypt(e.path, ciphertext)
}
proc.SetEncryptor(cereal.EncryptAES, &vaultEncryptor{client, "transit/keys/mykey"})
Custom Hashers
Implement the Hasher interface:
type Hasher interface {
Hash(data []byte) (string, error)
}
The return value is stored directly (typically hex or PHC format).
type hmacHasher struct {
key []byte
}
func (h *hmacHasher) Hash(data []byte) (string, error) {
mac := hmac.New(sha256.New, h.key)
mac.Write(data)
return hex.EncodeToString(mac.Sum(nil)), nil
}
proc.SetHasher(cereal.HashSHA256, &hmacHasher{key})
Field Type Support
| Tag | string | []byte |
|---|---|---|
store.encrypt | Yes | Yes |
load.decrypt | Yes | Yes |
receive.hash | Yes | Yes |
String vs Byte Slice Encoding
String fields are base64 encoded after encryption. This ensures compatibility with text-based codecs (JSON, XML, YAML) that cannot represent arbitrary binary data.
Byte slice fields store raw ciphertext without encoding. Binary codecs (MessagePack, BSON) handle this natively.
type Record struct {
// Base64 encoded in JSON: "email":"SGVsbG8gV29ybGQ..."
Email string `json:"email" store.encrypt:"aes" load.decrypt:"aes"`
// Raw bytes in MessagePack/BSON, base64 in JSON
Data []byte `json:"data" store.encrypt:"aes" load.decrypt:"aes"`
}
Tradeoff: Base64 encoding adds ~33% size overhead (4 bytes output per 3 bytes input). For performance-critical paths with large encrypted fields:
- Use
[]bytefields with binary codecs (MessagePack, BSON) - Implement
Encryptable/Decryptableinterfaces with custom encoding - Store encrypted data separately from the serialized struct