Cryptography Basics Cheat Sheet
Introduces symmetric and asymmetric encryption, hashing, and digital signatures with practical examples of correct algorithm usage.
Core Concepts
Foundational cryptographic terms and their purpose.
- Symmetric encryption- Same key encrypts and decrypts (e.g. AES); fast, used for bulk data
- Asymmetric encryption- Public key encrypts, private key decrypts (e.g. RSA, ECC); used for key exchange/signing
- Hashing- One-way function producing a fixed-size digest (e.g. SHA-256); used for integrity, not encryption
- Digital signature- Private key signs a hash of data; anyone with the public key can verify authenticity/integrity
- Salt- Random value added to input before hashing to prevent rainbow-table attacks
- HMAC- Keyed-hash message authentication code, verifies integrity and authenticity together
Password Hashing (Python)
Correct way to hash and verify passwords using a slow, salted algorithm.
import bcrypt# Hash a password (bcrypt generates and stores the salt automatically)hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())# Verify a password against a stored hashif bcrypt.checkpw(password.encode(), hashed): print("Password matches")
Symmetric Encryption (Python, AES-GCM)
Authenticated encryption using AES in GCM mode.
from cryptography.hazmat.primitives.ciphers.aead import AESGCMimport oskey = AESGCM.generate_key(bit_length=256)aesgcm = AESGCM(key)nonce = os.urandom(12) # Never reuse a nonce with the same keyciphertext = aesgcm.encrypt(nonce, b"secret data", associated_data=None)plaintext = aesgcm.decrypt(nonce, ciphertext, associated_data=None)
Algorithm Selection Guidance
Which algorithms to prefer or avoid as of current best practice.
- Use- AES-256-GCM, ChaCha20-Poly1305, SHA-256/SHA-3, RSA-2048+ or ECC (P-256/Curve25519), bcrypt/Argon2 for passwords
- Avoid- MD5, SHA-1 (for security purposes), DES/3DES, RC4, ECB mode encryption
- Never roll your own- Use vetted libraries (OpenSSL, libsodium, language crypto standard libs) instead of custom crypto
AEAD with Associated Data (ChaCha20-Poly1305)
Binds ciphertext to context (like a record header) so it can't be replayed or spliced into a different message, without encrypting the context itself.
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305import oskey = ChaCha20Poly1305.generate_key()chacha = ChaCha20Poly1305(key)nonce = os.urandom(12)# associated_data is authenticated but NOT encrypted (e.g. a header/version byte)aad = b"v1|user:1234"ct = chacha.encrypt(nonce, b"transfer $500", aad)# Decryption fails with InvalidTag if ciphertext, nonce, or aad were tampered withpt = chacha.decrypt(nonce, ct, aad)
Key Derivation with HKDF
Derives multiple independent-looking subkeys from a single high-entropy master secret, standard practice after a key exchange.
from cryptography.hazmat.primitives.kdf.hkdf import HKDFfrom cryptography.hazmat.primitives import hashesdef derive_key(shared_secret: bytes, info: bytes, length: int = 32) -> bytes: return HKDF( algorithm=hashes.SHA256(), length=length, salt=None, # optional; a random salt strengthens weak-entropy inputs info=info, # context string binds the key to its purpose ).derive(shared_secret)encryption_key = derive_key(shared_secret, b"app-v1-encryption")mac_key = derive_key(shared_secret, b"app-v1-mac")# HKDF, not the raw ECDH output, is what should ever reach a cipher
Argon2id (Modern Password Hashing)
The current OWASP-recommended default over bcrypt, memory-hard to resist GPU/ASIC cracking at scale.
from argon2 import PasswordHasherfrom argon2.exceptions import VerifyMismatchError# time_cost, memory_cost (KiB), parallelism tuned to ~250-500ms per hash on target hardwareph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4)hashed = ph.hash(password)try: ph.verify(hashed, password) if ph.check_needs_rehash(hashed): hashed = ph.hash(password) # upgrade older hashes transparentlyexcept VerifyMismatchError: raise AuthenticationError("invalid credentials")
Constant-Time Comparison and Timing Attacks
A naive == comparison on secrets leaks timing information proportional to the number of matching leading bytes.
import hmacimport hashlib# UNSAFE: short-circuits on first mismatching byte, leaking timing signal# if computed_mac == submitted_mac: ...# SAFE: constant-time comparison regardless of where bytes differif hmac.compare_digest(computed_mac, submitted_mac): accept()# Same principle applies to API key / token verification, not just MACsdef verify_api_key(stored_hash: bytes, candidate: str) -> bool: candidate_hash = hashlib.sha256(candidate.encode()).digest() return hmac.compare_digest(stored_hash, candidate_hash)
Common Cryptographic Pitfalls
Mistakes that silently break confidentiality or integrity even when using "correct" algorithms.
- Nonce reuse under one key- Reusing a GCM/ChaCha20 nonce with the same key breaks confidentiality and can leak the authentication key entirely
- ECB mode- Encrypts identical plaintext blocks to identical ciphertext blocks, leaking patterns (the classic 'ECB penguin')
- Encrypt-then-MAC vs MAC-then-encrypt- Always encrypt-then-MAC (or use AEAD) to avoid padding-oracle and chosen-ciphertext vulnerabilities
- IV predictability (CBC)- A predictable IV in CBC mode enables chosen-plaintext attacks like BEAST; must be unpredictable, not just unique
- Key/nonce confusion- Deriving a nonce from user input instead of a counter/random source risks accidental reuse
- Weak randomness- Using non-CSPRNG sources (random.random(), time-seeded RNGs) for keys/nonces/IVs undermines every guarantee above
- Padding oracle- Distinguishable error messages on bad padding (CBC + PKCS7) let attackers decrypt ciphertext byte-by-byte
Never encrypt passwords for storage — hash them with a slow, salted algorithm like bcrypt or Argon2; encryption is reversible by design, which is exactly what you don't want for credential storage.