Hash functions: one-way fingerprints
A cryptographic hash turns any input into a fixed-size fingerprint, and a short list of precise properties — one-wayness, collision resistance, the avalanche effect — is what makes that fingerprint useful for integrity, content-addressing, and, with real care, passwords.
Hash functions: one-way fingerprints
Feed a hash function a single character, or the entire text of a novel, or a ten-gigabyte video file, and it hands back the same thing every time: a fixed number of bits. SHA-256 always returns 256 bits — 64 hex characters — whether you fed it "a" or a whole Linux kernel source tree. That's already strange enough to pause on: an unbounded amount of information compressed into a fixed-size box. The interesting part isn't the compression, though — plenty of things compress data. It's the specific set of guarantees the box has to satisfy for that fingerprint to be trustworthy, and those guarantees are what this lesson is about.
The frame: a hash maps any input to a fixed-size digest, and it's useful only because of a short list of hard-to-satisfy properties — determinism, one-wayness, collision resistance, and the avalanche effect. Those properties, not the compression itself, are what make hashes the basis of integrity, content-addressing, and (carefully) password storage.
Any size in, fixed size out
In the browser, this is one call:
async function sha256Hex(text) {
const bytes = new TextEncoder().encode(text);
const digest = await crypto.subtle.digest("SHA-256", bytes);
return Array.from(new Uint8Array(digest))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
await sha256Hex("hello world");
// "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde"Notice the output length: always 64 hex characters, 256 bits, no matter what you pass in. That fixed size is the whole point of "digest" — it's a stand-in for the input, small and constant, that you can store, compare, and transmit cheaply instead of the original.
But a fixed-size stand-in is only useful if it behaves in specific ways. A function that just truncated the input to 256 bits would also map anything to a fixed size — and would be useless, because different inputs sharing the same first 256 bits would collide constantly, and you could "invert" it trivially for short inputs. The properties below are what separate a real cryptographic hash from that kind of shortcut.
The properties, precisely
Deterministic. The same input always produces the same digest — every time, on every machine, forever. This sounds obvious, but it's what makes a digest checkable: you can recompute it later and expect an exact match.
Preimage resistance (one-wayness). Given a digest, it's computationally infeasible to find any input that produces it. This is why a hash is called "one-way" — going forward (input → digest) is fast; going backward (digest → input) is not "hard," it's infeasible, requiring an amount of work close to trying inputs at random. There is no "unhash" function, and if a scheme lets you recover the input from the digest without brute-forcing, it isn't a working cryptographic hash.
Second-preimage and collision resistance. Given one input, it should be infeasible to find a different input with the same digest (second-preimage resistance). More broadly, it should be infeasible to find any two different inputs that hash to the same digest (collision resistance) — you're not even given a starting input, just asked to find a pair. Collisions must exist in principle, since infinite inputs map to a finite set of digests, but "exist" and "findable" are different things. A good hash makes them findable only by luck, not by strategy.
The avalanche effect. Change a single bit of the input — flip one letter, add one space — and the digest should change completely: roughly half its output bits flip, in a way that looks unpredictable and unrelated to which bit you changed. There is no "graduated" similarity between hashes of similar inputs. Two inputs differing by one character produce digests that look like two entirely unrelated random strings.
Try it below. It computes a real SHA-256 digest for whatever you type, and shows how a single-character edit reshuffles roughly half the output bits.
comparing "hello" against "" — last character nudged by one code point
A cryptographic hash has the avalanche property: changing even one character — really just one bit — of the input flips roughly half of the output bits, unpredictably. That is why the two 64-character digests above look almost entirely different even though the inputs differ by a single code point. This is what makes a digest a tamper-evident fingerprint: there is no way to nudge an input toward a target hash, because a tiny change in input does not produce a correspondingly tiny change in output.
Why these properties are the whole point
Once you have determinism, one-wayness, collision resistance, and the avalanche effect, several things fall out for free.
Integrity. Publish a file alongside its hash. Anyone who downloads the file can recompute the hash locally and compare it to the published value. If even one bit was corrupted or tampered with in transit, the digest changes completely (avalanche effect) and the mismatch is obvious. This is exactly how software distribution checksums and Git's own integrity model work — a git commit hash is a hash over the commit's content, so a single-byte change anywhere in the history changes every hash downstream of it.
Content-addressing and dedup. If two files hash to the same digest, collision resistance says they're overwhelmingly likely to be identical content. That turns a hash into an address: instead of naming a piece of data by where it's stored, you name it by what it is. Git's object store is built on exactly this — every blob, tree, and commit is stored and looked up by its SHA-1 (historically) or SHA-256 hash. Identical content, anywhere in the repository's history, is stored once, because it hashes to the same address.
Commitments. Publish a hash now, reveal the input later, and anyone can verify you didn't change your answer after the fact — because one-wayness means the published hash didn't leak the input in advance, and collision resistance means you can't later "find" a different input that also matches the hash you committed to. This is the mechanism behind sealed-bid auctions, commit-reveal voting schemes, and provably-fair game mechanics.
The birthday bound: why 256 bits, not 128
Collision resistance has a subtlety that trips people up: it doesn't take anywhere near 2^256 attempts to find a collision in a 256-bit hash. The birthday problem — the same math behind "in a room of 23 people, there's a 50% chance two share a birthday" — means collisions become likely after roughly the square root of the total number of possible digests, not the full count. For an n-bit hash, that's around 2^(n/2) attempts, not 2^n.
That's why SHA-256 targets 256 bits of output for only 128 bits of collision security, not 256. If you need 128-bit security against a birthday attack, you need a digest twice that size. This is a pure counting argument, not a weakness in any particular algorithm — it applies to any hash function, no matter how well designed, and it's precisely why cryptographic hash sizes look "oversized" relative to the security level they claim.
Current standards, and what's broken
The standards in active use today are the SHA-2 family (SHA-256, SHA-512, and variants) and SHA-3, a structurally different design adopted as a backup in case weaknesses were ever found in SHA-2. Both are considered collision-resistant with no practical attacks known.
Two clarifications worth being precise about
A hash is not encryption. Encryption uses a key and is meant to be reversed by whoever holds the right key — that's the entire purpose of decrypting. A hash has no key, and one-wayness means it is not meant to be reversed by anyone, key or no key. There is no "unhash" function waiting to be called with a secret. If you need to recover the original data, you need encryption (lesson 4), not a hash.
A plain hash is also not how you store passwords, even though it feels like the natural fit — "one-way, and I can verify by recomputing" sounds exactly like the property you want. The problem is that SHA-256 is fast, deliberately so, because integrity-checking a large file shouldn't take long. But an attacker with a stolen table of password hashes can compute billions of SHA-256 guesses per second on ordinary hardware, which turns "infeasible to invert" into "feasible to just guess fast enough." Fixing that needs a hash-like function that's deliberately slow — which is exactly where lesson 3 goes next.
Where this goes next
Hash functions give you a one-way fingerprint with precise, useful guarantees — but "one-way" alone doesn't make them safe for the one job they look most suited for: passwords. The next lesson is entirely about why that plain hash fails there, and the two additional ingredients — salting and deliberate slowness — that fix it: Password hashing: salts and slow KDFs.
Go deeper
- MDN — SubtleCrypto.digest() — The browser API used in this lesson's code, with the full list of supported algorithms (SHA-1, SHA-256, SHA-384, SHA-512).
- Crypto 101 — Covers hash construction (Merkle-Damgard, sponge functions) in more depth than this lesson, including why SHA-3 is structurally different from SHA-2.
- NIST — Hash functions project — The standards body's own record of SHA-2, SHA-3, and the retirement timeline for SHA-1 and MD5.
Check yourself
Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.
- What two things stay fixed and what stays variable when you hash different inputs with SHA-256?
- Explain preimage resistance in your own words, and why 'there is no unhash function' is the right way to think about it.
- What's the difference between second-preimage resistance and collision resistance? (Hint: is an input given to you in both cases?)
- Describe the avalanche effect and explain why it defeats an attacker trying to 'nudge' a guess closer to a target digest.
- Why does a 256-bit hash target only 128 bits of collision security rather than 256? Name the mathematical effect responsible.
- Give one concrete reason MD5 and SHA-1 are unsafe for security use today, and one thing they're still fine for.
- Why does a hash's one-wayness make it unsuitable, by itself, for storing passwords — even though 'one-way' sounds like exactly the right property?