SHA-256 shows up everywhere: Git commit IDs, TLS certificates, blockchain, file integrity checks, API signatures. It's one of those primitives you use constantly, often without thinking about it. But when you do need to reach for it directly, a few misconceptions can cost you real security bugs. Here's the working knowledge you need.
What SHA-256 actually is
SHA-256 is a cryptographic hash function from the SHA-2 family. You feed it any input — a string, a file, a byte array — and it produces a fixed 256-bit output, normally shown as 64 hexadecimal characters. Same input, same output, every time. Change a single bit and the whole digest looks completely different.
Three properties make it useful:
- Deterministic: the same input always produces the same hash.
- One-way: you can't practically reverse a digest back into the original input.
- Collision-resistant: it's computationally infeasible to find two different inputs that hash to the same value.
The key thing to internalise: a hash is not encryption. There's no key, and there's no way to "decrypt" it. It's a fingerprint, not a locked box.
When and why you'd reach for it
Integrity checks
If you distribute a file and publish its SHA-256 digest, anyone can hash their downloaded copy and confirm it wasn't corrupted or tampered with in transit. This is why Linux ISOs and package managers ship checksums.
Content addressing and deduplication
Git identifies objects by their hash. Content-addressable storage systems do the same — the hash is the key. If two files hash to the same value, they're the same file, so you store one copy.
Signatures and HMAC
Webhook providers like Stripe and GitHub sign payloads using HMAC-SHA256. You recompute the HMAC with your shared secret and compare it to the header they send. If they match, the request is authentic and unmodified.
Where SHA-256 is the wrong tool
Storing user passwords. This trips people up constantly. SHA-256 is fast, which is exactly what you don't want for passwords — an attacker with a leaked database can try billions of guesses per second on a GPU. Use a purpose-built password hash like bcrypt, scrypt, or Argon2, which are deliberately slow and salted. SHA-256 is for integrity and identity, not credential storage.
A worked example
Say you want to verify a webhook signature. The provider sends the raw body and a signature header. You compute an HMAC-SHA256 and compare. In Node.js:
const crypto = require('crypto');
function verifyWebhook(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
// Constant-time comparison to avoid timing attacks
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(signatureHeader, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}And a plain SHA-256 of a string, which you'll use for integrity checks or content IDs:
const hash = crypto
.createHash('sha256')
.update('hello world')
.digest('hex');
console.log(hash);
// b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9That digest for hello world is fixed — you can reproduce it anywhere. When I'm debugging why two systems disagree on a hash, I want a quick, trustworthy reference to check against my code's output. A browser-based SHA-256 Hash Generator is handy for exactly that: paste the input, compare the digest, and rule out your language runtime as the culprit.
Common gotchas
Encoding mismatches
The single most common source of "but the hashes don't match!" is text encoding. Hashing the UTF-8 bytes of a string is different from hashing UTF-16 or Latin-1 bytes. Trailing newlines, BOM markers, and CRLF vs LF line endings all change the digest. When you compare across systems, agree on the exact byte representation first.
Hashing objects instead of bytes
You can only hash bytes. If you're hashing a JSON object, you're really hashing a serialised string — and JSON serialisation isn't canonical. Key order, whitespace, and number formatting can vary between libraries. Two "equal" objects can serialise differently and produce different hashes. If you need stable hashes of structured data, use a canonical serialisation.
Hex vs Base64 vs raw
The same 256-bit digest can be represented as 64 hex characters, a Base64 string, or raw binary. If one system outputs hex and another outputs Base64, they'll look completely different despite being identical underneath. Always confirm the output format on both ends.
Non-constant-time comparison
When comparing a computed hash against a user-supplied one (like a signature), don't use === or a naive equality check. A standard comparison bails out at the first mismatched byte, and the tiny timing difference can leak information. Use a constant-time comparison — crypto.timingSafeEqual in Node, hmac.compare_digest in Python.
Salting for the wrong reasons
Salts belong to password hashing, not general-purpose SHA-256. Adding a salt to a file checksum accomplishes nothing useful. Understand why you're adding a salt before you do.
Quick reference
- Output size: 256 bits = 32 bytes = 64 hex characters.
- Speed: fast — good for integrity, bad for passwords.
- Reversible: no. It's a one-way fingerprint.
- For passwords: use Argon2, bcrypt, or scrypt instead.
- For authenticity: use HMAC-SHA256 with a shared secret.
SHA-256 is boring in the best way — it's a well-understood, widely trusted building block. Most of the trouble people hit isn't with the algorithm itself but with the plumbing around it: encoding, serialisation, output format, and comparison. Get those right and it'll do exactly what you expect, every single time.
Photo by Chris Ried on Unsplash