A cryptographic hash is a one-way fingerprint of data. Learn what SHA-256 guarantees, what it does not, how to verify integrity, and why HMAC — not a bare hash — proves a message is authentic.
A cryptographic hash function maps any input to a fixed-size digest, with three properties: the same input always yields the same digest, you cannot reverse the digest back to the input (one-way), and you cannot feasibly find two inputs with the same digest (collision-resistant). This makes a hash a fingerprint for data — used for integrity checks, deduplication, and as a building block in signatures and blockchains.
SHA-256("hello") -> 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
SHA-256("hellp") -> d9014c4624844aa5bac314773d6b689... (totally different)
Properties that make it useful:
deterministic same input -> same digest, always
one-way digest -> input is infeasible (not encryption!)
collision-resistant can't find two inputs with the same digest
avalanche 1-bit change flips ~half the output bits
A hash is NOT encryption (no key, can't be reversed) and NOT a
password store on its own (next lesson). Use SHA-256/SHA-3; never MD5
or SHA-1 (both broken for security use).The everyday use of hashing is integrity: publish the hash of a file, and anyone can recompute it to confirm the bytes were not altered in transit or storage. This is how you verify a downloaded binary matches what the vendor built — the same principle behind image digests and Git commit IDs.
# publish + verify a file's digest
sha256sum installer.bin
# 9f2b...c1 installer.bin <- compare to the value the vendor published
# verify against a published checksum file
sha256sum -c SHA256SUMS # OK / FAILED per file
# in code (Python)
import hashlib
digest = hashlib.sha256(open("installer.bin","rb").read()).hexdigest()
# a single flipped bit -> a completely different digest -> tamper detected.Integrity ("unchanged") is not authenticity ("came from who I think"). Anyone can recompute a plain hash, so a bare hash appended to a message proves nothing about its origin — an attacker who changes the message just recomputes the hash. HMAC mixes a secret key into the hash, so only someone holding the key can produce a valid tag. Use HMAC to verify webhooks, API messages, and tokens.
import hmac, hashlib
key = b"shared-secret-known-only-to-both-parties"
msg = b"amount=100&to=alice"
# sender computes a tag; receiver recomputes and compares
tag = hmac.new(key, msg, hashlib.sha256).hexdigest()
def verify(msg, tag):
expected = hmac.new(key, msg, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, tag) # constant-time compare!
# Without the key an attacker can change msg but CANNOT forge the tag.
# This is exactly how you validate a GitHub/Stripe webhook signature.A subtle but real bug: comparing secrets (tags, tokens) with a normal equality check leaks information through timing — it returns faster when the first bytes differ, letting an attacker recover the value byte by byte. Always compare secret values with a constant-time function. Small detail, real vulnerability class.
Normal == bails out at the first differing byte -> timing side channel:
an attacker measures response time to guess the secret one byte at a time.
Right: hmac.compare_digest(a, b) # takes the same time regardless
Wrong: a == b # for secrets/tags/tokens
The theme of this whole course: the algorithms are strong; the BUGS
are in how we use them. Use vetted primitives, and the safe function.