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 ¶
- Variables
- func DefaultPHIFields() []string
- func DeriveKey(password, salt []byte, iterations int) []byte
- func GenerateKey() ([]byte, error)
- func GenerateSalt() ([]byte, error)
- func HashKey(key []byte) string
- func SecureWipe(data []byte)
- type Config
- type Encryptor
- func (e *Encryptor) Decrypt(ciphertext string) ([]byte, error)
- func (e *Encryptor) DecryptField(encrypted string) (string, error)
- func (e *Encryptor) DecryptString(ciphertext string) (string, error)
- func (e *Encryptor) Encrypt(plaintext []byte) (string, error)
- func (e *Encryptor) EncryptField(value string) (string, error)
- func (e *Encryptor) EncryptString(plaintext string) (string, error)
- func (e *Encryptor) IsEnabled() bool
- func (e *Encryptor) KeyManager() *KeyManager
- type EnvelopeConfig
- type EnvelopeEncryptor
- type FieldEncryptionConfig
- type Key
- type KeyDerivationConfig
- type KeyManager
- type KeyRotationConfig
- type Reencryptor
- type RotationConfig
- type RotationManager
Constants ¶
This section is empty.
Variables ¶
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 ¶
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:
- Mixes everything together
- Blends it thousands of times (600,000!)
- 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 ¶
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 ¶
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 ¶
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:
- Stamps a version number on the outside (key version)
- Puts your message inside with a special seal (encryption)
- Adds a tamper-proof tape (authentication tag)
- 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:
- Reads the version number to know which key to use
- Checks the tamper-proof tape (fails if anyone modified it)
- Opens the envelope with the right key
- 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 ¶
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:
- You give it your password (any length, any characters)
- It mixes it with a secret ingredient called "salt" (unique per installation)
- It stirs the mixture 600,000 times (to make it really hard to guess)
- 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) DecryptField ¶
DecryptField decrypts a field value encrypted by EncryptField.
func (*Encryptor) DecryptString ¶
DecryptString decrypts base64 ciphertext and returns the original string.
func (*Encryptor) Encrypt ¶
Encrypt encrypts plaintext using AES-256-GCM. Returns base64-encoded ciphertext with key version header.
func (*Encryptor) EncryptField ¶
EncryptField encrypts a sensitive field value. Returns format: "enc:v{version}:{base64_ciphertext}"
func (*Encryptor) EncryptString ¶
EncryptString encrypts a string and returns base64 result.
func (*Encryptor) KeyManager ¶
func (e *Encryptor) KeyManager() *KeyManager
KeyManager returns the underlying key manager.
type EnvelopeConfig ¶
type EnvelopeEncryptor ¶
type EnvelopeEncryptor struct {
// contains filtered or unexported fields
}
func NewEnvelopeEncryptor ¶
func NewEnvelopeEncryptor(provider kms.KeyProvider, cfg EnvelopeConfig) *EnvelopeEncryptor
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.
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:
- New keys are generated with sequential version IDs
- Old keys remain available for decryption of legacy data
- Expired keys beyond retention count are automatically cleaned up
- 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 RotationConfig ¶
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()