Part 3 — Hashing: fingerprints for data
Chapter 8 — What a hash function is
In one sentence
A hash function turns any input into a fixed-size fingerprint, such that the same input always gives the same fingerprint and you can't work backwards.
The idea
ANY input, any size Fixed-size output
------------------ -----------------
"hello" --> SHA-256 --> 2cf24dba5fb0a30e26e83b2ac5b9e29e
1b161e5c1fa7425e73043362938b9824
a 4 GB video file --> SHA-256 --> (a different 64 hex characters)
the empty string --> SHA-256 --> e3b0c44298fc1c149afbf4c8996fb924
27ae41e4649b934ca495991b7852b855
Always 256 bits (64 hex characters) out, regardless of input size.
The properties that make it useful
A cryptographic hash function must have all four:
1. DETERMINISTIC same input -> always the same output
(otherwise it's useless for comparison)
2. ONE-WAY given the hash, you cannot find the input
(preimage resistance) except by guessing
3. AVALANCHE change ONE BIT of input -> ~half the output bits flip
(so similar inputs look completely unrelated)
4. COLLISION RESISTANT it's infeasible to find TWO inputs with the
same output
Seeing the avalanche effect
sha256("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
sha256("hello ") = 5e3235a8346e5a4585f8c58562f5052b8fe26a3bb122e1e96c76784964dfc461
^^ one added space -- completely different output
sha256("Hello") = 185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969
^^ one changed bit (case) -- completely different
This is why hashes detect tampering. Any change, however small, produces an unrecognisably different result.
Collisions must exist (and why that's OK)
Infinite possible inputs, finite outputs (2^256 for SHA-256). So collisions mathematically must exist. The security property is that finding one is infeasible.
The birthday paradox: because of how probability works, finding any collision takes about
2^(n/2)work, not2^n. So SHA-256's 256-bit output gives 128 bits of collision resistance, not 256. This is why "128-bit security" is the target and why SHA-256 (not SHA-128) is the baseline.
What hashes are for
| Use | How |
|---|---|
| Integrity checking | Publish the hash of a download; the user verifies |
| Deduplication | Same hash = same content, cheaply |
| Data structures | Hash tables, Merkle trees, git commits, blockchains |
| Password storage | With critical modifications — see Chapter 10 |
| Digital signatures | Sign the hash, not the (huge) document (Chapter 21) |
| Commitment | Publish a hash now, reveal the data later, proving you didn't change it |
What hashes are NOT for
NOT encryption -- there's no key and no way back. You can't "decrypt a hash".
NOT for passwords -- not plain, anyway. Chapter 10.
NOT for secrecy of -- if the input space is small (a credit card number,
low-entropy data a phone number, a UK postcode), an attacker just
hashes every possibility and looks yours up.
That last one catches people. Hashing an email address does not anonymise it — there are only so many email addresses, and attackers have precomputed them.
Practice (20 min)
cd ~/sec-lab/scratch
# --- basic hashing ---
printf 'hello' | openssl dgst -sha256
printf 'hello' | shasum -a 256 # same thing, different tool
# --- avalanche: change one character ---
for s in "hello" "hellp" "Hello" "hello "; do
printf "%-8s " "$s"; printf "%s" "$s" | openssl dgst -sha256 | awk '{print $2}'
done
# --- deterministic: run it 3 times ---
for i in 1 2 3; do printf 'hello' | openssl dgst -sha256 -r; done
# --- fixed size regardless of input ---
printf 'a' | openssl dgst -sha256 | awk '{print length($2), $2}'
head -c 10000000 /dev/urandom | openssl dgst -sha256 | awk '{print length($2), $2}'
# --- verifying a download (do this for real, always) ---
echo "important data" > file.txt
openssl dgst -sha256 file.txt > file.txt.sha256
cat file.txt.sha256
# now tamper with it:
echo "important data " > file.txt # added ONE space
openssl dgst -sha256 file.txt # compare -- completely different
Break it: hash a 4-digit PIN with SHA-256. Now write four lines of Python that recover the PIN from the hash. How long did it take? That's why Chapter 10 exists.
python3 - <<'PY'
import hashlib
target = hashlib.sha256(b"4271").hexdigest()
print("hash:", target)
for i in range(10000):
if hashlib.sha256(f"{i:04d}".encode()).hexdigest() == target:
print("cracked:", f"{i:04d}"); break
PY
Common confusions
- "Can I decrypt this hash?" No. There is nothing to decrypt. If someone "reverses" a hash they either guessed the input or looked it up in a table of precomputed hashes.
- "Hashing anonymises personal data." Only if the input has high entropy. Hashed emails, phone numbers, and IDs are readily reversible by brute force and are generally still personal data under GDPR.
- "Two files with the same hash are the same file." For a strong hash, practically yes. For MD5 or SHA-1, no — see Chapter 9.
Check yourself
- Name the four properties of a cryptographic hash function.
- Why must collisions exist?
- Why does a 256-bit hash give 128-bit collision resistance?
- Why doesn't hashing protect a credit card number?
Further reading
- Video: Computerphile, "Hashing Algorithms and Security" (~8 min).
- Book: Serious Cryptography, Chapter 6 (Hash Functions).
- Tool:
crackstation.net— paste a hash of a common word and watch it come back instantly. A visceral lesson in why unsalted hashes fail.