Day 3 of 5
⏱ ~60 minutes
Cryptography in 5 Days — Day 3

Cryptographic Hashing

Hash functions take arbitrary input and produce a fixed-size digest. They are one-way, deterministic, and collision-resistant. Today covers SHA-2 and SHA-3, HMAC, password hashing, and Merkle trees.

SHA-2 and SHA-3

SHA-256 produces a 256-bit digest. Any change to the input produces a completely different output (avalanche effect). SHA-256 powers Bitcoin mining, Git commit IDs, TLS certificates, and software integrity verification. SHA-3 (Keccak) uses a fundamentally different construction (sponge function) and provides an alternative if SHA-2 is ever weakened. MD5 and SHA-1 are broken — never use them for security.

HMAC: Hash-Based Message Authentication

HMAC combines a hash function with a secret key: HMAC(K, M) = H((K XOR opad) || H((K XOR ipad) || M)). This produces a MAC — a value that proves both the message integrity and that the sender knew the key. HMAC-SHA256 is used in JWTs, API request signing (AWS SigV4), TLS record authentication, and cookie signing. Verify HMAC before decrypting to avoid padding oracle attacks.

Password Hashing: bcrypt, Argon2, scrypt

Never hash passwords with SHA-256 — it is too fast. Attackers with GPUs can test 10 billion SHA-256 hashes/second. Password hashing functions are deliberately slow and memory-hard. bcrypt has been standard since 1999. Argon2 (NIST recommended, PHC winner) is the current best choice. Use a work factor that takes 100-300ms on your hardware. Always use a unique random salt per password.

python
import hashlib, hmac, os
from argon2 import PasswordHasher

# SHA-256 hash
message = b'Precision AI Academy'
digest = hashlib.sha256(message).hexdigest()
print(f'SHA-256: {digest}')

# HMAC-SHA256 (message authentication)
key = os.urandom(32)
tag = hmac.new(key, message, hashlib.sha256).hexdigest()
print(f'HMAC: {tag}')

# Constant-time comparison (prevents timing attacks)
def verify_hmac(key, message, tag):
    expected = hmac.new(key, message, hashlib.sha256).digest()
    return hmac.compare_digest(expected, bytes.fromhex(tag))

# Argon2 password hashing
ph = PasswordHasher(time_cost=2, memory_cost=65536, parallelism=2)
hash_val = ph.hash('user_password_here')
print(f'Argon2 hash: {hash_val[:50]}...')

# Verify password
try:
    ph.verify(hash_val, 'user_password_here')
    print('Password correct')
except:
    print('Password incorrect')
💡
Use hmac.compare_digest() instead of == when comparing HMACs. Direct equality comparison leaks timing information that attackers can exploit to forge authentication tokens.
📝 Day 3 Exercise
Build a Secure Login System
  1. Install argon2-cffi: pip install argon2-cffi
  2. Write a user registration function that hashes passwords with Argon2
  3. Store hash results in a SQLite database (never the plaintext password)
  4. Write a login function that verifies the password using constant-time comparison
  5. Test it: register a user, log in successfully, then verify a wrong password fails

Day 3 Summary

  • SHA-256 produces a deterministic, collision-resistant 256-bit digest
  • MD5 and SHA-1 are cryptographically broken — never use for security
  • HMAC adds a secret key to a hash for message authentication
  • Use hmac.compare_digest() for constant-time MAC comparison
  • Argon2 is the current best practice for password hashing
Challenge

Implement a file integrity monitoring system that hashes all files in a directory, saves the hashes to a signed manifest (HMAC-SHA256), and detects any changes when run again.

Finished this lesson?