Skip to content
Back to Quality Assurance

Cryptography Best Practices

Use cryptography correctly: the right algorithm, the right mode, the right key management — never roll your own.

Why

Cryptography is one of the few areas where "it works" and "it is secure" are wildly different. A home-rolled encryption function produces ciphertext that decrypts correctly and looks encrypted, but is breakable in seconds by anyone who knows the pattern. Using AES-ECB (an insecure mode) produces ciphertext that decrypts fine but leaks plaintext patterns — the famous ECB penguin. Using a fast hash (MD5, SHA1) for passwords lets an attacker brute-force billions per second on a GPU. The only safe approach is to use well-reviewed libraries, the algorithms and modes that cryptographers recommend, and to never customize. The library is correct; the danger is in choosing the wrong option from the library's menu.

When

Whenever you encrypt, hash, sign, or generate random values. Review all crypto usage during the security audit. If you are writing crypto code from scratch (not calling a library), stop and call a library.

How

1. Passwords: use a slow, salted, memory-hard hash
   - bcrypt (cost ≥ 12), argon2id (preferred), scrypt — never MD5, SHA1, SHA256
   - The slowness is the feature — it makes brute-force infeasible
   - Store the hash + salt + parameters together (bcrypt encodes all three)

2. Symmetric encryption: AES-256 in GCM mode (authenticated encryption)
   - GCM provides confidentiality AND integrity (a tampered ciphertext is detected)
   - Never use ECB (leaks patterns) or CBC without a MAC (malleable)
   - Use a unique IV per encryption (GCM generates it; never reuse)

3. Asymmetric: use the library's high-level APIs, not the primitives
   - TLS for transport (never implement yourself), libsodium for sealing/boxing
   - RSA-OAEP for encryption, RSA-PSS or Ed25519 for signatures — never PKCS1v1.5
   - Use 2048+ bit RSA or 256-bit curve (Ed25519, X25519) keys

4. Random: use the cryptographically secure PRNG, never Math.random()
   - crypto.randomBytes() (Node), secrets module (Python), getrandom (Go)
   - Math.random() is predictable and must never be used for tokens, IDs, or keys

5. Key management: never hardcode, rotate, separate by purpose
   - One key per purpose (encryption, signing, API auth) — never reuse a key
   - Store in a secrets manager (Vault, KMS), rotate on a schedule
   - Derive per-record keys with HKDF if you need many keys from one master

Example

// ❌ WRONG — home-rolled, breakable in seconds
function encrypt(secret, key) {
  return secret.split("").map(c =>
    String.fromCharCode(c.charCodeAt(0) + key)  // Caesar cipher — not encryption
  ).join("");
}

// ❌ WRONG — ECB leaks plaintext patterns
import crypto from "crypto";
const ct = crypto.createCipheriv("aes-256-ecb", key, null).update(pt);

// ❌ WRONG — MD5 is brute-forceable at billions/sec on a GPU
const hash = crypto.createHash("md5").update(password).digest("hex");

// ✅ CORRECT — bcrypt for passwords, AES-256-GCM for data
import bcrypt from "bcrypt";
import crypto from "crypto";

// Password hashing — slow, salted, memory-hard
const hash = await bcrypt.hash(password, 12);          // cost factor 12
const valid = await bcrypt.compare(password, hash);     // constant-time compare

// Symmetric encryption — authenticated, unique IV per message
const iv = crypto.randomBytes(12);                       // 96-bit IV for GCM
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
const ct = Buffer.concat([cipher.update(pt), cipher.final()]);
const tag = cipher.getAuthTag();                         // integrity tag
// Store iv + tag + ct; decrypt verifies the tag before returning plaintext.

// Random tokens — cryptographically secure
const token = crypto.randomBytes(32).toString("hex");   // not Math.random()

Common Problems

  • ⚠Using a fast hash (MD5, SHA1, SHA256) for password storage — these are designed for speed (file checksums), and an attacker with a GPU can brute-force billions per second, turning a 8-character password space into minutes.
  • ⚠Using AES in ECB mode — ECB encrypts each block independently, so identical plaintext blocks produce identical ciphertext blocks, leaking the structure of the plaintext (the ECB penguin image is the classic demonstration).
  • ⚠Reusing an IV/nonce across GCM encryptions with the same key destroys the security guarantee — GCM with a reused nonce is not just weakened, it is catastrophically broken (the key can be recovered).
  • ⚠Using Math.random() for security-sensitive values (session tokens, password reset codes) — Math.random() is a PRNG designed for speed, not unpredictability, and its output can be predicted.

Best Practices

  • ✓For passwords, always use bcrypt (cost ≥ 12), argon2id, or scrypt — these are deliberately slow and memory-hard, making GPU brute-force infeasible, and they encode the salt and parameters with the hash.
  • ✓For symmetric encryption, always use AES-256-GCM (or libsodium's secretbox) — authenticated encryption detects tampering, and GCM's unique-IV requirement prevents the catastrophic reuse attack.
  • ✓For random values, always use the CSPRNG (crypto.randomBytes, secrets module) — the cost of using Math.random() for a session token is a predictable token and a compromised session.
  • ✓Never implement cryptographic primitives yourself — use libsodium, Tink, or the language's standard crypto library, and use their high-level APIs (seal/box, sign) not the raw primitives.