Frontend Labs
⋯

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.


Generated from week6.md · 8 Oct 2026 16:36