Day 43 — Symmetric Encryption (AES-GCM)
Learning Objectives
- Encrypt sensitive data at rest with AES-256-GCM
- Understand why GCM > CBC (authenticated encryption)
- Build an encrypted audit log — PII encrypted in transit and at rest
Key Concepts
AES-GCM (Galois/Counter Mode):
- Authenticated encryption: provides both confidentiality AND integrity
- If ciphertext is tampered with, Decrypt() returns an error
- Requires a unique nonce for every encryption (never reuse!)
- 96-bit (12-byte) nonce: standard for GCM
- 256-bit (32-byte) key: AES-256
AES-CBC (Cipher Block Chaining):
- Older mode — no authentication (attacker can flip bits without detection)
- Requires padding (PKCS#7) — vulnerable to padding oracle attacks
- Avoid for new designs
Key derivation:
- Never use passwords directly as AES keys
- Use PBKDF2, scrypt, or Argon2 to derive a key from a password
- For service-to-service: use a random 32-byte key from crypto/rand
Exercise: Encrypted Audit Log
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/json"
"fmt"
"io"
"os"
"time"
)
// AuditRecord represents a security event
type AuditRecord struct {
Time time.Time `json:"time"`
RequestID string `json:"request_id"`
UserID string `json:"user_id"`
Action string `json:"action"`
Resource string `json:"resource"`
IP string `json:"client_ip"`
Result string `json:"result"` // allow, deny, block
}
// EncryptedAuditLog writes AES-256-GCM encrypted audit records
type EncryptedAuditLog struct {
key []byte // 32 bytes (AES-256)
file *os.File
}
func NewEncryptedAuditLog(path string, keyHex string) (*EncryptedAuditLog, error) {
// Derive key from hex string (in production: load from secrets manager)
key := make([]byte, 32)
if n, err := fmt.Sscanf(keyHex, "%x", &key); n != 1 || err != nil {
// Generate a random key if not provided
if _, err := io.ReadFull(rand.Reader, key); err != nil {
return nil, err
}
fmt.Printf("Generated key: %x\n", key)
}
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil { return nil, err }
return &EncryptedAuditLog{key: key, file: f}, nil
}
// Write encrypts and appends an audit record
func (l *EncryptedAuditLog) Write(record AuditRecord) error {
// Serialize record
plaintext, err := json.Marshal(record)
if err != nil { return err }
// Create AES-256-GCM cipher
block, err := aes.NewCipher(l.key)
if err != nil { return err }
gcm, err := cipher.NewGCM(block)
if err != nil { return err }
// Generate random nonce (must be unique per encryption)
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return err }
// Encrypt: nonce + ciphertext + GCM auth tag
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
// Write length-prefixed encrypted record
// Format: [4 bytes length][nonce+ciphertext+tag]
length := make([]byte, 4)
binary.BigEndian.PutUint32(length, uint32(len(ciphertext)))
l.file.Write(length)
l.file.Write(ciphertext)
return nil
}
// ReadAll decrypts all audit records from the log file
func ReadAll(path string, key []byte) ([]AuditRecord, error) {
f, err := os.Open(path)
if err != nil { return nil, err }
defer f.Close()
block, err := aes.NewCipher(key)
if err != nil { return nil, err }
gcm, err := cipher.NewGCM(block)
if err != nil { return nil, err }
var records []AuditRecord
lengthBuf := make([]byte, 4)
for {
if _, err := io.ReadFull(f, lengthBuf); err != nil {
if err == io.EOF { break }
return nil, err
}
length := binary.BigEndian.Uint32(lengthBuf)
ciphertext := make([]byte, length)
if _, err := io.ReadFull(f, ciphertext); err != nil { return nil, err }
nonceSize := gcm.NonceSize()
if len(ciphertext) < nonceSize { return nil, fmt.Errorf("ciphertext too short") }
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil { return nil, fmt.Errorf("decrypt failed (tampered?): %w", err) }
var record AuditRecord
if err := json.Unmarshal(plaintext, &record); err != nil { return nil, err }
records = append(records, record)
}
return records, nil
}
func main() {
log, err := NewEncryptedAuditLog("audit.enc", "")
if err != nil { fmt.Println("ERROR:", err); return }
records := []AuditRecord{
{Time: time.Now(), RequestID: "req-001", UserID: "user:42", Action: "login", Resource: "/auth/login", IP: "10.0.0.1", Result: "allow"},
{Time: time.Now(), RequestID: "req-002", UserID: "", Action: "access", Resource: "/api/admin", IP: "203.0.113.1", Result: "deny"},
}
for _, r := range records {
if err := log.Write(r); err != nil {
fmt.Println("write error:", err)
}
}
fmt.Println("Wrote encrypted audit log: audit.enc")
fmt.Println("Content is encrypted — cat audit.enc shows only ciphertext")
}
Real-World Context
HIPAA requires encryption of PHI at rest. PCI DSS requires encryption of cardholder data. GDPR requires appropriate technical measures for PII. AES-256-GCM with a random nonce per record is the correct pattern. The GCM auth tag detects tampering — if someone modifies the log file, decryption fails.