# Defense Patterns: Cryptography and Secret Management
> How to store, handle, and destroy secrets correctly — from OS keychain to memory zeroization.

---

## 1. OS Keychain Integration

Every major OS provides a hardware-backed or OS-protected credential store. Use it as the root of trust for small, high-value secrets (master keys, API tokens, passphrases).

**By platform:**

| Platform | Mechanism | Notes |
|---|---|---|
| macOS | Keychain Services (SecItemAdd / SecItemCopyMatching) | Backed by Secure Enclave on Apple Silicon. System Keychain accessible to daemons at boot. |
| Windows | Credential Manager via DPAPI | `CryptProtectData` ties the blob to the user account. Machine-scope (`LOCAL_MACHINE`) usable for services. Limit: ~2,500 bytes per entry. |
| Linux (desktop) | Secret Service API (GNOME Keyring / KDE Wallet) via D-Bus | No per-application isolation — any same-user process can read all entries. |
| Linux (headless / CI) | Kernel keyring (`keyutils`) | No D-Bus required. Keys are session-scoped and do not survive reboots. |
| Linux (systemd ≥ 250) | `systemd-creds` | AES-256-GCM, optionally bound to TPM2. Credentials delivered as ramfs at service start. |

**Design principle:** The OS keychain stores only one thing — a 32-byte master key generated by a CSPRNG at setup time. All other secrets live in an encrypted vault file, unlocked by this master key. This pattern bypasses the size limit of Windows DPAPI and the isolation weakness of Linux Secret Service.

**Daemon-at-boot caveat:** A daemon that starts before user login cannot access a login-session keychain (macOS Data Protection Keychain, Linux D-Bus session). Design a fallback: systemd-creds, a locked mode requiring an explicit `unlock` command, or a file-based key with filesystem-permission protection as a last resort.

---

## 2. Encrypted File Storage for Bulk Secrets

For structured secrets that exceed keychain entry size limits, use a dedicated encrypted file unlocked by the keychain-held master key.

**Recommended approach:**
- Serialize all secrets (tokens, credentials, key material) into a structured format (JSON, TOML, or binary serialization).
- Encrypt the blob with AES-256-GCM using the master key. Prepend the randomly generated nonce (12 bytes) to the ciphertext.
- Store the file with restrictive permissions (mode `0600` on Unix).

**Why AES-256-GCM and not other modes:**
- CBC: vulnerable to padding oracle attacks (POODLE 2014, BEAST 2011).
- ECB: identical plaintext blocks produce identical ciphertext — reveals structure.
- CTR without authentication: malleable, no integrity guarantee.
- AES-256-GCM: authenticated encryption (Encrypt-then-MAC integrated), single-pass, hardware-accelerated on all modern CPUs.

**Nonce discipline:** Every encryption operation must use a fresh nonce generated by a CSPRNG. Nonce reuse with the same key under AES-GCM is catastrophic — it breaks both confidentiality and authentication. Never derive a nonce deterministically from the data being encrypted.

**Phantom proxy pattern for untrusted environments:** When a secret must be available to an agent, plugin, or untrusted subprocess, never pass the real credential. Instead, the trusted process holds the credential and the untrusted component calls back to request specific operations. The real secret never enters the untrusted address space.

---

## 3. Memory Zeroization After Use

Garbage-collected and managed runtimes do not guarantee that freed memory is overwritten before reuse. Language-level garbage collection is not a security mechanism for secrets.

**What standard cleanup does NOT do:**
- `del variable` (Python): removes the reference; the `str` object may persist in the interpreter's intern pool.
- `drop(value)` (Rust): calls the destructor and returns memory to the allocator; the allocator does not overwrite the bytes.
- `string = null` (Java/JS): removes the reference; GC timing and memory layout are uncontrolled.
- Compiler optimization: a zero-fill write followed by no further use of the buffer is a "dead store" that compilers are permitted to eliminate at -O2 and above.

**Correct approach — key properties of a zeroization implementation:**
1. Uses a write operation the compiler cannot prove is unobservable (e.g., `volatile` write, a platform memory-barrier primitive, or a dedicated OS call like `explicit_bzero`).
2. Applies a memory fence after writing to prevent reordering.
3. Operates on the full allocated buffer capacity, not just the used length.
4. Is applied on every code path, including error paths and panics (use RAII / `Drop` where the language supports it).

**Zeroize on copy:** If a secret is copied before use (e.g., deserialized into a temporary buffer, formatted into a string for an HTTP header), the copy must also be zeroized. Log strings, serialized payloads, and formatted messages that include secret material are the most common accidental copies.

**mlock (optional hardening):** Locking memory pages prevents the OS from swapping them to disk. This is defense-in-depth, not a replacement for zeroization. Budget is limited on Linux (~2-8 MB for non-root). Prioritize: zeroize on drop > encrypted at rest > mlock.

---

## 4. CSPRNG for All Random Values

Never use the standard pseudo-random number generator (PRNG) for any security-sensitive value. Standard PRNGs are designed for speed and distribution, not unpredictability.

**Use a Cryptographically Secure Pseudo-Random Number Generator (CSPRNG) for:**
- Encryption keys and IVs/nonces
- Session tokens and API keys
- CSRF tokens
- OTP secrets and salts
- Any ID that must be unguessable

**What "CSPRNG" means by platform:** The OS exposes a CSPRNG backed by hardware entropy: `/dev/urandom` (Linux), `CryptGenRandom` / `BCryptGenRandom` (Windows), `SecRandomCopyBytes` (macOS), `getrandom(2)` syscall (Linux 3.17+). All modern cryptographic libraries use these internally — prefer library-provided key generation over raw OS calls to avoid mistakes in buffer sizing and entropy seeding.

**Standard PRNG failures on record:** Debian OpenSSL bug (2008): a patch accidentally eliminated entropy seeding, making all keys generated on Debian systems for two years predictable from the process ID alone. Approximately 32,000 weak key pairs were generated and remained in use.

---

## 5. Constant-Time Comparison for All Secrets

Never compare secrets — tokens, hashes, MACs, OTPs, API keys — with standard equality operators (`==`, `===`, `strcmp`, `equals()`). These operators return early on the first differing byte, leaking timing information.

**When to use constant-time comparison:**
- Comparing a provided token against a stored token
- Verifying an HMAC or MAC tag
- Checking an OTP or TOTP code
- Comparing any value derived from a secret

**How constant-time comparison works:** XOR every byte of both buffers and accumulate the result. Return equal only if the accumulated XOR is zero. The loop always runs for the full length, regardless of where a mismatch occurs.

**Length check first:** If lengths differ, return false immediately — but do not leak which length is correct. Compare against a fixed canonical length, or return false for length mismatch before the constant-time loop.

**CVE on record:** CVE-2022-29185 / RUSTSEC-2022-0018 — `totp-rs` < v1.1.0 used `==` for TOTP code comparison, making timing oracle attacks automatable with approximately 200 measurements per position.

---

## 6. Recommended Algorithm Reference

| Purpose | Recommended algorithm | Avoid |
|---|---|---|
| Password / passphrase hashing | Argon2id (m=64 MiB, t=3, p=4) | MD5, SHA-1, unsalted SHA-256, bcrypt with cost < 10 |
| Symmetric encryption | AES-256-GCM | DES, 3DES, AES-ECB, AES-CBC without MAC |
| Data integrity (non-secret) | SHA-256 or BLAKE3 | MD5, SHA-1, CRC32 |
| Digital signatures | Ed25519 | RSA < 2048 bits, ECDSA with P-192 |
| Key exchange | X25519 | DH with groups < 2048 bits |
| Key derivation from high-entropy material | HKDF-SHA256 | Simple hash truncation |
| Key derivation from low-entropy passphrase | Argon2id | PBKDF2 with < 600,000 iterations (SHA-256) or < 210,000 (SHA-512) |
| CSPRNG | OS-provided (getrandom, BCryptGenRandom, SecRandomCopyBytes) | `Math.random()`, `rand()`, `mt_rand()` |

**Argon2id parameter rationale:** The `id` variant resists both GPU attacks (time-hard component from Argon2d) and side-channel attacks (data-independent memory access from Argon2i). OWASP 2024 recommends minimum m=19 MiB; use m=64 MiB where feasible.

---

## 7. Code Review Checklist

Before every release, verify each item is true across the entire codebase:

**Secret storage:**
- [ ] No secret is stored in a standard string type without a zeroize-on-drop wrapper
- [ ] All long-lived secrets (master keys, API tokens) are stored in the OS keychain or an encrypted vault, never in plaintext config files
- [ ] No secret appears in a `.env` file committed to version control (check `git log --all`)
- [ ] No secret is hardcoded in source code (run a secrets scanner on every commit)

**Cryptography:**
- [ ] All keys and nonces are generated by a CSPRNG, never by a standard PRNG
- [ ] AES-GCM nonces are generated fresh per operation, never reused, never deterministic
- [ ] All secret comparisons use constant-time functions, not `==` or `strcmp`
- [ ] Password/passphrase hashing uses Argon2id with parameters at or above OWASP minimums
- [ ] Encryption uses AES-256-GCM or ChaCha20-Poly1305 — no CBC, no ECB, no unauthenticated CTR

**Memory:**
- [ ] Secrets are zeroized on every exit path (normal return, error, and panic/exception)
- [ ] Temporary copies created during serialization, formatting, or IPC are also zeroized
- [ ] Core dumps are disabled in production (`RLIMIT_CORE = 0` on Linux, WER disabled on Windows)
- [ ] Log output is filtered — no log line may contain a key, token, or password

**Inter-process:**
- [ ] Secrets are never passed as CLI arguments
- [ ] IPC messages carrying secrets are authenticated (HMAC or equivalent)
- [ ] Subprocess binaries are integrity-checked (hash verification) before execution
- [ ] The phantom proxy pattern is used when secrets must not enter an untrusted process
