# Encryption keys are read from env, not hardcoded

- **Pattern:** `ab-000289` (`ai-slop-security-theater.half-wired-crypto.crypto-keys-from-env`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000289
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000289)

## Why it matters

A hardcoded AES key like `'this-is-a-32-character-secret!!'` passed to `crypto.createCipheriv()` is committed to source control and therefore permanently exposed to every developer who has ever had repo access, every CI system, every build log, and every git history tool. CWE-321 (Use of Hard-coded Cryptographic Key) and OWASP A02 (Cryptographic Failures) describe the consequence: if the key is compromised — and it is, the moment it was committed — every record encrypted with that key is decryptable by anyone with the history.

## Severity rationale

Medium because hardcoded crypto keys are permanently exposed in source history, but exploitation requires access to the codebase or ciphertext — not just network access.

## Remediation

Read the key from an environment variable and validate it at startup with a Zod schema to fail fast if it's missing or malformed.

```ts
// Bad: key is a string literal in source
const cipher = crypto.createCipheriv('aes-256-cbc', 'this-is-a-32-character-secret!!', iv)

// Good: key from env, validated at startup
import { z } from 'zod'
const Env = z.object({ ENCRYPTION_KEY: z.string().length(64) })  // 32 bytes = 64 hex chars
const env = Env.parse(process.env)
const key = Buffer.from(env.ENCRYPTION_KEY, 'hex')
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv)
```

Generate a secure key with `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"` and store it in `.env.local` (never commit it). Rotate the key and re-encrypt all records if it was previously hardcoded.

## Detection

- **ID:** `crypto-keys-from-env`
- **Severity:** `medium`
- **What to look for:** Walk source files for calls to `crypto.createCipheriv(`, `crypto.createDecipheriv(`, `crypto.createHmac(`, `crypto.scryptSync(`, `crypto.createSign(`, `crypto.createVerify(`, `nacl.secretbox(`, `nacl.box.keyPair.fromSecretKey(`, `aes.encrypt(`, `aes.decrypt(`, `Cipher(` (CryptoJS). For each call site, before evaluating, extract and quote the key argument expression. Count all call sites where the key argument is a string literal of length 16/24/32/64 (likely hardcoded key) versus those where the key references `process.env.X`, `env.X`, or `Buffer.from(process.env.X, 'hex')`.
- **Pass criteria:** 100% of crypto calls source their key from environment variables. Report: "X crypto call sites, Y use env-based keys, 0 hardcoded."
- **Fail criteria:** At least 1 crypto call passes a string literal as the key argument.
- **Skip (N/A) when:** No `crypto.createCipheriv` / `nacl` / `crypto-js` calls anywhere in source.
- **Detail on fail:** `"1 hardcoded crypto key: src/lib/encrypt.ts line 8 calls crypto.createCipheriv('aes-256-cbc', 'this-is-a-32-character-secret!!', iv) — key is a literal, not from env"`
- **Remediation:** Hardcoded crypto keys are committed to source control and visible in every backup, log, and stack trace. Always read keys from env:

  ```ts
  // Bad: hardcoded key
  const cipher = crypto.createCipheriv('aes-256-cbc', 'this-is-a-32-character-secret!!', iv)

  // Good: env-based key
  const key = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex')
  const cipher = crypto.createCipheriv('aes-256-cbc', key, iv)
  ```

  Use a typed env schema (Zod) to fail fast at startup if `ENCRYPTION_KEY` is missing.

## External references

- cwe CWE-321
- cwe CWE-798
- owasp A02
- owasp A07

Taxons: cryptography-and-secrets

HTML version: https://auditbuffet.com/patterns/ab-000289
