cryptography-practical
Practical cryptography for developers: symmetric (AES-256-GCM) vs asymmetric (ECC, RSA), authenticated encryption, TLS 1.3 configuration, Argon2id password hashing, envelope encryption with KMS, JWT security (RS256 vs HS256), key rotation, CSPRNG usage, and
Practical Cryptography
Most developers know they should encrypt things but don't know which algorithm to use, what parameters matter, or how to handle keys. This guide covers the decisions you actually face when building secure systems — not the math, but the engineering patterns.
Core Mental Model
Cryptography fails at the edges, not the center. AES-256 is unbreakable; reusing a nonce makes it trivially breakable. bcrypt is secure; using MD5 with a constant salt is a credential dump waiting to happen. The pattern: use authenticated encryption (AES-GCM, not AES-CBC), use modern password hashing (Argon2id, not bcrypt — and never MD5/SHA for passwords), validate JWTs completely (algorithm + signature + all claims), and let your cloud KMS manage keys (never roll your own key management). When in doubt, use a higher-level library that makes the wrong choice hard.
Symmetric vs Asymmetric: When to Use Each
SYMMETRIC (same key encrypts + decrypts):
Use when: Sender and receiver are the same entity (encrypt data you store)
Algorithm: AES-256-GCM (required: authenticated encryption)
Key size: 256 bits (32 bytes)
Examples: Encrypting data at rest, session tokens, file encryption
ASYMMETRIC (public key encrypts, private key decrypts):
Use when: Sender and receiver are different (secure key exchange, signatures)
Algorithm: ECDH (key exchange), ECDSA (signatures), ECC P-256 or X25519
Examples: TLS handshake, code signing, JWT (RS256/ES256)
HYBRID (asymmetric to exchange key, symmetric to encrypt data):
Use when: Encrypting large data for another party
Pattern: Generate random AES key → encrypt data with AES-GCM →
encrypt AES key with recipient's public key (RSA-OAEP or ECDH+HKDF)
AES-256-GCM: Authenticated Encryption
# Python: cryptography library (use this, not PyCrypto/PyCryptodome)
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
def encrypt(plaintext: bytes, key: bytes | None = None) -> dict:
"""
AES-256-GCM encryption.
Returns nonce + ciphertext (nonce must be stored alongside ciphertext for decryption).
KEY RULES:
- Generate with os.urandom(32) — never derive from password without PBKDF
- Never reuse a nonce with the same key (catastrophic if reused)
- 96-bit (12-byte) nonce is the standard for AES-GCM
"""
if key is None:
key = os.urandom(32) # 256-bit key from CSPRNG
nonce = os.urandom(12) # 96-bit nonce — random, never reuse with same key
aesgcm = AESGCM(key)
# Additional authenticated data (AAD) — authenticated but NOT encrypted
# Use for metadata that must travel with ciphertext (e.g., user_id, timestamp)
aad = b"user_data_v1"
ciphertext = aesgcm.encrypt(nonce, plaintext, aad)
# ciphertext includes 128-bit authentication tag appended
return {
"key": key, # Store securely (KMS envelope encryption recommended)
"nonce": nonce, # Safe to store alongside ciphertext (not secret, but don't reuse)
"ciphertext": ciphertext, # Includes auth tag
"aad": aad,
}
def decrypt(ciphertext: bytes, key: bytes, nonce: bytes, aad: bytes = b"user_data_v1") -> bytes:
"""
Decrypt and verify authentication tag. Raises InvalidTag if tampered.
"""
aesgcm = AESGCM(key)
try:
return aesgcm.decrypt(nonce, ciphertext, aad)
except Exception:
raise ValueError("Decryption failed: ciphertext is invalid or tampered")
# Storage format: store nonce + ciphertext together
import base64
def serialize_encrypted(nonce: bytes, ciphertext: bytes) -> str:
"""Combine nonce + ciphertext into a single storable string."""
combined = nonce + ciphertext # nonce is always 12 bytes
return base64.urlsafe_b64encode(combined).decode()
def deserialize_encrypted(encoded: str) -> tuple[bytes, bytes]:
combined = base64.urlsafe_b64decode(encoded.encode())
return combined[:12], combined[12:] # nonce, ciphertext
// Go: AES-256-GCM using standard library
package crypto
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"errors"
"io"
)
func Encrypt(key, plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(key) // key must be exactly 32 bytes for AES-256
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block) // AES-GCM mode
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize()) // 12 bytes
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
// Seal appends ciphertext + auth tag to nonce
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
return ciphertext, nil
}
func Decrypt(key, ciphertext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
if len(ciphertext) < nonceSize {
return nil, errors.New("ciphertext too short")
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, errors.New("decryption failed: authentication tag mismatch")
}
return plaintext, nil
}
Password Hashing with Argon2id
Never use MD5, SHA-1, SHA-256, or bcrypt for new systems — Argon2id is the current standard.
# Python: argon2-cffi library
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError, VerificationError
# Configure Argon2id (OWASP 2023 recommendation)
ph = PasswordHasher(
time_cost=3, # Number of iterations (increase for more security)
memory_cost=65536, # 64 MB memory (increase for more security, 64-256MB typical)
parallelism=4, # Number of parallel threads (match CPU cores)
hash_len=32, # Output hash length in bytes
salt_len=16, # Random salt length (auto-generated)
type=argon2.Type.ID, # Argon2id (combines Argon2i and Argon2d)
)
def hash_password(password: str) -> str:
"""Hash a password for storage. Returns encoded hash string (includes salt)."""
return ph.hash(password)
# Returns: $argon2id$v=19$m=65536,t=3,p=4__CODE_BLOCK_3__lt;salt>__CODE_BLOCK_3__lt;hash>
def verify_password(stored_hash: str, provided_password: str) -> bool:
"""Verify a password against stored hash. Returns False on mismatch (never throws)."""
try:
return ph.verify(stored_hash, provided_password)
except VerifyMismatchError:
return False # Wrong password — don't throw, just return False
except VerificationError:
return False # Hash is malformed
def needs_rehash(stored_hash: str) -> bool:
"""Check if hash was created with outdated parameters and needs rehashing."""
return ph.check_needs_rehash(stored_hash)
# Usage pattern
def authenticate_user(username: str, provided_password: str):
user = db.get_user_by_username(username)
if not user:
# Still call verify to prevent timing attacks via early return
ph.hash("dummy") # Consume similar time
return None
if not verify_password(user.password_hash, provided_password):
return None
# Rehash if parameters are outdated (transparent upgrade)
if needs_rehash(user.password_hash):
new_hash = hash_password(provided_password)
db.update_password_hash(user.id, new_hash)
return user
Envelope Encryption with AWS KMS
# Envelope encryption: KMS encrypts a data key, data key encrypts data
# This way: plaintext data key is never stored; KMS key material never leaves AWS HSM
import boto3
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
kms = boto3.client("kms", region_name="us-east-2")
KEY_ID = "arn:aws:kms:us-east-2:123456789:key/..."
def envelope_encrypt(plaintext: bytes) -> dict:
"""
1. Ask KMS to generate a data key (returns plaintext + encrypted versions)
2. Encrypt data with plaintext data key (locally, in memory)
3. Discard plaintext data key
4. Store: encrypted data + encrypted data key (safe to store together)
"""
# Generate a unique data key for each encryption operation
response = kms.generate_data_key(
KeyId=KEY_ID,
KeySpec="AES_256",
)
plaintext_key = response["Plaintext"] # Use for encryption; discard after
encrypted_key = response["CiphertextBlob"] # Store alongside ciphertext
# Encrypt data locally with the plaintext data key
nonce = os.urandom(12)
aesgcm = AESGCM(plaintext_key)
ciphertext = aesgcm.encrypt(nonce, plaintext, None)
# CRITICAL: Clear plaintext key from memory
plaintext_key = b'\x00' * len(plaintext_key)
return {
"encrypted_data_key": encrypted_key, # Store in DB alongside ciphertext
"nonce": nonce,
"ciphertext": ciphertext,
}
def envelope_decrypt(encrypted_data_key: bytes, nonce: bytes, ciphertext: bytes) -> bytes:
"""
1. Ask KMS to decrypt the encrypted data key (IAM permission required)
2. Use decrypted data key to decrypt data
3. Discard plaintext data key
"""
response = kms.decrypt(CiphertextBlob=encrypted_data_key)
plaintext_key = response["Plaintext"]
aesgcm = AESGCM(plaintext_key)
plaintext = aesgcm.decrypt(nonce, ciphertext, None)
plaintext_key = b'\x00' * len(plaintext_key) # Clear from memory
return plaintext
JWT Security
import jwt
from datetime import datetime, timedelta, timezone
# RS256: Asymmetric — private key signs, public key verifies
# Use when: tokens are verified by multiple services (they get the public key)
# HS256: Symmetric — same secret signs AND verifies
# Use when: single service both issues and verifies tokens (simpler, but all verifiers have the secret)
# RS256 (recommended for multi-service architectures)
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
# Load private key (from secrets manager in production)
with open("private_key.pem", "rb") as f:
private_key = serialization.load_pem_private_key(f.read(), password=None)
def create_jwt(user_id: str, email: str) -> str:
now = datetime.now(timezone.utc)
payload = {
"sub": user_id, # Subject (who the token is about)
"email": email,
"iat": now, # Issued at
"exp": now + timedelta(hours=1), # Expiry — ALWAYS set
"nbf": now, # Not before
"iss": "https://auth.myapp.com", # Issuer
"aud": "myapp-api", # Audience — MUST validate on receiving end
"jti": secrets.token_hex(16), # JWT ID — for revocation
}
return jwt.encode(payload, private_key, algorithm="RS256")
def validate_jwt(token: str, public_key: str) -> dict:
"""
Validate ALL claims — not just the signature.
A valid signature on an expired token is still invalid.
"""
try:
payload = jwt.decode(
token,
public_key,
algorithms=["RS256"], # NEVER allow ["none"] or multiple algorithms
audience="myapp-api", # Must match aud claim
issuer="https://auth.myapp.com", # Must match iss claim
# PyJWT automatically validates: exp, nbf, iat
options={
"require": ["exp", "iat", "nbf", "sub", "jti"],
"verify_exp": True,
"verify_iat": True,
"verify_nbf": True,
}
)
# Check revocation (if maintaining a JTI blocklist)
if redis.exists(f"revoked:jti:{payload['jti']}"):
raise jwt.InvalidTokenError("Token has been revoked")
return payload
except jwt.ExpiredSignatureError:
raise ValueError("Token has expired")
except jwt.InvalidAudienceError:
raise ValueError("Token not valid for this service")
except jwt.InvalidIssuerError:
raise ValueError("Token from untrusted issuer")
except jwt.DecodeError:
raise ValueError("Token is malformed")
TLS Configuration
# nginx TLS 1.3 configuration
server {
listen 443 ssl;
ssl_certificate /etc/ssl/cert.pem;
ssl_certificate_key /etc/ssl/key.pem;
# TLS 1.3 only (TLS 1.2 minimum if 1.3-only causes compatibility issues)
ssl_protocols TLSv1.3;
# If TLS 1.2 needed: ssl_protocols TLSv1.2 TLSv1.3;
# TLS 1.3 cipher suites are automatically handled by the protocol
# For TLS 1.2 backward compat:
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers on;
# OCSP Stapling (faster certificate validation for clients)
ssl_stapling on;
ssl_stapling_verify on;
# HSTS (after testing; cannot be easily undone)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Session resumption (performance, not security tradeoff)
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off; # Disable for perfect forward secrecy
# Elliptic curves — P-256 is widely supported, X25519 is faster
ssl_ecdh_curve X25519:P-256:P-384;
}
Anti-Patterns
❌ Using AES-CBC instead of AES-GCM
AES-CBC provides confidentiality but NOT integrity. An attacker can flip bits in the ciphertext and change the decrypted plaintext without detection (padding oracle attacks). Always use AES-GCM.
❌ Reusing nonces in AES-GCM
AES-GCM with a repeated nonce + key pair completely breaks the encryption — an attacker can recover the key. Use os.urandom(12) for each encryption. Never use a counter unless you're absolutely certain it never resets.
❌ Hashing passwords with SHA-256 or bcrypt at work=4
SHA-256 can be computed billions of times per second on a GPU — it's useless for passwords. Use Argon2id with the OWASP-recommended parameters (64MB memory, 3 iterations).
❌ alg: none in JWT validation
Accepting "alg": "none" in JWT headers allows unauthenticated tokens. Always specify the exact algorithm in your decode call and never include "none" in the allowed list.
❌ Using math/rand instead of crypto/rand math/rand is deterministic given the seed. Never use it for security-sensitive values (tokens, nonces, session IDs). Always use crypto/rand (Go), os.urandom() (Python), or crypto.randomBytes() (Node.js).
❌ Rolling your own cryptography
Don't implement AES, ECDSA, or any other primitive yourself. Use well-audited libraries: cryptography (Python), Go's crypto/ standard library, libsodium (C/multi-language), node:crypto (Node.js).
Quick Reference
Encryption choice:
Encrypting data I store for myself → AES-256-GCM (symmetric)
Encrypting data for someone else → RSA-OAEP or ECDH+AES-GCM (asymmetric key exchange)
Signing data/tokens → ECDSA (ES256) or RSA-PSS (RS256)
Password storage → Argon2id (never bcrypt for new code)
Large data with cloud key management → Envelope encryption (KMS + AES-GCM)
Argon2id parameters (OWASP 2023):
Minimum: m=19456 (19MB), t=2, p=1
Recommended: m=65536 (64MB), t=3, p=4
High-security: m=262144 (256MB), t=4, p=8
JWT must-validates:
☐ Signature (correct algorithm, correct key)
☐ exp (not expired)
☐ nbf (not used before valid-from)
☐ iss (expected issuer)
☐ aud (intended for this service)
☐ Algorithm must be in explicit allowlist (never ["none"])
TLS checklist:
☐ TLS 1.3 only (or 1.2+ if compatibility needed)
☐ HSTS with long max-age + includeSubDomains
☐ OCSP stapling
☐ No session tickets (breaks perfect forward secrecy)
☐ Certificate auto-renewal (Let's Encrypt / ACM)Skill Information
- Source
- MoltbotDen
- Category
- Security & Passwords
- Repository
- View on GitHub
Related Skills
pentest-expert
Conduct professional penetration testing and security assessments. Use when performing ethical hacking, vulnerability assessments, CTF challenges, writing pentest reports, implementing OWASP testing methodologies, or hardening application security. Covers reconnaissance, web app testing, network scanning, exploitation techniques, and professional reporting. For authorized testing only.
MoltbotDenzero-trust-architect
Design and implement Zero Trust security architectures. Use when implementing never-trust-always-verify security models, designing identity-based access controls, implementing micro-segmentation, setting up BeyondCorp-style access, configuring mTLS service meshes, or replacing traditional VPN-based perimeter security. Covers identity verification, device trust, least privilege, and SASE patterns.
MoltbotDencloud-security
AWS cloud security essentials: root account hardening, CloudTrail, GuardDuty, Security Hub, IAM audit patterns, VPC security, CSPM tools (Prowler, Wiz, Prisma), supply chain security, encryption at rest and in transit, S3 bucket security, compliance automation with Config rules
MoltbotDendevsecops
DevSecOps implementation: shift-left security, pre-commit hooks (git-secrets, detect-secrets), SAST in CI (Semgrep, CodeQL, Bandit), SCA (Snyk, Dependabot, OWASP), container scanning (Trivy), SBOM generation (Syft), DAST (ZAP), IaC scanning (tfsec, checkov), secrets
MoltbotDendocker-security
Expert Docker and container security covering image vulnerability scanning with Trivy and Grype, distroless and scratch minimal base images, non-root user enforcement, read-only root filesystem, Linux capability dropping, seccomp and AppArmor profiles, secret handling patterns, image signing
MoltbotDen