Cryptography Best Practices
Risk Severity
๐ด Critical Fix Effort
๐ง Medium Est. Time
โฑ๏ธ 4-8 hours Reference
A02:2021 CWE-327, CWE-326
Using weak or deprecated cryptographic algorithms, poor key management, or predictable random number generators can compromise all your security controls. Always use modern, well-vetted cryptographic libraries.
Don't Roll Your Own Crypto
NEVER implement your own cryptographic algorithms. Even experts make mistakes.
Use established libraries like libsodium, OpenSSL, or language-specific crypto APIs.
๐ Table of Contents
๐ Encryption & Hashing
๐ Key & Secret Management
Deprecated Algorithms (Never Use)
โ Hashing
- โข MD5: Broken (collisions)
- โข SHA1: Deprecated (2017)
- โข MD4: Severely broken
- โข CRC32: Not cryptographic
โ Encryption
- โข DES/3DES: 56-bit keys too small
- โข RC4: Stream cipher broken
- โข ECB mode: Patterns leak
- โข RSA < 2048: Too weak
โ Other
- โข Custom encoding: Not encryption
- โข XOR cipher: Trivially broken
- โข Random() for keys: Predictable
- โข TLS 1.0/1.1: Deprecated
Modern Symmetric Encryption
Python: AES-GCM (Recommended)
python
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.backends import default_backend
import os
# โ VULNERABLE: Weak encryption
import base64
from Crypto.Cipher import DES # DES is broken!
def weak_encrypt(data, key):
cipher = DES.new(key, DES.MODE_ECB) # ECB leaks patterns!
return base64.b64encode(cipher.encrypt(data))
# โ
SECURE: AES-GCM provides encryption + authentication
def secure_encrypt(plaintext: bytes, key: bytes = None) -> tuple:
"""
Encrypt with AES-256-GCM (authenticated encryption)
Returns: (ciphertext, nonce, tag)
"""
if key is None:
key = AESGCM.generate_key(bit_length=256) # 256-bit key
aesgcm = AESGCM(key)
nonce = os.urandom(12) # 96-bit nonce (NEVER reuse!)
# GCM mode provides both encryption and authentication
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data=None)
return ciphertext, nonce, key
def secure_decrypt(ciphertext: bytes, nonce: bytes, key: bytes) -> bytes:
"""Decrypt and verify authentication tag"""
aesgcm = AESGCM(key)
try:
plaintext = aesgcm.decrypt(nonce, ciphertext, associated_data=None)
return plaintext
except Exception:
raise ValueError("Decryption failed - data tampered or wrong key")
# Example usage
plaintext = b"Sensitive data to encrypt"
ciphertext, nonce, key = secure_encrypt(plaintext)
# Store: ciphertext + nonce (key stored separately!)
decrypted = secure_decrypt(ciphertext, nonce, key)
assert decrypted == plaintext
# โ
SECURE: Using associated data (AEAD)
def encrypt_with_metadata(plaintext: bytes, metadata: bytes, key: bytes) -> tuple:
"""
Authenticated encryption with associated data (AEAD)
metadata is authenticated but not encrypted
"""
aesgcm = AESGCM(key)
nonce = os.urandom(12)
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data=metadata)
return ciphertext, nonceNode.js: AES-GCM
javascript
const crypto = require('crypto');
// โ VULNERABLE: Weak encryption
function weakEncrypt(text, password) {
const cipher = crypto.createCipher('des', password); // DES is broken!
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
}
// โ
SECURE: AES-256-GCM with proper key derivation
function secureEncrypt(plaintext, password) {
// Derive key from password using PBKDF2
const salt = crypto.randomBytes(16);
const key = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
// Generate random IV (nonce)
const iv = crypto.randomBytes(12);
// Create cipher
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
// Encrypt
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
// Get authentication tag
const authTag = cipher.getAuthTag();
// Return all components needed for decryption
return {
ciphertext: encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex'),
salt: salt.toString('hex')
};
}
function secureDecrypt(encrypted, password) {
// Derive same key from password
const salt = Buffer.from(encrypted.salt, 'hex');
const key = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
// Create decipher
const iv = Buffer.from(encrypted.iv, 'hex');
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
// Set authentication tag
const authTag = Buffer.from(encrypted.authTag, 'hex');
decipher.setAuthTag(authTag);
// Decrypt
let decrypted = decipher.update(encrypted.ciphertext, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// Example usage
const data = secureEncrypt('Sensitive information', 'strong-password');
const decrypted = secureDecrypt(data, 'strong-password');
// โ
SECURE: Using Web Crypto API (browser)
async function browserEncrypt(plaintext, password) {
const enc = new TextEncoder();
// Derive key from password
const keyMaterial = await crypto.subtle.importKey(
'raw',
enc.encode(password),
'PBKDF2',
false,
['deriveKey']
);
const salt = crypto.getRandomValues(new Uint8Array(16));
const key = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
key,
enc.encode(plaintext)
);
return { ciphertext, iv, salt };
}Password Hashing
Never Store Plaintext Passwords
Always hash passwords with a purpose-built algorithm (Argon2, bcrypt, scrypt).
Never use fast hashes like SHA-256 for passwords!
Python: Argon2 (Best Choice)
python
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
import hashlib
# โ VULNERABLE: Using SHA-256 for passwords
def weak_password_hash(password):
return hashlib.sha256(password.encode()).hexdigest()
# Fast = easy to brute force (billions of hashes/second)
# โ VULNERABLE: No salt
passwords = {}
def store_password(username, password):
passwords[username] = hashlib.sha256(password.encode()).hexdigest()
# Rainbow tables can crack this!
# โ
SECURE: Argon2id (winner of password hashing competition)
ph = PasswordHasher(
time_cost=2, # Number of iterations
memory_cost=65536, # 64 MB of RAM
parallelism=4, # Number of threads
hash_len=32, # Output length
salt_len=16 # Salt length
)
def hash_password(password: str) -> str:
"""Hash password with Argon2id"""
return ph.hash(password)
def verify_password(password: str, hash: str) -> bool:
"""Verify password against hash"""
try:
ph.verify(hash, password)
# Check if rehashing is needed (algorithm updated)
if ph.check_needs_rehash(hash):
return "rehash_needed"
return True
except VerifyMismatchError:
return False
# Example usage
password = "user_password_123"
hash = hash_password(password)
# Output: $argon2id$v=19$m=65536,t=2,p=4$randomsalt$randomhash
# Verify
is_valid = verify_password(password, hash)
assert is_valid == True
# โ
SECURE: bcrypt (also good, older but proven)
import bcrypt
def bcrypt_hash(password: str) -> bytes:
"""Hash with bcrypt (work factor 12)"""
salt = bcrypt.gensalt(rounds=12) # 2^12 iterations
return bcrypt.hashpw(password.encode(), salt)
def bcrypt_verify(password: str, hash: bytes) -> bool:
"""Verify bcrypt hash"""
return bcrypt.checkpw(password.encode(), hash)Java: BCrypt
java
import org.mindrot.jbcrypt.BCrypt;
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;
public class PasswordHashing {
// โ VULNERABLE: Using SHA-256
public static String weakHash(String password) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest(password.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hash);
// No salt, fast = vulnerable to rainbow tables
}
// โ
SECURE: BCrypt with salt
public static String hashPassword(String password) {
// Automatically generates salt and uses 2^10 rounds
return BCrypt.hashpw(password, BCrypt.gensalt(12));
// Output: $2a$12$randomsalt$randomhash
}
public static boolean verifyPassword(String password, String hash) {
try {
return BCrypt.checkpw(password, hash);
} catch (Exception e) {
return false;
}
}
// Example usage
public static void main(String[] args) {
String password = "user_password_123";
// Hash password
String hash = hashPassword(password);
System.out.println("Hash: " + hash);
// Verify password
boolean isValid = verifyPassword(password, hash);
System.out.println("Valid: " + isValid);
// Wrong password
boolean isInvalid = verifyPassword("wrong", hash);
System.out.println("Invalid: " + isInvalid);
}
}Secure Key Management
Key Storage Rules
- โ Use environment variables or secrets managers (AWS KMS, Azure Key Vault, HashiCorp Vault)
- โ Never commit keys to git - add to .gitignore
- โ Rotate keys regularly (every 90 days)
- โ Use different keys per environment (dev, staging, prod)
- โ NEVER hardcode keys in source
- โ NEVER derive keys from passwords directly (use PBKDF2/Argon2)
Environment Variables (Basic)
bash
# .env file (NEVER commit to git!)
ENCRYPTION_KEY=hex:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
DATABASE_ENCRYPTION_KEY=base64:YW5vdGhlcnNlY3VyZWtleWhlcmU=
# Python: Load from environment
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def get_encryption_key():
key_hex = os.environ.get('ENCRYPTION_KEY')
if not key_hex:
raise ValueError("ENCRYPTION_KEY not set")
# Remove 'hex:' prefix and decode
key_hex = key_hex.replace('hex:', '')
return bytes.fromhex(key_hex)
# Node.js: Load from environment
require('dotenv').config();
function getEncryptionKey() {
const keyHex = process.env.ENCRYPTION_KEY;
if (!keyHex) {
throw new Error('ENCRYPTION_KEY not set');
}
return Buffer.from(keyHex.replace('hex:', ''), 'hex');
}AWS KMS (Production)
python
import boto3
import base64
# โ
SECURE: Use AWS KMS for key management
kms = boto3.client('kms', region_name='us-east-1')
def encrypt_with_kms(plaintext: str, key_id: str) -> dict:
"""Encrypt data using AWS KMS"""
response = kms.encrypt(
KeyId=key_id,
Plaintext=plaintext.encode()
)
return {
'ciphertext': base64.b64encode(response['CiphertextBlob']).decode(),
'key_id': response['KeyId']
}
def decrypt_with_kms(ciphertext: str) -> str:
"""Decrypt data using AWS KMS"""
ciphertext_blob = base64.b64decode(ciphertext)
response = kms.decrypt(
CiphertextBlob=ciphertext_blob
)
return response['Plaintext'].decode()
# Example
encrypted = encrypt_with_kms(
"Sensitive data",
key_id="arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"
)
decrypted = decrypt_with_kms(encrypted['ciphertext'])Cryptographically Secure Random Numbers
Never Use Predictable RNGs
Standard random() functions are NOT cryptographically secure. Use secrets or os.urandom().
python
import random
import secrets
import os
# โ VULNERABLE: Predictable random
session_token = ''.join(random.choices('0123456789', k=16))
# Attacker can predict next values if they know seed!
# โ VULNERABLE: Timestamp-based
import time
token = int(time.time()) # Predictable!
# โ
SECURE: Use secrets module (Python 3.6+)
session_token = secrets.token_hex(32) # 32 bytes = 64 hex chars
session_token_url = secrets.token_urlsafe(32) # URL-safe
session_token_bytes = secrets.token_bytes(32) # Raw bytes
# โ
SECURE: Use os.urandom (all Python versions)
token = os.urandom(32) # 32 random bytes
token_hex = token.hex()
# โ
SECURE: Generate secure passwords
import string
alphabet = string.ascii_letters + string.digits + string.punctuation
password = ''.join(secrets.choice(alphabet) for i in range(20))
# โ
SECURE: Generate cryptographic keys
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
key = AESGCM.generate_key(bit_length=256)
# Node.js equivalents
// โ VULNERABLE
const token = Math.random().toString(36);
// โ
SECURE
const crypto = require('crypto');
const token = crypto.randomBytes(32).toString('hex');
const token_base64 = crypto.randomBytes(32).toString('base64');TLS/SSL Configuration
nginx
# โ VULNERABLE: TLS 1.0/1.1, weak ciphers
server {
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ALL:!EXPORT:!LOW:!aNULL:!eNULL:!SSLv2;
}
# โ
SECURE: TLS 1.2+ only, strong ciphers (nginx)
server {
listen 443 ssl http2;
# Use TLS 1.2 and 1.3 only
ssl_protocols TLSv1.2 TLSv1.3;
# Strong cipher suites (Mozilla Modern compatibility)
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;
# Strong DH parameters
ssl_dhparam /etc/nginx/dhparam.pem;
# HSTS (force HTTPS for 1 year)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
# Session cache
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
}
# Python: Enforce TLS 1.2+
import ssl
import urllib.request
# โ
Create secure SSL context
context = ssl.create_default_context()
context.minimum_version = ssl.TLSVersion.TLSv1_2
context.maximum_version = ssl.TLSVersion.TLSv1_3
# Use with requests
import requests
response = requests.get('https://example.com', verify=True)
# Node.js: Enforce TLS 1.2+
const https = require('https');
const tls = require('tls');
const options = {
minVersion: 'TLSv1.2',
maxVersion: 'TLSv1.3',
ciphers: tls.DEFAULT_CIPHERS
};
https.get('https://example.com', options, (res) => {
// Handle response
});๐งช Testing Verification
Test TLS Configuration
bash
# Test SSL/TLS with sslyze
pip install sslyze
sslyze --regular example.com:443
# Test with testssl.sh (comprehensive)
git clone https://github.com/drwetter/testssl.sh
cd testssl.sh
./testssl.sh https://example.com
# Check certificate
openssl s_client -connect example.com:443 -servername example.com
# Online scanners
# https://www.ssllabs.com/ssltest/
# https://observatory.mozilla.org/Detect Weak Crypto in Code
bash
# Semgrep rules for crypto issues
semgrep --config=p/security-audit --config=p/secrets .
# Specific crypto checks
semgrep --config "r/python.cryptography" .
# Bandit (Python security linter)
pip install bandit
bandit -r . -f json -o bandit-report.json
# Checks for:
# - Use of MD5/SHA1
# - Weak random number generators
# - Hardcoded passwords/keys
# - Insecure SSL/TLS configurationsโ ๏ธ Common Mistakes
โ Using ECB Mode
ECB (Electronic Codebook) mode encrypts each block independently, leaking patterns.
python
# โ WRONG - ECB leaks patterns (see "ECB Penguin")
cipher = AES.new(key, AES.MODE_ECB)
# โ
CORRECT - Use GCM, CTR, or CBC with authentication
cipher = AESGCM(key)โ Reusing IVs/Nonces
NEVER reuse an IV/nonce with the same key - it breaks encryption!
python
# โ WRONG - hardcoded IV
iv = b'0' * 16
cipher = AES.new(key, AES.MODE_CBC, iv)
# โ
CORRECT - random IV each time
iv = os.urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv)โ Not Authenticating Ciphertext
Always use authenticated encryption (GCM, ChaCha20-Poly1305) to prevent tampering.
python
# โ WRONG - no authentication (padding oracle attacks!)
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(pad(plaintext, 16))
# โ
CORRECT - authenticated encryption
aesgcm = AESGCM(key)
ciphertext = aesgcm.encrypt(nonce, plaintext, None)