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-2

Cipher Options

Velocity supports multiple AES cipher modes:

CipherKey SizeModeAuthentication
AES-128-CBC16 bytesCBCHMAC-SHA256
AES-256-CBC32 bytesCBCHMAC-SHA256
AES-128-GCM16 bytesGCMBuilt-in
AES-256-GCM32 bytesGCMBuilt-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) error

Custom 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_KEY

Example .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-2

The crypto package will:

  1. Always encrypt with the current CRYPTO_KEY
  2. Attempt decryption with current key first
  3. Fall back to previous keys if current key fails
  4. 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

  1. Use Strong Keys: Always use crypto.GenerateKey() to generate cryptographically secure keys
  2. Rotate Keys Regularly: Implement periodic key rotation (e.g., every 90 days)
  3. Use GCM for New Projects: GCM mode provides authenticated encryption
  4. Protect Your Keys: Never commit .env files to version control
  5. Use HTTPS: Always transmit encrypted data over secure connections
  6. Validate Before Decrypt: Check data integrity before decryption
  7. 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