Cryptography
> Encrypt and decrypt data with AES cipher modes and key rotation in Velocity.
Velocity provides robust encryption utilities for securing sensitive data with support for multiple AES cipher modes and automatic key rotation.
Quick Start
Auto-initialization: The crypto package automatically initializes from your
.env file when CRYPTO_KEY or APP_KEY is set.import "github.com/velocitykode/velocity/crypto"
func main() {
// Encrypt data (auto-initialized from .env)
encrypted, err := crypto.Encrypt("sensitive data")
if err != nil {
log.Error("Encryption failed", "error", err)
return
}
// Decrypt data
plaintext, err := crypto.Decrypt(encrypted)
if err != nil {
log.Error("Decryption failed", "error", err)
return
}
fmt.Println(plaintext) // Output: sensitive data
}import (
"encoding/json"
"github.com/velocitykode/velocity/crypto"
)
func encryptUserData(user User) (string, error) {
// Serialize data to JSON
data, err := json.Marshal(user)
if err != nil {
return "", err
}
// Encrypt the JSON bytes
return crypto.EncryptBytes(data)
}
func decryptUserData(encrypted string) (*User, error) {
// Decrypt to bytes
data, err := crypto.DecryptBytes(encrypted)
if err != nil {
return nil, err
}
// Deserialize from JSON
var user User
if err := json.Unmarshal(data, &user); err != nil {
return nil, err
}
return &user, nil
}import "github.com/velocitykode/velocity/crypto"
func generateNewKey() error {
// Generate a new encryption key
key, err := crypto.GenerateKey()
if err != nil {
return err
}
// Key is base64 encoded and ready to use
fmt.Printf("Add this to your .env file:\n")
fmt.Printf("CRYPTO_KEY=%s\n", key)
return nil
}Configuration
Configure encryption through environment variables in your .env file:
# Encryption key (required)
CRYPTO_KEY=base64:your-base64-encoded-key-here
# Alternative env var name (also read by default)
APP_KEY=base64:your-base64-encoded-key-here
# Cipher algorithm (optional, defaults to AES-256-CBC)
CRYPTO_CIPHER=AES-256-CBC
# Previous keys for rotation (optional, comma-separated)
CRYPTO_OLD_KEYS=base64:old-key-1,base64:old-key-2Cipher Options
Velocity supports multiple AES cipher modes:
| Cipher | Key Size | Mode | Authentication |
|---|---|---|---|
AES-128-CBC | 16 bytes | CBC | HMAC-SHA256 |
AES-256-CBC | 32 bytes | CBC | HMAC-SHA256 |
AES-128-GCM | 16 bytes | GCM | Built-in |
AES-256-GCM | 32 bytes | GCM | Built-in |
Recommended: AES-256-GCM for new projects (authenticated encryption). Use AES-256-CBC if you need interop with existing systems using that cipher.
API Reference
Global Functions
// Encrypt encrypts plaintext using the global encryptor
func Encrypt(plaintext string) (string, error)
// EncryptBytes encrypts bytes using the global encryptor
func EncryptBytes(plaintext []byte) (string, error)
// Decrypt decrypts a payload using the global encryptor
func Decrypt(payload string) (string, error)
// DecryptBytes decrypts a payload to bytes using the global encryptor
func DecryptBytes(payload string) ([]byte, error)
// GenerateKey generates a new encryption key for the current cipher
func GenerateKey() (string, error)
// Init initializes the global encryptor with custom configuration
func Init(config Config) errorCustom Encryptor Instances
import "github.com/velocitykode/velocity/crypto"
func createCustomEncryptor() {
// Create encryptor with custom configuration
config := crypto.Config{
Key: "base64:your-key-here",
Cipher: "AES-256-GCM",
PreviousKeys: []string{
"base64:old-key-1",
"base64:old-key-2",
},
}
encryptor, err := crypto.NewEncryptor(config)
if err != nil {
log.Error("Failed to create encryptor", "error", err)
return
}
// Use the custom encryptor
encrypted, _ := encryptor.Encrypt("secret data")
plaintext, _ := encryptor.Decrypt(encrypted)
}Key Rotation
Velocity supports seamless key rotation for enhanced security:
// Step 1: Generate a new key
newKey, _ := crypto.GenerateKey()
// Step 2: Update your .env file
// Move current CRYPTO_KEY to CRYPTO_OLD_KEYS
// Set new key as CRYPTO_KEYExample .env after rotation:
# New key (used for encryption)
CRYPTO_KEY=base64:new-key-here
# Old keys (used for decryption only)
CRYPTO_OLD_KEYS=base64:old-key-1,base64:old-key-2The crypto package will:
- Always encrypt with the current
CRYPTO_KEY - Attempt decryption with current key first
- Fall back to previous keys if current key fails
- Return error if all keys fail
Re-encrypting Data
func reencryptUserTokens() error {
// Fetch all encrypted tokens
var tokens []EncryptedToken
db.Find(&tokens)
for _, token := range tokens {
// Decrypt with old key (automatic fallback)
plaintext, err := crypto.Decrypt(token.Value)
if err != nil {
log.Error("Failed to decrypt", "id", token.ID, "error", err)
continue
}
// Re-encrypt with new key
newEncrypted, err := crypto.Encrypt(plaintext)
if err != nil {
log.Error("Failed to encrypt", "id", token.ID, "error", err)
continue
}
// Update database
token.Value = newEncrypted
db.Save(&token)
}
return nil
}Payload Format
Encrypted data uses a structured JSON payload:
CBC Mode Payload
{
"iv": "base64-encoded-initialization-vector",
"value": "base64-encoded-ciphertext",
"mac": "base64-encoded-hmac-sha256"
}GCM Mode Payload
{
"iv": "base64-encoded-nonce",
"value": "base64-encoded-ciphertext",
"tag": "base64-encoded-auth-tag"
}The entire payload is base64-URL-encoded for safe storage and transmission.
Common Use Cases
Encrypting Sensitive Database Fields
type User struct {
ID uint
Email string
EncryptedAPIKey string `orm:"column:api_key"`
}
func (u *User) SetAPIKey(key string) error {
encrypted, err := crypto.Encrypt(key)
if err != nil {
return err
}
u.EncryptedAPIKey = encrypted
return nil
}
func (u *User) GetAPIKey() (string, error) {
return crypto.Decrypt(u.EncryptedAPIKey)
}Encrypting Session Data
func encryptSession(data map[string]interface{}) (string, error) {
// Serialize to JSON
jsonData, err := json.Marshal(data)
if err != nil {
return "", err
}
// Encrypt
return crypto.EncryptBytes(jsonData)
}
func decryptSession(encrypted string) (map[string]interface{}, error) {
// Decrypt
jsonData, err := crypto.DecryptBytes(encrypted)
if err != nil {
return nil, err
}
// Deserialize from JSON
var data map[string]interface{}
if err := json.Unmarshal(jsonData, &data); err != nil {
return nil, err
}
return data, nil
}Encrypting File Contents
func encryptFile(inputPath, outputPath string) error {
// Read file
data, err := os.ReadFile(inputPath)
if err != nil {
return err
}
// Encrypt
encrypted, err := crypto.EncryptBytes(data)
if err != nil {
return err
}
// Write encrypted data
return os.WriteFile(outputPath, []byte(encrypted), 0644)
}
func decryptFile(inputPath, outputPath string) error {
// Read encrypted file
encrypted, err := os.ReadFile(inputPath)
if err != nil {
return err
}
// Decrypt
data, err := crypto.DecryptBytes(string(encrypted))
if err != nil {
return err
}
// Write decrypted data
return os.WriteFile(outputPath, data, 0644)
}Security Best Practices
- Use Strong Keys: Always use
crypto.GenerateKey()to generate cryptographically secure keys - Rotate Keys Regularly: Implement periodic key rotation (e.g., every 90 days)
- Use GCM for New Projects: GCM mode provides authenticated encryption
- Protect Your Keys: Never commit
.envfiles to version control - Use HTTPS: Always transmit encrypted data over secure connections
- Validate Before Decrypt: Check data integrity before decryption
- Log Failures Carefully: Don’t log plaintext or keys in error messages
Error Handling
func handleEncryption() {
encrypted, err := crypto.Encrypt("data")
if err != nil {
switch err {
case crypto.ErrNotInitialized:
log.Error("Crypto not initialized - check CRYPTO_KEY in .env")
case crypto.ErrInvalidKey:
log.Error("Invalid encryption key")
default:
log.Error("Encryption failed", "error", err)
}
return
}
plaintext, err := crypto.Decrypt(encrypted)
if err != nil {
switch err {
case crypto.ErrInvalidPayload:
log.Error("Invalid encrypted payload format")
case crypto.ErrDecryptionFailed:
log.Error("Decryption failed - wrong key or corrupted data")
default:
log.Error("Decryption failed", "error", err)
}
return
}
fmt.Println(plaintext)
}Testing
func TestEncryption(t *testing.T) {
// Initialize with test key
testKey := "base64:" + base64.StdEncoding.EncodeToString(make([]byte, 32))
crypto.Init(crypto.Config{
Key: testKey,
Cipher: "AES-256-CBC",
})
// Test encryption/decryption
plaintext := "test data"
encrypted, err := crypto.Encrypt(plaintext)
assert.NoError(t, err)
assert.NotEqual(t, plaintext, encrypted)
decrypted, err := crypto.Decrypt(encrypted)
assert.NoError(t, err)
assert.Equal(t, plaintext, decrypted)
// Test bytes encryption
data := []byte("binary data")
encryptedBytes, err := crypto.EncryptBytes(data)
assert.NoError(t, err)
decryptedBytes, err := crypto.DecryptBytes(encryptedBytes)
assert.NoError(t, err)
assert.Equal(t, data, decryptedBytes)
}Performance Considerations
- AES-GCM is faster: GCM mode typically performs better than CBC
- Encrypt once: Cache encrypted values when possible
- Batch operations: Group encryption operations to amortize overhead
- Key size impact: AES-256 is slightly slower than AES-128 but more secure
Benchmarks
BenchmarkEncrypt-8 50000 25847 ns/op 2048 B/op 12 allocs/op
BenchmarkDecrypt-8 50000 27234 ns/op 2304 B/op 14 allocs/op
BenchmarkEncryptGCM-8 75000 18932 ns/op 1792 B/op 10 allocs/op
BenchmarkDecryptGCM-8 75000 19421 ns/op 1920 B/op 11 allocs/op