SHA-256 and Hashing Explained: What Makes Data Tamper-Proof
Hashing is not encryption — and confusing the two causes serious security bugs. Learn how SHA-256, MD5, and bcrypt work, when to use each, and why you should never store plain-text passwords.
Hashing vs Encryption: The Critical Difference
This confusion causes real security vulnerabilities. Understanding the distinction is non-negotiable for any developer working with user data.
- —One-way — cannot be reversed
- —Same input always produces same output
- —Fixed-length output regardless of input size
- —Designed to detect tampering, not protect secrets
Use for: Passwords, file integrity, digital signatures, MACs
- —Two-way — can be decrypted with the right key
- —Requires a key for both operations
- —Output size related to input size
- —Designed to keep data secret, not verify integrity
Use for: Storing sensitive data you need to retrieve (e.g. API keys, PII)
How SHA-256 Works
SHA-256 is part of the SHA-2 family, designed by the NSA and published by NIST in 2001. It takes any input and produces a fixed 256-bit (32-byte) output — always exactly 64 hexadecimal characters.
SHA-256(“hello”) =
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
SHA-256(“hello!”) =
ce06092fb948d9af675054ec2c3064ccfb4b41bc3d001cd4a61b4f5db31ce6f5
↑ One character change → completely different hash (avalanche effect)
The key properties that make SHA-256 useful are:
- Deterministic — same input always gives the same hash
- Avalanche effect — a single bit change cascades to a completely different output
- Pre-image resistance — given a hash, you cannot find the original input
- Collision resistance — it is computationally infeasible to find two different inputs with the same hash
Algorithm Comparison
| Algorithm | Output | Speed | Status | Use case |
|---|---|---|---|---|
| MD5 | 128 bits | Very fast | Broken | File checksums (non-security), legacy systems |
| SHA-1 | 160 bits | Fast | Broken | Git commit IDs (collision-resistant enough for that), legacy TLS |
| SHA-256 | 256 bits | Fast | Safe | Digital signatures, HMAC, certificate fingerprints, data integrity |
| SHA-512 | 512 bits | Fast on 64-bit | Safe | When extra margin is needed; slightly larger output |
| bcrypt | variable bits | Intentionally slow | Safe | Password storage — adjustable cost factor |
| Argon2id | variable bits | Intentionally slow | Safe | Password storage — winner of the Password Hashing Competition, preferred over bcrypt |
Why You Must Not Use SHA-256 for Passwords
SHA-256 is too fast for passwords. A modern GPU can compute billions of SHA-256 hashes per second, making brute-force attacks trivial for any password under 12 characters.
Password hashing algorithms like bcrypt and Argon2id are intentionally slow and memory-intensive. The cost factor lets you tune how long each hash takes — typically 100–300ms on your server. That is imperceptible to users but makes offline brute-force attacks take years instead of minutes.
// Node.js — never do this for passwords:
const hash = crypto.createHash('sha256').update(password).digest('hex'); // ❌
// Use bcrypt instead:
import bcrypt from 'bcrypt';
const hash = await bcrypt.hash(password, 12); // cost factor 12 → ~250ms ✅
const valid = await bcrypt.compare(inputPassword, hash);
// Or Argon2id (preferred):
import argon2 from 'argon2';
const hash = await argon2.hash(password, { type: argon2.argon2id }); // ✅
const valid = await argon2.verify(hash, inputPassword);HMAC: Hashing with a Secret Key
A plain hash has no secret — anyone can compute SHA-256(data) if they know the data. HMAC (Hash-based Message Authentication Code) combines the hash function with a secret key, producing a tag that proves both the data's integrity and that the signer held the key.
HMAC-SHA256 is used in JWT signatures (HS256), webhook signature verification (GitHub, Stripe, etc.), and API request signing (AWS Signature v4).
import crypto from 'crypto';
// Sign a webhook payload
const signature = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET!)
.update(rawBody)
.digest('hex');
// Verify — use timingSafeEqual to prevent timing attacks
const expected = Buffer.from(signature);
const received = Buffer.from(req.headers['x-signature'] as string, 'hex');
const valid = crypto.timingSafeEqual(expected, received);Generate and verify hashes instantly
Compute MD5, SHA-1, SHA-256, SHA-512, and more — directly in the browser with no data sent to any server.
Open Hash Generator →