encryption

package
v1.1.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 25, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package encryption provides data-at-rest encryption for NornicDB.

This package implements AES-256-GCM encryption for data at rest, following compliance requirements for GDPR, HIPAA, FISMA, and SOC2:

  • GDPR Art.32: Appropriate security of processing
  • HIPAA §164.312(a)(2)(iv): Encryption and decryption
  • FISMA SC-13: Cryptographic Protection
  • SOC2 CC6.1: Encryption

Features:

  • AES-256-GCM authenticated encryption
  • Key rotation support with versioned keys
  • Secure key derivation (PBKDF2/Argon2)
  • Transparent encryption for sensitive fields
  • Key management interface for external KMS integration

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidKey       = errors.New("encryption: invalid key length (must be 32 bytes)")
	ErrInvalidData      = errors.New("encryption: invalid encrypted data")
	ErrDecryptionFailed = errors.New("encryption: decryption failed (authentication error)")
	ErrNoKey            = errors.New("encryption: no encryption key available")
	ErrKeyNotFound      = errors.New("encryption: key version not found")
	ErrKeyExpired       = errors.New("encryption: key has expired")
)

Errors

Functions

func DefaultPHIFields

func DefaultPHIFields() []string

DefaultPHIFields returns commonly required encrypted fields for compliance.

func DeriveKey

func DeriveKey(password, salt []byte, iterations int) []byte

DeriveKey derives a 32-byte AES-256 key from password and salt using PBKDF2-HMAC-SHA256.

This is a low-level function for custom key derivation scenarios. For most use cases, prefer NewEncryptorWithPassword() which handles key derivation and management automatically.

Parameters

password: User password as bytes (convert string with []byte(password)) salt: Unique salt per installation (32 bytes recommended) iterations: PBKDF2 iteration count (0 = default 600,000)

Security Considerations

Iteration Count:

  • OWASP 2023: 600,000 minimum for PBKDF2-HMAC-SHA256
  • NIST SP 800-132: 10,000 minimum (outdated, use OWASP)
  • Higher = more secure but slower (~100ms is acceptable)

Salt Requirements:

  • MUST be cryptographically random (use GenerateSalt())
  • MUST be unique per installation/user
  • 32 bytes (256 bits) recommended
  • Store securely but doesn't need to be secret

Password Strength:

  • Minimum 20 characters recommended
  • Mix uppercase, lowercase, digits, symbols
  • Avoid common passwords and patterns

Performance Characteristics

Timing (600,000 iterations):

  • ~100-200ms on modern CPUs
  • ~50-100ms on high-end server CPUs
  • Intentionally slow to prevent brute-force

Memory:

  • Minimal: ~1KB during derivation
  • Returns 32-byte key

Thread Safety

DeriveKey is stateless and thread-safe:

  • Safe to call from multiple goroutines
  • No shared state or locks

Example (Basic Usage):

password := []byte("SecurePassword123!")
salt, _ := encryption.GenerateSalt()

// Derive with default iterations (600,000)
key := encryption.DeriveKey(password, salt, 0)
fmt.Printf("Derived key: %x\n", key)

// Store salt for later use
os.WriteFile("salt.key", salt, 0600)

Example (Custom Iterations):

// High-security: 1 million iterations (~200ms)
key := encryption.DeriveKey(password, salt, 1000000)

// Fast iteration for dev (not recommended for production)
devKey := encryption.DeriveKey(password, salt, 10000)

Example (Multi-Tenant Key Derivation):

// Derive tenant-specific keys from master password
func deriveTenantKey(masterPassword, tenantID string) []byte {
	// Generate deterministic salt from tenant ID
	h := sha256.New()
	h.Write([]byte("nornicdb-tenant"))
	h.Write([]byte(tenantID))
	salt := h.Sum(nil)

	return encryption.DeriveKey([]byte(masterPassword), salt, 600000)
}

tenant1Key := deriveTenantKey("MasterPass123!", "tenant-001")
tenant2Key := deriveTenantKey("MasterPass123!", "tenant-002")
// Different keys despite same password

Example (Key Stretching for Weak Passwords):

// User provides weak password
weakPassword := []byte("password")
salt := mustLoadSalt()

// Extra iterations to compensate
key := encryption.DeriveKey(weakPassword, salt, 2000000) // 2M iterations

// Better: Enforce strong passwords at input
if len(password) < 20 {
	return errors.New("password too short")
}

ELI12 Explanation

Think of DeriveKey as a special blender that makes smoothies:

You put in:

  • Your password (like fruit)
  • A salt (like ice - makes it unique)
  • How many times to blend (iterations)

The blender:

  1. Mixes everything together
  2. Blends it thousands of times (600,000!)
  3. Produces exactly 32 bytes of perfect key material

Why blend so many times?

  • It takes ~100ms for you (barely noticeable)
  • An attacker trying every password takes 100ms per guess
  • Guessing billions of passwords would take centuries!

Why use salt?

  • Two people with "password123" get different keys
  • Attackers can't use pre-made lists (rainbow tables)
  • Each installation is unique and independent

The output is always exactly 32 bytes (256 bits), perfect for AES-256 encryption!

func GenerateKey

func GenerateKey() ([]byte, error)

GenerateKey generates a cryptographically secure random 32-byte AES-256 key.

This function uses crypto/rand to generate high-quality random keys suitable for production encryption. The key can be used directly with KeyManager or Encryptor.

Security Features

Randomness Source:

  • Uses crypto/rand (OS-provided CSPRNG)
  • /dev/urandom on Unix (non-blocking, cryptographically secure)
  • CryptGenRandom on Windows
  • No deterministic generation or weak PRNGs

Key Properties:

  • 256 bits (32 bytes) for AES-256
  • Each bit has 50% probability of 0 or 1
  • 2^256 possible keys (~10^77 combinations)
  • Brute force would take longer than universe's age

Performance Characteristics

Generation Time:

  • <1µs on modern hardware
  • OS kernel overhead dominates
  • Can generate millions per second

Memory:

  • Allocates 32 bytes
  • No persistent state
  • Garbage collected normally

Thread Safety

crypto/rand.Read is thread-safe:

  • Safe to call from multiple goroutines
  • No locking required in application code
  • OS handles concurrency

Example (Basic Usage):

// Generate a new key
key, err := encryption.GenerateKey()
if err != nil {
	log.Fatalf("Key generation failed: %v", err)
}

fmt.Printf("Generated key: %x\n", key)
// Output: Generated key: 3f7a2b9c... (32 bytes hex)

// Use with KeyManager
km := encryption.NewKeyManager(encryption.DefaultConfig())
km.AddKey(&encryption.Key{
	ID:       1,
	Material: key,
	Active:   true,
})

Example (Production Setup):

// Generate and store master key (do once!)
key, err := encryption.GenerateKey()
if err != nil {
	log.Fatal(err)
}

// Store in secure location
// Option 1: KMS (recommended)
storeInKMS("nornicdb-master-key", key)

// Option 2: Encrypted file
encrypted := encryptWithHSM(key)
os.WriteFile("/etc/nornicdb/master.key", encrypted, 0600)

// Option 3: Environment variable (Base64)
encoded := base64.StdEncoding.EncodeToString(key)
os.Setenv("NORNICDB_KEY", encoded)

// Securely wipe from memory
encryption.SecureWipe(key)

Example (Key Rotation):

// Generate new key for rotation
newKey, err := encryption.GenerateKey()
if err != nil {
	return err
}

// Add to key manager
km.AddKey(&encryption.Key{
	ID:        km.KeyCount() + 1,
	Material:  newKey,
	CreatedAt: time.Now(),
	Active:    true,
})

log.Printf("Rotated to new key v%d", km.KeyCount())

Example (Multi-Environment Key Generation):

// Generate different keys per environment
func generateEnvironmentKeys() map[string][]byte {
	keys := make(map[string][]byte)

	for _, env := range []string{"dev", "staging", "prod"} {
		key, err := encryption.GenerateKey()
		if err != nil {
			log.Fatalf("Failed to generate key for %s: %v", env, err)
		}
		keys[env] = key

		// Store securely
		filename := fmt.Sprintf("/etc/nornicdb/%s.key", env)
		os.WriteFile(filename, key, 0600)
	}

	return keys
}

Example (Batch Key Generation):

// Generate multiple keys for multi-tenant setup
func generateTenantKeys(tenantIDs []string) (map[string][]byte, error) {
	keys := make(map[string][]byte)

	for _, tenantID := range tenantIDs {
		key, err := encryption.GenerateKey()
		if err != nil {
			return nil, fmt.Errorf("tenant %s: %w", tenantID, err)
		}
		keys[tenantID] = key
	}

	return keys, nil
}

tenants := []string{"acme-corp", "contoso", "fabrikam"}
keys, _ := generateTenantKeys(tenants)

Example (Key Generation with Backup):

// Generate key and create encrypted backup
key, err := encryption.GenerateKey()
if err != nil {
	return err
}

// Store primary
storeInKMS("primary-key", key)

// Create encrypted backup
backupKey, _ := encryption.GenerateKey()
backup := encryptKey(key, backupKey)
storeInS3("key-backup", backup)

// Store backup key separately
storeInVault("backup-key", backupKey)

ELI12 Explanation

GenerateKey is like rolling a perfect 256-sided die 8 times:

Regular die (6 sides):

  • Rolling once gives you 6 possibilities
  • Easy to guess if you try a few times

Our crypto die (256 sides):

  • Rolling once gives you 256 possibilities
  • Rolling 8 times gives you 256^8 possibilities
  • That's 18,446,744,073,709,551,616 combinations (18 quintillion!)

But we actually use 32 bytes (not 8):

  • That's 256^32 possible keys
  • More combinations than atoms in the universe!
  • Impossible to guess, even with all computers on Earth

The randomness comes from your computer's special random generator:

  • Uses hardware events (mouse movements, keyboard timing, network noise)
  • Cryptographically secure (no patterns)
  • Each key is completely unique and unpredictable

This is why it's safe: even if an attacker knows you used this function, they have no way to guess which of the 2^256 possible keys you got!

func GenerateSalt

func GenerateSalt() ([]byte, error)

GenerateSalt generates a cryptographically secure random 32-byte salt for key derivation.

This function generates a unique salt for use with DeriveKey() or NewEncryptorWithPassword(). The salt should be generated once per installation and stored securely (but doesn't need to be kept secret).

Salt Purpose

Prevents Rainbow Table Attacks:

  • Pre-computed hash tables become useless
  • Each installation has unique derived keys
  • Same password → different keys with different salts

Uniqueness:

  • Each installation should have unique salt
  • Multi-tenant: Each tenant should have unique salt
  • Users sharing password won't have same derived key

Security Properties

Randomness:

  • Uses crypto/rand (same as GenerateKey)
  • 256 bits of entropy (32 bytes)
  • No predictable patterns

Storage:

  • Salt does NOT need to be secret
  • Can store in config files (restricted permissions)
  • Should be backed up with database
  • Never reuse across installations

Performance Characteristics

Generation Time:

  • <1µs (identical to GenerateKey)
  • One-time operation per installation

Memory:

  • 32 bytes allocated
  • Persist in config or database

Thread Safety

crypto/rand.Read is thread-safe:

  • Safe to call from multiple goroutines
  • No synchronization needed

Example (Initial Setup):

// Generate salt during installation
salt, err := encryption.GenerateSalt()
if err != nil {
	log.Fatalf("Failed to generate salt: %v", err)
}

// Store in config file
config := map[string]string{
	"salt": hex.EncodeToString(salt),
}
json.WriteFile("/etc/nornicdb/config.json", config, 0600)

// Or store in environment
encoded := base64.StdEncoding.EncodeToString(salt)
os.Setenv("NORNICDB_SALT", encoded)

Example (Production Setup with Persistence):

// Generate once during first run
saltFile := "/etc/nornicdb/salt.key"

var salt []byte
if _, err := os.Stat(saltFile); os.IsNotExist(err) {
	// First run: generate and save
	salt, err = encryption.GenerateSalt()
	if err != nil {
		log.Fatal(err)
	}

	err = os.WriteFile(saltFile, salt, 0600)
	if err != nil {
		log.Fatalf("Failed to save salt: %v", err)
	}

	log.Println("Generated new salt")
} else {
	// Subsequent runs: load existing
	salt, err = os.ReadFile(saltFile)
	if err != nil {
		log.Fatalf("Failed to load salt: %v", err)
	}

	log.Println("Loaded existing salt")
}

// Use with key derivation
password := os.Getenv("MASTER_PASSWORD")
key := encryption.DeriveKey([]byte(password), salt, 600000)

Example (Multi-Tenant Salt Management):

// Generate unique salt per tenant
type TenantConfig struct {
	ID   string
	Salt []byte
}

func createTenant(tenantID string) (*TenantConfig, error) {
	salt, err := encryption.GenerateSalt()
	if err != nil {
		return nil, err
	}

	config := &TenantConfig{
		ID:   tenantID,
		Salt: salt,
	}

	// Store in database
	db.Save(config)

	return config, nil
}

// Each tenant gets isolated encryption
tenant1, _ := createTenant("acme-corp")
tenant2, _ := createTenant("contoso")

// Derive tenant-specific keys
masterPassword := os.Getenv("MASTER_PASSWORD")
key1 := encryption.DeriveKey([]byte(masterPassword), tenant1.Salt, 600000)
key2 := encryption.DeriveKey([]byte(masterPassword), tenant2.Salt, 600000)
// Different keys despite same password!

Example (Database-Backed Salt):

// Store salt in database for multi-instance deployments
func getOrCreateSalt(db *sql.DB) ([]byte, error) {
	// Try to load existing
	var salt []byte
	err := db.QueryRow("SELECT salt FROM config WHERE key = 'master_salt'").Scan(&salt)

	if err == sql.ErrNoRows {
		// First run: generate and save
		salt, err = encryption.GenerateSalt()
		if err != nil {
			return nil, err
		}

		_, err = db.Exec("INSERT INTO config (key, salt) VALUES (?, ?)", "master_salt", salt)
		if err != nil {
			return nil, err
		}

		log.Println("Generated and stored new salt")
	} else if err != nil {
		return nil, err
	}

	return salt, nil
}

salt, err := getOrCreateSalt(db)
// All instances use same salt (important for key derivation)

Example (Salt Backup and Recovery):

// Generate with backup strategy
salt, err := encryption.GenerateSalt()
if err != nil {
	return err
}

// Store primary
os.WriteFile("/etc/nornicdb/salt.key", salt, 0600)

// Store backup in different location
os.WriteFile("/var/backup/nornicdb-salt.key", salt, 0600)

// Store in secrets manager
storeInSecretsManager("nornicdb-salt", salt)

// Print for manual backup (Base64)
encoded := base64.StdEncoding.EncodeToString(salt)
fmt.Printf("BACKUP THIS SALT: %s\n", encoded)

ELI12 Explanation

Think of salt like the secret ingredient in your grandma's cookie recipe:

Without salt:

  • Everyone using "password123" gets the same key
  • Hackers can make a list: "password123" → key A, "qwerty" → key B
  • They use this list to crack millions of passwords instantly (rainbow table)

With salt:

  • Your salt is like adding "secret ingredient #847392"
  • Same "password123" + your salt = different key than everyone else
  • Hackers' pre-made lists are useless - they'd need a list for EVERY salt
  • Your installation is unique and independent

Important:

  • Generate once and save it (like writing down the recipe)
  • The salt doesn't need to be secret (everyone knows salt is salt)
  • But you need the SAME salt every time (can't change the recipe!)
  • Each installation/tenant should have their own unique salt

This makes password cracking go from "instant with pre-made lists" to "must try every password individually for your specific installation"!

func HashKey

func HashKey(key []byte) string

HashKey returns a SHA-256 hash of the key material for logging and identification.

This function creates a non-reversible fingerprint of a key that can be safely logged or displayed without exposing the actual key material. The hash can be used to identify which key is being used without revealing sensitive data.

Security Properties

Non-Reversible:

  • SHA-256 is cryptographically secure one-way function
  • Cannot derive original key from hash
  • Safe to log in audit trails

Collision Resistance:

  • 128-bit output (16 bytes) from 256-bit hash
  • Probability of collision: ~1 in 2^128
  • Practically impossible with small key counts

Deterministic:

  • Same key always produces same hash
  • Useful for key identification and tracking

Use Cases

Logging:

  • Audit trails without exposing keys
  • Key rotation tracking
  • Debugging encryption issues

Key Identification:

  • Verify correct key is loaded
  • Compare keys without exposing material
  • Track key usage across systems

NOT for:

  • Key storage (use the key directly)
  • Password hashing (use bcrypt/argon2)
  • Authentication (use secure comparison)

Performance Characteristics

Hashing Time:

  • <1µs for 32-byte key
  • SHA-256 is highly optimized

Memory:

  • 32-byte hex string output
  • No persistent allocations

Thread Safety

SHA-256 is stateless and thread-safe:

  • Safe to call from multiple goroutines
  • No shared state

Example (Basic Usage):

key, _ := encryption.GenerateKey()
hash := encryption.HashKey(key)

fmt.Printf("Key fingerprint: %s\n", hash)
// Output: Key fingerprint: 3f7a2b9c1d4e5f6a...

// Safe to log
log.Printf("Loaded key with hash %s", hash)

Example (Key Rotation Tracking):

// Track key rotation in audit log
func rotateKey(km *encryption.KeyManager) error {
	old, _ := km.CurrentKey()
	oldHash := encryption.HashKey(old.Material)

	newKey, err := km.RotateKey()
	if err != nil {
		return err
	}

	newHash := encryption.HashKey(newKey.Material)

	// Log rotation event
	auditLog.Printf("Key rotated: %s → %s", oldHash, newHash)
	auditLog.Printf("Old key v%d retired, new key v%d active", old.ID, newKey.ID)

	return nil
}

Example (Key Verification):

// Verify loaded key matches expected
expectedHash := "3f7a2b9c1d4e5f6a" // From secure config

key, err := loadKeyFromKMS()
if err != nil {
	return err
}

actualHash := encryption.HashKey(key)
if actualHash != expectedHash {
	return fmt.Errorf("key verification failed: got %s, want %s", actualHash, expectedHash)
}

log.Printf("Key verified: %s", actualHash)

Example (Multi-Key Management):

// Track multiple keys by hash
type KeyInfo struct {
	Version int
	Hash    string
	Loaded  time.Time
}

keyRegistry := make(map[string]KeyInfo)

func registerKey(version int, material []byte) {
	hash := encryption.HashKey(material)
	keyRegistry[hash] = KeyInfo{
		Version: version,
		Hash:    hash,
		Loaded:  time.Now(),
	}

	log.Printf("Registered key v%d: %s", version, hash)
}

Example (Debugging Encryption Issues):

// Compare keys across environments
prodKey := loadKey("production")
devKey := loadKey("development")

prodHash := encryption.HashKey(prodKey)
devHash := encryption.HashKey(devKey)

if prodHash == devHash {
	log.Warning("Production and dev using same key! (SECURITY RISK)")
} else {
	log.Info("Production key: %s", prodHash)
	log.Info("Dev key: %s", devHash)
}

Example (Compliance Audit Trail):

// Log key usage for compliance
func encryptPHI(data string, enc *encryption.Encryptor) (string, error) {
	// Get current key
	km := enc.KeyManager()
	key, _ := km.CurrentKey()
	keyHash := encryption.HashKey(key.Material)

	// Encrypt
	ciphertext, err := enc.EncryptString(data)
	if err != nil {
		return "", err
	}

	// Audit log (HIPAA §164.312(b))
	auditLog.Printf("PHI encrypted with key %s (v%d) at %s",
		keyHash, key.ID, time.Now().Format(time.RFC3339))

	return ciphertext, nil
}

ELI12 Explanation

Think of HashKey like making a fingerprint of your actual key:

Your encryption key:

  • 32 bytes of secret data
  • Must NEVER be shown or logged
  • Like the actual key to your house

The hash (fingerprint):

  • 16 bytes that identify the key
  • Can be safely shown and logged
  • Like a photo of your key - you can show people the photo without worrying they'll copy your key

Why this is safe:

  • You can't recreate the key from its hash (one-way function)
  • Each key has a unique hash (like unique fingerprints)
  • Perfect for logs: "Used key abc123" instead of showing real key

Real-world example:

  • Your key: "3f7a2b9c..." (32 bytes, secret)
  • The hash: "d4e5f6a7..." (16 bytes, safe to show)
  • Attacker sees hash: Can't recover key (SHA-256 is unbreakable)
  • You see hash in logs: "Oh, that's key version 5!"

This lets you track which key is being used without ever exposing the actual key material in logs or debug output!

func SecureWipe

func SecureWipe(data []byte)

SecureWipe zeros out sensitive data in memory to prevent recovery.

This function overwrites sensitive data (keys, passwords, plaintexts) with zeros to reduce the window of exposure in memory. While Go's garbage collector will eventually reclaim the memory, SecureWipe provides immediate erasure.

Security Benefits

Memory Exposure:

  • Reduces time sensitive data remains in memory
  • Prevents recovery from memory dumps
  • Defense against cold boot attacks
  • Reduces process memory scanning risk

Compliance:

  • GDPR Art.32: Appropriate security measures
  • HIPAA §164.312(a)(1): Technical safeguards
  • PCI-DSS 3.2.1: Render PAN unrecoverable

Limitations

Not a Silver Bullet:

  • Go compiler may optimize away the writes
  • Memory may be swapped to disk before wiping
  • Copies may exist elsewhere in memory
  • GC may have moved data before wipe

Best Practices:

  • Wipe immediately after use
  • Minimize sensitive data lifetime
  • Use mlock() to prevent swapping (if available)
  • Disable core dumps in production

Performance Characteristics

Execution Time:

  • O(n) where n = bytes to wipe
  • ~1-2 CPU cycles per byte
  • 32-byte key: ~30-60 nanoseconds

Memory:

  • No allocations
  • Operates in-place

Thread Safety

Operates on caller's data:

  • No shared state
  • Caller must ensure exclusive access
  • Not safe to wipe data in use by other goroutines

Example (Basic Usage):

// Generate sensitive data
password := []byte("SuperSecret123!")
key := encryption.DeriveKey(password, salt, 600000)

// Use key...
enc := encryption.NewEncryptor(km, true)

// Immediately wipe after use
encryption.SecureWipe(password)
encryption.SecureWipe(key)

// Memory now contains zeros
fmt.Printf("%x\n", password) // "000000000000000000000000000000"

Example (Key Loading from KMS):

func loadKeyFromKMS(keyID string) (*encryption.Key, error) {
	// Fetch from KMS
	resp, err := kmsClient.Decrypt(keyID)
	if err != nil {
		return nil, err
	}

	// Use the key material
	key := &encryption.Key{
		ID:       1,
		Material: resp.Plaintext,
		Active:   true,
	}

	// Wipe plaintext from KMS response
	defer encryption.SecureWipe(resp.Plaintext)

	return key, nil
}

Example (Password Handling):

func authenticateUser(username, password string) error {
	// Convert to bytes for secure wiping
	passBytes := []byte(password)
	defer encryption.SecureWipe(passBytes)

	// Load user's password hash
	storedHash := db.GetPasswordHash(username)

	// Compare
	err := bcrypt.CompareHashAndPassword(storedHash, passBytes)

	// passBytes wiped on function exit
	return err
}

Example (Decrypted PHI Handling):

// Decrypt PHI and wipe immediately after processing
func processPHI(encrypted string, enc *encryption.Encryptor) error {
	// Decrypt
	plaintext, err := enc.Decrypt(encrypted)
	if err != nil {
		return err
	}
	defer encryption.SecureWipe(plaintext)

	// Process data
	result := analyzeData(plaintext)

	// plaintext wiped here (defer)
	return db.SaveResult(result)
}

Example (Batch Key Generation with Cleanup):

func generateAndStoreKeys(count int) error {
	keys := make([][]byte, count)

	// Generate keys
	for i := 0; i < count; i++ {
		key, err := encryption.GenerateKey()
		if err != nil {
			return err
		}
		keys[i] = key
	}

	// Store in KMS
	for i, key := range keys {
		err := kms.Store(fmt.Sprintf("key-%d", i), key)
		if err != nil {
			return err
		}

		// Wipe immediately after storage
		encryption.SecureWipe(key)
	}

	return nil
}

Example (Secure Password Prompt):

func promptForPassword() (string, error) {
	fmt.Print("Enter password: ")
	passBytes, err := term.ReadPassword(int(os.Stdin.Fd()))
	if err != nil {
		return "", err
	}
	defer encryption.SecureWipe(passBytes)

	// Derive key immediately
	key := encryption.DeriveKey(passBytes, salt, 600000)

	// Return hex-encoded (for storage)
	// Original password bytes wiped
	return hex.EncodeToString(key), nil
}

Example (Multi-Stage Wiping):

// Wipe data at multiple stages
func secureEncryptionFlow() error {
	// Stage 1: Load password
	password := []byte(os.Getenv("PASSWORD"))
	defer encryption.SecureWipe(password)

	// Stage 2: Derive key
	key := encryption.DeriveKey(password, salt, 600000)
	defer encryption.SecureWipe(key)

	// Stage 3: Load plaintext
	plaintext := []byte("sensitive data")
	defer encryption.SecureWipe(plaintext)

	// Encrypt (ciphertext can stay in memory)
	enc := encryption.NewEncryptor(km, true)
	ciphertext, err := enc.Encrypt(plaintext)

	// All sensitive data wiped on function exit
	return err
}

ELI12 Explanation

Think of SecureWipe like shredding a paper document:

Without SecureWipe:

  • You throw the paper in the trash
  • It sits there until garbage day
  • Someone could dig through the trash and read it
  • In computers: sensitive data sits in memory until garbage collector runs

With SecureWipe:

  • You shred the paper immediately
  • The information is gone right away
  • Even if someone digs through trash, they can't read it
  • In computers: we overwrite memory with zeros immediately

What gets wiped:

  • Passwords (after checking login)
  • Encryption keys (after using them)
  • Decrypted data (after processing)
  • Any sensitive bytes in memory

Why this helps:

  • Shorter window for memory sniffing attacks
  • Protection if process memory is dumped
  • Defense against cold boot attacks (freezing RAM to read it)
  • Required for some compliance standards (PCI-DSS)

Important: This is defense-in-depth, not perfect security. The data existed in memory briefly, but we minimize exposure time!

Types

type Config

type Config struct {
	// Whether encryption is enabled
	Enabled bool

	// Key derivation settings
	KeyDerivation KeyDerivationConfig

	// Key rotation settings
	Rotation KeyRotationConfig
}

Config holds encryption configuration.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns secure default configuration.

type Encryptor

type Encryptor struct {
	// contains filtered or unexported fields
}

Encryptor provides encryption/decryption operations.

func NewEncryptor

func NewEncryptor(km *KeyManager, enabled bool) *Encryptor

NewEncryptor creates a new encryptor with a key manager for data encryption operations.

The Encryptor provides:

  • AES-256-GCM authenticated encryption with automatic key versioning
  • Transparent passthrough when encryption is disabled
  • Base64 encoding for storage compatibility
  • Full At-Rest encryption
  • Integration with KeyManager for automatic key rotation

Encryption Features

Data Format:

  • Raw: [4-byte version][12-byte nonce][ciphertext+tag]
  • Encoded: Base64(raw) for string storage
  • Field: "enc:v{version}:{base64}" for selective encryption

Authentication:

  • GCM mode provides authenticated encryption (AEAD)
  • Detects tampering and corruption automatically
  • 128-bit authentication tag prevents forgery

Compliance Features

GDPR Art.32 - Security of Processing:

  • Encryption protects personal data at rest
  • Key versioning enables data breach response
  • Transparent decryption for authorized access

HIPAA §164.312(a)(2)(iv) - Encryption Standard:

  • AES-256-GCM meets NIST requirements
  • PHI data encrypted before storage
  • Automatic key version tracking for audit

FISMA SC-13 - Cryptographic Protection:

  • FIPS 140-2 compliant AES implementation
  • Authenticated encryption prevents data modification
  • Key separation via KeyManager

SOC2 CC6.1 - Encryption:

  • Data-at-rest encryption for sensitive fields
  • Key lifecycle managed separately
  • Audit trail through version tracking

Performance Characteristics

Encryption Operations:

  • Encrypt: ~2-5 µs per KB on modern hardware
  • Decrypt: ~2-5 µs per KB (slightly faster)
  • Base64 encoding: ~1 µs per KB overhead

Memory Usage:

  • Zero allocations for disabled mode
  • ~100 bytes overhead for encryption metadata
  • No persistent state beyond KeyManager reference

Throughput:

  • ~200-500 MB/s single-threaded
  • Linear scaling with concurrent operations
  • Network I/O typically the bottleneck

Thread Safety

The Encryptor is safe for concurrent use:

  • Stateless operations (no internal state mutation)
  • KeyManager handles concurrency internally
  • Each operation is independent

Example (Basic Setup):

// Create key manager and add key
km := encryption.NewKeyManager(encryption.DefaultConfig())
material, _ := encryption.GenerateKey()
km.AddKey(&encryption.Key{
	ID:       1,
	Material: material,
	Active:   true,
})

// Create encryptor
enc := encryption.NewEncryptor(km, true)

// Encrypt data
ciphertext, _ := enc.EncryptString("sensitive data")
fmt.Println(ciphertext) // Base64-encoded result

// Decrypt data
plaintext, _ := enc.DecryptString(ciphertext)
fmt.Println(plaintext) // "sensitive data"

Example (HIPAA PHI Protection):

// Setup encryption for PHI data
km := encryption.NewKeyManager(encryption.Config{
	Enabled: true,
	Rotation: encryption.KeyRotationConfig{
		Enabled:     true,
		Interval:    90 * 24 * time.Hour,
		RetainCount: 8,
	},
})

// Load production key from KMS
material := loadFromKMS("hipaa-phi-key")
km.AddKey(&encryption.Key{
	ID:        1,
	Material:  material,
	CreatedAt: time.Now(),
	Active:    true,
})

enc := encryption.NewEncryptor(km, true)

// Encrypt PHI fields
patient := Patient{
	ID:   "P12345",
	Name: "John Doe",              // Not encrypted (directory info)
}
patient.SSN, _ = enc.EncryptField("123-45-6789")
patient.Diagnosis, _ = enc.EncryptField("Type 2 Diabetes")
patient.Medication, _ = enc.EncryptField("Metformin 500mg")

// Store in database with encrypted PHI
db.Save(patient)

// Retrieve and decrypt
loaded := db.Load("P12345")
ssn, _ := enc.DecryptField(loaded.SSN)
diagnosis, _ := enc.DecryptField(loaded.Diagnosis)
fmt.Printf("SSN: %s, Diagnosis: %s\n", ssn, diagnosis)

Example (Selective Field Encryption):

// Define which fields require encryption
fieldConfig := encryption.FieldEncryptionConfig{
	PHIFields: encryption.DefaultPHIFields(),
	EncryptFields: []string{
		"credit_card", "bank_account", "api_key",
	},
}

enc := encryption.NewEncryptor(km, true)

// Encrypt graph node properties selectively
node := &graph.Node{
	Labels: []string{"User"},
	Properties: map[string]interface{}{
		"id":           "U123",                    // Not encrypted
		"name":         "Alice Smith",             // Not encrypted
		"email":        "alice@example.com",       // Encrypt (PII)
		"phone":        "555-1234",                // Encrypt (PII)
		"credit_card":  "4111-1111-1111-1111",     // Encrypt (PCI)
		"preferences":  "dark_mode",               // Not encrypted
	},
}

// Encrypt sensitive properties
for key, value := range node.Properties {
	if fieldConfig.ShouldEncryptField(key) {
		if strVal, ok := value.(string); ok {
			encrypted, _ := enc.EncryptField(strVal)
			node.Properties[key] = encrypted
		}
	}
}

// Later, decrypt on read
for key, value := range node.Properties {
	if strVal, ok := value.(string); ok {
		if decrypted, err := enc.DecryptField(strVal); err == nil {
			node.Properties[key] = decrypted
		}
	}
}

Example (Dev/Test with Disabled Encryption):

// Development: Skip encryption for faster iteration
devEnc := encryption.NewEncryptor(nil, false)

// Operations become passthrough
data, _ := devEnc.EncryptString("test data")
fmt.Println(data) // Base64 of plaintext (not encrypted)

plaintext, _ := devEnc.DecryptString(data)
fmt.Println(plaintext) // "test data"

// Benefits:
// - No key management needed in dev
// - Faster tests (no crypto overhead)
// - Same API as production code
// - Easy to enable for integration tests

Example (Batch Encryption):

// Encrypt multiple records efficiently
func encryptBatch(enc *encryption.Encryptor, records []Record) error {
	// Reuse encryptor across records (it's thread-safe)
	var wg sync.WaitGroup
	errCh := make(chan error, len(records))

	for i := range records {
		wg.Add(1)
		go func(r *Record) {
			defer wg.Done()

			// Encrypt sensitive fields
			encrypted, err := enc.EncryptField(r.SensitiveData)
			if err != nil {
				errCh <- fmt.Errorf("encrypt record %s: %w", r.ID, err)
				return
			}
			r.SensitiveData = encrypted
		}(&records[i])
	}

	wg.Wait()
	close(errCh)

	// Check for errors
	for err := range errCh {
		if err != nil {
			return err
		}
	}

	return nil
}

// Process 10,000 records in ~100ms
records := loadRecords(10000)
encryptBatch(enc, records)

ELI12 Explanation

Think of the Encryptor as a magic envelope sealer:

When you want to protect a secret message, you put it in the envelope (encrypt), and the envelope sealer:

  1. Stamps a version number on the outside (key version)
  2. Puts your message inside with a special seal (encryption)
  3. Adds a tamper-proof tape (authentication tag)
  4. Converts it to a code you can write down (base64)

When you want to read the message, you give the envelope to the sealer (decrypt), and it:

  1. Reads the version number to know which key to use
  2. Checks the tamper-proof tape (fails if anyone modified it)
  3. Opens the envelope with the right key
  4. Gives you back the original message

If encryption is disabled (like in development), the envelope sealer just wraps your message in clear plastic instead of an opaque envelope - you can still see the message, but it looks the same from the outside.

The KeyManager is like a key ring that holds all the keys, and the Encryptor knows how to use them automatically!

func NewEncryptorWithPassword

func NewEncryptorWithPassword(password string, config Config) (*Encryptor, error)

NewEncryptorWithPassword creates an encryptor with a key derived from password using PBKDF2.

This is a convenience function for simple deployments where key management is derived from a master password rather than external KMS. The password is stretched using PBKDF2 to produce a cryptographically strong 256-bit key.

Key Derivation Process

Password → PBKDF2(password, salt, iterations) → AES-256 key

Parameters:

  • Password: Master password (should be high-entropy)
  • Salt: Unique per installation (prevents rainbow table attacks)
  • Iterations: 600,000+ for OWASP 2023 recommendation

The derived key is stored in a new KeyManager as version 1 and marked active.

Security Considerations

Password Requirements:

  • Minimum 20 characters for high entropy
  • Mix of uppercase, lowercase, numbers, symbols
  • Never hardcode in source code
  • Store in environment variables or secrets manager

Salt Requirements:

  • MUST be unique per installation
  • Generate with GenerateSalt() and persist securely
  • Never use the default salt in production
  • 32 bytes minimum (256 bits)

Iteration Count:

  • OWASP 2023: 600,000 iterations minimum
  • Higher counts increase brute-force resistance
  • Balance security vs. login time (~100ms is acceptable)

Compliance Features

GDPR Art.32 - Security of Processing:

  • Strong key derivation prevents weak password attacks
  • Unique salt per installation
  • Configurable iteration count for future-proofing

HIPAA §164.312(a)(2)(i) - Access Control:

  • Password-based access to encryption keys
  • PBKDF2 meets NIST SP 800-132 requirements
  • Key derivation audit trail

FISMA SC-13 - Cryptographic Protection:

  • NIST-approved key derivation (PBKDF2)
  • SHA-256 as PRF (FIPS 180-4)
  • 256-bit output for AES-256

SOC2 CC6.1 - Encryption:

  • Secure key derivation from passwords
  • Salt uniqueness enforced
  • Iteration count configurable

Performance Characteristics

Key Derivation Time (600,000 iterations):

  • ~100-200ms on modern CPUs
  • Intentionally slow to prevent brute-force
  • One-time cost at application startup

Memory Usage:

  • Minimal: ~1KB during derivation
  • No persistent state beyond KeyManager

Scalability:

  • Derivation is one-time per application instance
  • Subsequent operations use derived key (fast)
  • No impact on per-request performance

Thread Safety

Key derivation is not thread-safe (not required):

  • Call once during application initialization
  • Resulting Encryptor is thread-safe

Example (Basic Setup):

// Load password from environment
password := os.Getenv("NORNICDB_MASTER_PASSWORD")
if password == "" {
	log.Fatal("NORNICDB_MASTER_PASSWORD not set")
}

// Use default config with custom salt
config := encryption.DefaultConfig()
config.KeyDerivation.Salt = []byte("your-unique-installation-salt-32bytes")

// Create encryptor
enc, err := encryption.NewEncryptorWithPassword(password, config)
if err != nil {
	log.Fatalf("Failed to create encryptor: %v", err)
}

// Use for encryption
ciphertext, _ := enc.EncryptString("sensitive data")
fmt.Println(ciphertext)

Example (Production HIPAA Setup):

// Generate and persist unique salt (do this once!)
salt, err := encryption.GenerateSalt()
if err != nil {
	log.Fatal(err)
}
// Store salt in config file or secrets manager
// Example: /etc/nornicdb/salt.key
os.WriteFile("/etc/nornicdb/salt.key", salt, 0600)

// Load salt from secure storage
salt, err := os.ReadFile("/etc/nornicdb/salt.key")
if err != nil {
	log.Fatalf("Failed to load salt: %v", err)
}

// Load master password from secrets manager (e.g., AWS Secrets Manager)
password := loadFromSecretsManager("nornicdb/master-password")

// HIPAA-compliant configuration
config := encryption.Config{
	Enabled: true,
	KeyDerivation: encryption.KeyDerivationConfig{
		Salt:       salt,
		Iterations: 600000,    // OWASP 2023
		UseArgon2:  false,     // Use true if available
	},
	Rotation: encryption.KeyRotationConfig{
		Enabled:     true,
		Interval:    90 * 24 * time.Hour,
		RetainCount: 8,
	},
}

enc, err := encryption.NewEncryptorWithPassword(password, config)
if err != nil {
	log.Fatalf("Encryption setup failed: %v", err)
}

// Encrypt PHI data
patient := &Patient{
	ID:  "P12345",
	MRN: "MRN-98765",
}
patient.SSN, _ = enc.EncryptField("123-45-6789")
patient.DOB, _ = enc.EncryptField("1980-01-15")
db.Save(patient)

Example (Multi-Tenant with Tenant-Specific Keys):

// Derive different keys for each tenant
func createTenantEncryptor(tenantID, password string) (*encryption.Encryptor, error) {
	// Generate tenant-specific salt
	h := sha256.New()
	h.Write([]byte("nornicdb-tenant-salt"))
	h.Write([]byte(tenantID))
	salt := h.Sum(nil)

	config := encryption.Config{
		Enabled: true,
		KeyDerivation: encryption.KeyDerivationConfig{
			Salt:       salt,
			Iterations: 600000,
		},
	}

	return encryption.NewEncryptorWithPassword(password, config)
}

// Each tenant gets isolated encryption
tenant1Enc, _ := createTenantEncryptor("tenant-1", masterPassword)
tenant2Enc, _ := createTenantEncryptor("tenant-2", masterPassword)

// Tenant 1 data encrypted with tenant 1 key
data1, _ := tenant1Enc.EncryptString("tenant 1 data")

// Tenant 2 cannot decrypt tenant 1 data
_, err := tenant2Enc.DecryptString(data1) // Will fail

Example (Development vs Production):

// Development: Fast iterations, weak password OK
devConfig := encryption.Config{
	Enabled: true,
	KeyDerivation: encryption.KeyDerivationConfig{
		Salt:       []byte("dev-salt-not-for-production"),
		Iterations: 10000, // Fast for dev
	},
	Rotation: encryption.KeyRotationConfig{
		Enabled: false, // No rotation in dev
	},
}
devEnc, _ := encryption.NewEncryptorWithPassword("dev-password", devConfig)

// Production: Slow iterations, strong password required
prodConfig := encryption.Config{
	Enabled: true,
	KeyDerivation: encryption.KeyDerivationConfig{
		Salt:       loadProductionSalt(),
		Iterations: 1000000, // Extra secure
	},
	Rotation: encryption.KeyRotationConfig{
		Enabled:     true,
		Interval:    90 * 24 * time.Hour,
		RetainCount: 8,
	},
}
prodPassword := os.Getenv("NORNICDB_MASTER_PASSWORD")
prodEnc, _ := encryption.NewEncryptorWithPassword(prodPassword, prodConfig)

Example (Password Rotation):

// To rotate the master password:
// 1. Create new encryptor with new password
newEnc, err := encryption.NewEncryptorWithPassword(newPassword, config)
if err != nil {
	log.Fatal(err)
}

// 2. Re-encrypt all data
records := db.QueryAll()
for _, record := range records {
	// Decrypt with old encryptor
	plaintext, err := oldEnc.DecryptField(record.EncryptedData)
	if err != nil {
		log.Printf("Failed to decrypt record %s: %v", record.ID, err)
		continue
	}

	// Re-encrypt with new encryptor
	ciphertext, err := newEnc.EncryptField(plaintext)
	if err != nil {
		log.Printf("Failed to encrypt record %s: %v", record.ID, err)
		continue
	}

	// Update record
	record.EncryptedData = ciphertext
	db.Update(record)
}

// 3. Update application to use new password
os.Setenv("NORNICDB_MASTER_PASSWORD", newPassword)

ELI12 Explanation

Imagine you have a password like "SuperSecret123!" but you need a special key that fits exactly into a lock (the encryption algorithm).

NewEncryptorWithPassword is like a key-making machine:

  1. You give it your password (any length, any characters)
  2. It mixes it with a secret ingredient called "salt" (unique per installation)
  3. It stirs the mixture 600,000 times (to make it really hard to guess)
  4. Out comes a perfectly-sized key that fits the lock!

The salt is important because:

  • Two people with the same password get different keys (different salts)
  • Attackers can't use pre-made lists of common passwords (rainbow tables)
  • Each installation is unique

The 600,000 stirs (iterations) are important because:

  • It takes ~100ms to make your key (barely noticeable)
  • An attacker trying to guess takes 100ms per guess
  • With 600,000 stirs, guessing millions of passwords takes years!

This is safer than storing the key directly because:

  • You only need to remember one password
  • The real key is never stored anywhere
  • If someone steals your salt, they still need the password
  • Different installations can't decrypt each other's data

func (*Encryptor) Decrypt

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

Decrypt decrypts base64-encoded ciphertext.

func (*Encryptor) DecryptField

func (e *Encryptor) DecryptField(encrypted string) (string, error)

DecryptField decrypts a field value encrypted by EncryptField.

func (*Encryptor) DecryptString

func (e *Encryptor) DecryptString(ciphertext string) (string, error)

DecryptString decrypts base64 ciphertext and returns the original string.

func (*Encryptor) Encrypt

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

Encrypt encrypts plaintext using AES-256-GCM. Returns base64-encoded ciphertext with key version header.

func (*Encryptor) EncryptField

func (e *Encryptor) EncryptField(value string) (string, error)

EncryptField encrypts a sensitive field value. Returns format: "enc:v{version}:{base64_ciphertext}"

func (*Encryptor) EncryptString

func (e *Encryptor) EncryptString(plaintext string) (string, error)

EncryptString encrypts a string and returns base64 result.

func (*Encryptor) IsEnabled

func (e *Encryptor) IsEnabled() bool

IsEnabled returns whether encryption is enabled.

func (*Encryptor) KeyManager

func (e *Encryptor) KeyManager() *KeyManager

KeyManager returns the underlying key manager.

type EnvelopeConfig

type EnvelopeConfig struct {
	DEKCacheTTL time.Duration
	Label       string
}

type EnvelopeEncryptor

type EnvelopeEncryptor struct {
	// contains filtered or unexported fields
}

func NewEnvelopeEncryptor

func NewEnvelopeEncryptor(provider kms.KeyProvider, cfg EnvelopeConfig) *EnvelopeEncryptor

func (*EnvelopeEncryptor) Decrypt

func (e *EnvelopeEncryptor) Decrypt(ctx context.Context, envelope []byte) ([]byte, error)

func (*EnvelopeEncryptor) Encrypt

func (e *EnvelopeEncryptor) Encrypt(ctx context.Context, plaintext []byte) ([]byte, error)

type FieldEncryptionConfig

type FieldEncryptionConfig struct {
	// Fields to encrypt by property name
	EncryptFields []string

	// Fields containing PHI/PII that require encryption (for compliance)
	PHIFields []string

	// Regex patterns for field names to encrypt
	FieldPatterns []string
}

FieldEncryptionConfig defines which fields should be encrypted.

func (*FieldEncryptionConfig) ShouldEncryptField

func (c *FieldEncryptionConfig) ShouldEncryptField(fieldName string) bool

ShouldEncryptField checks if a field should be encrypted based on config.

type Key

type Key struct {
	ID        uint32    // Key version ID
	Material  []byte    // 32-byte AES-256 key
	CreatedAt time.Time // When key was created
	ExpiresAt time.Time // When key expires (zero = never)
	Active    bool      // Whether key can be used for new encryption
}

Key represents an encryption key with metadata.

func (*Key) IsExpired

func (k *Key) IsExpired() bool

IsExpired returns true if the key has expired.

func (*Key) Validate

func (k *Key) Validate() error

Validate checks if the key is valid for use.

type KeyDerivationConfig

type KeyDerivationConfig struct {
	// Salt for key derivation (should be unique per installation)
	Salt []byte

	// PBKDF2 iterations (default: 600000 for OWASP recommendation)
	Iterations int

	// Use Argon2id instead of PBKDF2 (recommended)
	UseArgon2 bool
}

KeyDerivationConfig configures key derivation from password.

type KeyManager

type KeyManager struct {
	// contains filtered or unexported fields
}

KeyManager manages encryption keys with rotation support.

func NewKeyManager

func NewKeyManager(config Config) *KeyManager

NewKeyManager creates a new key manager for encryption key lifecycle management.

The KeyManager provides:

  • Multi-version key storage with concurrent access
  • Automatic key rotation with configurable intervals
  • Key expiration and retention policy enforcement
  • Thread-safe operations for high-concurrency environments
  • Support for external KMS integration

Key Rotation Strategy

Keys are versioned and rotated based on the configured interval:

  1. New keys are generated with sequential version IDs
  2. Old keys remain available for decryption of legacy data
  3. Expired keys beyond retention count are automatically cleaned up
  4. Each encrypted value stores its key version for transparent decryption

Compliance Features

GDPR Art.32 - Security of Processing:

  • Cryptographic key lifecycle management
  • Automatic key rotation reduces exposure window
  • Secure key derivation with PBKDF2/Argon2

HIPAA §164.312(e)(2)(ii) - Encryption and Decryption:

  • AES-256-GCM authenticated encryption
  • Key rotation for PHI data protection
  • Audit trail through key version tracking

FISMA SC-13 - Cryptographic Protection:

  • NIST-approved algorithms (AES-256, PBKDF2)
  • Key management best practices
  • Separation of key material from data

SOC2 CC6.1 - Logical and Physical Access Controls:

  • Centralized key management
  • Key version tracking for audit
  • Secure key generation with crypto/rand

Thread Safety

All KeyManager operations are thread-safe:

  • Read operations use RLock for concurrent access
  • Write operations (AddKey, RotateKey) use exclusive Lock
  • Safe for use across multiple goroutines

Performance Characteristics

Key Operations:

  • GetKey(): O(1) map lookup with read lock
  • CurrentKey(): O(1) with validation
  • AddKey(): O(1) with write lock
  • RotateKey(): O(n) where n = keys to cleanup

Memory Usage:

  • 32 bytes per key + metadata (~100 bytes)
  • Automatic cleanup keeps memory bounded
  • Default retention: 5 keys = ~660 bytes

Concurrency:

  • Read operations don't block each other
  • Write operations serialize but are infrequent
  • Rotation typically happens once per 90 days

Example (Basic Setup):

// Create with default config
config := encryption.DefaultConfig()
km := encryption.NewKeyManager(config)

// Generate and add initial key
material, _ := encryption.GenerateKey()
key := &encryption.Key{
	ID:        1,
	Material:  material,
	CreatedAt: time.Now(),
	Active:    true,
}
km.AddKey(key)

// Use with encryptor
enc := encryption.NewEncryptor(km, true)
ciphertext, _ := enc.EncryptString("sensitive data")

Example (Production HIPAA Setup):

// HIPAA-compliant configuration
config := encryption.Config{
	Enabled: true,
	KeyDerivation: encryption.KeyDerivationConfig{
		Salt:       mustGenerateSalt(),        // Unique per installation
		Iterations: 600000,                    // OWASP 2023 recommendation
		UseArgon2:  true,                      // Recommended for new deployments
	},
	Rotation: encryption.KeyRotationConfig{
		Enabled:     true,
		Interval:    90 * 24 * time.Hour,      // Rotate quarterly
		RetainCount: 8,                        // Keep 2 years of keys
	},
}

km := encryption.NewKeyManager(config)

// Load master key from secure vault (e.g., AWS KMS, HashiCorp Vault)
masterKey := loadFromVault("nornicdb-master-key")
key := &encryption.Key{
	ID:        1,
	Material:  masterKey,
	CreatedAt: time.Now(),
	Active:    true,
	ExpiresAt: time.Now().Add(180 * 24 * time.Hour), // Allow 2x rotation period
}
km.AddKey(key)

// Schedule automatic rotation
go func() {
	ticker := time.NewTicker(config.Rotation.Interval)
	for range ticker.C {
		newKey, err := km.RotateKey()
		if err != nil {
			log.Printf("Key rotation failed: %v", err)
			continue
		}
		log.Printf("Rotated to key v%d, hash=%s", newKey.ID, encryption.HashKey(newKey.Material))

		// Optional: Re-encrypt critical data with new key
		// This is application-specific and may be done lazily
	}
}()

Example (Multi-Environment):

// Development: Single key, no rotation
devConfig := encryption.Config{
	Enabled: true,
	Rotation: encryption.KeyRotationConfig{
		Enabled: false, // No rotation in dev
	},
}
devKM := encryption.NewKeyManager(devConfig)

// Staging: Shorter rotation for testing
stageConfig := encryption.DefaultConfig()
stageConfig.Rotation.Interval = 7 * 24 * time.Hour  // Weekly rotation
stageConfig.Rotation.RetainCount = 4                // 4 weeks history
stageKM := encryption.NewKeyManager(stageConfig)

// Production: Compliance-driven settings
prodConfig := encryption.DefaultConfig()
prodConfig.Rotation.Interval = 90 * 24 * time.Hour  // Quarterly
prodConfig.Rotation.RetainCount = 8                 // 2 years
prodKM := encryption.NewKeyManager(prodConfig)

Example (External KMS Integration):

// AWS KMS integration example
type KMSKeyManager struct {
	*encryption.KeyManager
	kmsClient *kms.Client
	keyID     string
}

func NewKMSKeyManager(kmsClient *kms.Client, keyID string) *KMSKeyManager {
	km := encryption.NewKeyManager(encryption.DefaultConfig())
	return &KMSKeyManager{
		KeyManager: km,
		kmsClient:  kmsClient,
		keyID:      keyID,
	}
}

func (k *KMSKeyManager) RotateKey() (*encryption.Key, error) {
	// Generate data key from KMS
	resp, err := k.kmsClient.GenerateDataKey(context.Background(), &kms.GenerateDataKeyInput{
		KeyId:   aws.String(k.keyID),
		KeySpec: types.DataKeySpecAes256,
	})
	if err != nil {
		return nil, fmt.Errorf("KMS GenerateDataKey failed: %w", err)
	}

	// Store encrypted key for backup
	// In production, store resp.CiphertextBlob in secure storage

	// Add to key manager
	key := &encryption.Key{
		ID:        k.KeyManager.KeyCount() + 1,
		Material:  resp.Plaintext,
		CreatedAt: time.Now(),
		Active:    true,
	}
	if err := k.AddKey(key); err != nil {
		return nil, err
	}

	// Securely wipe plaintext from memory
	encryption.SecureWipe(resp.Plaintext)

	return key, nil
}

Example (Key Version Migration):

// Migrate data from old key to new key
func migrateEncryptedData(km *encryption.KeyManager, oldVersion, newVersion uint32) error {
	oldEnc := encryption.NewEncryptor(km, true)
	newEnc := encryption.NewEncryptor(km, true)

	// Force old key for decryption
	oldKey, _ := km.GetKey(oldVersion)
	newKey, _ := km.GetKey(newVersion)

	// Query all encrypted records
	records := queryEncryptedRecords()

	for _, record := range records {
		// Decrypt with old key
		plaintext, err := oldEnc.DecryptString(record.EncryptedField)
		if err != nil {
			return fmt.Errorf("decrypt failed for record %s: %w", record.ID, err)
		}

		// Re-encrypt with new key
		ciphertext, err := newEnc.EncryptString(plaintext)
		if err != nil {
			return fmt.Errorf("encrypt failed for record %s: %w", record.ID, err)
		}

		// Update record
		record.EncryptedField = ciphertext
		record.KeyVersion = newVersion
		updateRecord(record)

		log.Printf("Migrated record %s from v%d to v%d", record.ID, oldVersion, newVersion)
	}

	return nil
}

ELI12 Explanation

Imagine you have a secret diary with a special lock. The KeyManager is like a lockbox that holds multiple keys, each with a number (version 1, 2, 3...).

When you write a new secret (encrypt data), you use the newest key and write its number on the page. Later, when you want to read that secret (decrypt), you look at the number and use the matching key from the lockbox.

Every few months, you create a new key and start using it for new secrets (key rotation). But you keep the old keys so you can still read secrets written with them. After 2 years, you throw away really old keys you don't need anymore (retention policy).

This is safer because:

  • If someone steals your current key, they can't read old secrets
  • If an old key is compromised, new secrets are safe
  • You always know which key was used (version tracking)
  • Multiple people can read secrets at once (thread safety)

The KeyManager makes sure all this happens automatically, so you just write and read secrets without worrying about which key to use!

func (*KeyManager) AddKey

func (km *KeyManager) AddKey(key *Key) error

AddKey adds a key to the manager.

func (*KeyManager) CurrentKey

func (km *KeyManager) CurrentKey() (*Key, error)

CurrentKey returns the current active key for encryption.

func (*KeyManager) GetKey

func (km *KeyManager) GetKey(version uint32) (*Key, error)

GetKey retrieves a key by version ID.

func (*KeyManager) KeyCount

func (km *KeyManager) KeyCount() int

KeyCount returns the number of keys in the manager.

func (*KeyManager) RotateKey

func (km *KeyManager) RotateKey() (*Key, error)

RotateKey generates a new key and sets it as current.

type KeyRotationConfig

type KeyRotationConfig struct {
	// Enable automatic key rotation
	Enabled bool

	// Interval between key rotations
	Interval time.Duration

	// Number of old keys to keep for decryption
	RetainCount int
}

KeyRotationConfig configures automatic key rotation.

type Reencryptor

type Reencryptor interface {
	ReencryptWithDataKey(ctx context.Context, key *kms.DataKey) (updated int, err error)
}

type RotationConfig

type RotationConfig struct {
	Enabled        bool
	Interval       time.Duration
	RetentionCount int
}

type RotationManager

type RotationManager struct {
	// contains filtered or unexported fields
}

func NewRotationManager

func NewRotationManager(provider kms.KeyProvider, reencryptor Reencryptor, cfg RotationConfig) *RotationManager

func (*RotationManager) Start

func (rm *RotationManager) Start(ctx context.Context)

func (*RotationManager) Stop

func (rm *RotationManager) Stop()

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL