blogs
Cryptography Fundamentals Every Security Engineer Should Know
The cryptographic primitives behind TLS, password hashing, and digital signatures -- what they do, when to use them, and how to avoid misusing them.
Cryptography Fundamentals Every Security Engineer Should Know
Cryptography is the mathematical foundation of information security. Every TLS connection, every encrypted database field, every password hash, every digital signature relies on cryptographic primitives. You don't need to be a mathematician to work in security, but you do need to understand what these tools do, when to use them, and — crucially — how to avoid misusing them.
Symmetric Encryption: AES
AES (Advanced Encryption Standard) is the dominant symmetric cipher — both parties share the same key for encryption and decryption.
Key sizes: AES-128, AES-192, AES-256. The number indicates the key length in bits. AES-256 is considered computationally infeasible to brute-force with any foreseeable technology.
Modes of operation matter enormously. The cipher alone isn't enough — you need to decide how blocks of data are chained together:
-
ECB (Electronic Codebook): Each block encrypted independently. Never use this. Identical plaintext blocks produce identical ciphertext blocks, leaking structural information. Famous example: ECB-encrypted images are still visually recognizable.
-
CBC (Cipher Block Chaining): Each block XORed with the previous ciphertext block before encryption. Requires a random IV (Initialization Vector). Good choice for data at rest when implemented correctly. IV must be random and unique per encryption operation.
-
GCM (Galois/Counter Mode): Authenticated encryption — provides both confidentiality and integrity/authentication in a single operation. The preferred mode for modern applications. Produces a ciphertext AND an authentication tag.
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
import base64
def encrypt_aes_gcm(plaintext: bytes, key: bytes) -> dict:
"""Encrypt using AES-256-GCM (authenticated encryption)"""
nonce = get_random_bytes(16) # Random nonce per encryption
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
ciphertext, tag = cipher.encrypt_and_digest(plaintext)
return {
'ciphertext': base64.b64encode(ciphertext).decode(),
'nonce': base64.b64encode(nonce).decode(),
'tag': base64.b64encode(tag).decode()
}
def decrypt_aes_gcm(encrypted: dict, key: bytes) -> bytes:
"""Decrypt and verify authentication tag"""
nonce = base64.b64decode(encrypted['nonce'])
ciphertext = base64.b64decode(encrypted['ciphertext'])
tag = base64.b64decode(encrypted['tag'])
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
return cipher.decrypt_and_verify(ciphertext, tag) # Raises exception if tampered
Asymmetric Encryption: RSA and Elliptic Curves
Asymmetric cryptography uses a key pair: a public key (shareable with anyone) and a private key (kept secret). Data encrypted with the public key can only be decrypted with the private key.
RSA: The classic asymmetric algorithm. Security relies on the difficulty of factoring large numbers. Minimum 2048-bit keys today; 4096-bit for long-term security. RSA is primarily used for key exchange and digital signatures — it's too slow for bulk data encryption.
Elliptic Curve Cryptography (ECC): Achieves equivalent security with much smaller keys. A 256-bit ECC key provides roughly the same security as a 3072-bit RSA key. Preferred for modern systems due to performance advantages.
ECDSA (Elliptic Curve Digital Signature Algorithm) is used to sign data — proving both authenticity (who sent it) and integrity (it wasn't modified).
A key insight: you should almost never use asymmetric encryption to encrypt bulk data directly. RSA encrypting a large file directly is slow and has size limitations. Instead, use hybrid encryption: encrypt the data with AES (fast), then encrypt the AES key with RSA (asymmetric). TLS does exactly this.
Hashing: One-Way Functions
A hash function takes arbitrary input and produces a fixed-length output (digest). Good hash functions are:
- Deterministic: Same input always produces same output
- One-way: Cannot recover input from output
- Collision-resistant: Hard to find two inputs producing the same output
- Avalanche: Small input change → dramatically different output
SHA-256: Part of the SHA-2 family. 256-bit output. Cryptographically secure and widely used for data integrity verification, digital signatures, and certificate fingerprints.
SHA-3: The newest SHA standard, based on a completely different internal structure (Keccak sponge construction). Not more widely deployed than SHA-256 but a good alternative.
MD5 and SHA-1: Broken. Do not use for security purposes. Both have known collision vulnerabilities — two different inputs can produce the same hash. Still commonly seen in legacy systems; when you encounter them, they should be flagged for replacement.
Password Hashing: Not the Same as Data Hashing
Here's a critical distinction that's often misunderstood: you should NOT use SHA-256 to hash passwords. SHA-256 is designed to be fast — exactly what you don't want for password hashing. A GPU can compute billions of SHA-256 hashes per second, enabling rapid brute-force attacks.
Password hashing algorithms are intentionally slow and memory-intensive:
bcrypt: Adaptive — you can increase the cost factor as hardware gets faster. The de facto standard for password hashing for many years.
Argon2: The winner of the Password Hashing Competition (2015). Configurable memory hardness, parallelism, and iteration count. Recommended for new systems.
scrypt: Memory-hard, predates Argon2. Used by many cryptocurrency systems.
import bcrypt
import argon2
# bcrypt
password = b"user_password_here"
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
is_valid = bcrypt.checkpw(password, hashed)
# Argon2 (preferred for new systems)
ph = argon2.PasswordHasher(time_cost=2, memory_cost=65536, parallelism=2)
hashed = ph.hash("user_password_here")
is_valid = ph.verify(hashed, "user_password_here")
Key Derivation Functions (KDF)
KDFs derive cryptographic keys from a source of entropy (often a password or a master key). I worked with HKDF extensively in my eCTF firmware work:
HKDF (HMAC-based Key Derivation Function): Two-phase process — extract (produce a uniform pseudorandom key from input) and expand (derive purpose-specific keys). This is how you safely generate multiple distinct keys from a single master secret.
PBKDF2: Password-Based Key Derivation Function. Takes a password, a salt, and an iteration count. Used in WPA2 Wi-Fi security and many encryption tools.
The Golden Rules
After all of this, here are the rules that matter most:
-
Don't implement your own cryptographic algorithms. Use well-vetted libraries (libsodium, OpenSSL, Python's cryptography package).
-
Don't use deprecated algorithms. No MD5, no SHA-1, no DES, no RC4.
-
Use authenticated encryption. AES-GCM over AES-CBC for new implementations.
-
Random values must be cryptographically random. Use
os.urandom(),secrets, or equivalent — notrandom.random(). -
Never hardcode keys. Keys in source code end up in version control, logs, and error messages.
-
Separate keys by purpose. Don't use the same key for encryption and authentication.
-
Plan for key rotation. Cryptographic keys have lifetimes. Build your system to support rotation without service disruption.
Cryptography is a tool. Like any tool, its effectiveness depends on using it correctly. The library will compute AES-GCM correctly; it's your job to use the right mode, generate random IVs, and store keys securely.
