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

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, nonce

Node.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)