Here's a fun fact: every time you log into a well-built application, your actual password is nowhere on that server. Not in a database, not in a log file, nowhere. What's there instead is a hash — a one-way mathematical fingerprint that proves you know the password without ever storing it. I've spent years working with these things, and the number of developers who mix up when to use SHA-256 vs bcrypt still surprises me. This guide is my attempt to clear that up once and for all.
What Is a Hash Function?
A cryptographic hash function takes an input of any size and produces a fixed-size output called a hash, digest, or checksum. Hash functions have four key properties:
- Deterministic: The same input always produces the same hash.
- One-way: It is computationally infeasible to reverse a hash back to the original input.
- Avalanche effect: A tiny change in input produces a completely different hash.
- Collision resistant: It is extremely difficult to find two different inputs that produce the same hash.
Example: SHA-256
Comparing Hash Algorithms
| Algorithm | Output Size | Speed | Security | Status |
|---|---|---|---|---|
| MD5 | 128 bits (32 hex) | Very fast | Broken | ❌ Deprecated |
| SHA-1 | 160 bits (40 hex) | Fast | Weak | ❌ Deprecated |
| SHA-256 | 256 bits (64 hex) | Fast | Strong | ✅ Recommended |
| SHA-512 | 512 bits (128 hex) | Moderate | Very strong | ✅ Recommended |
| SHA-3 | Variable | Moderate | Very strong | ✅ Modern standard |
Which Hash to Use for Each Use Case
File integrity verification (checksums)
→ Use: SHA-256
Fast, widely supported, and secure enough for verifying downloads and file transfers.
Digital signatures and certificates
→ Use: SHA-256 or SHA-384
Required by TLS/SSL certificates. SHA-1 is no longer accepted by browsers.
Data deduplication
→ Use: SHA-256 or SHA-512
Collision resistance ensures two different files won't produce the same hash.
Password storage
→ Use: bcrypt, Argon2, or scrypt
Never use SHA-256 or MD5 for passwords. Use a dedicated password hashing algorithm with salting and work factors.
Non-security checksums (cache keys, ETags)
→ Use: MD5 or SHA-1
For non-security purposes where speed matters and collision resistance is not critical, MD5 is acceptable.
HMAC (message authentication)
→ Use: HMAC-SHA256
Combines a secret key with SHA-256 to verify both integrity and authenticity of a message.
Why MD5 and SHA-1 Are Broken
MD5 and SHA-1 are considered cryptographically broken because researchers have demonstrated practical collision attacks — meaning it is possible to find two different inputs that produce the same hash. In 2017, Google's SHAttered attack produced the first known SHA-1 collision. In 2019, researchers demonstrated chosen-prefix collisions for SHA-1 in under 2 months of cloud computing time.
For any security-sensitive application, use SHA-256 or stronger. MD5 and SHA-1 are only acceptable for non-security purposes like cache keys or legacy checksums.
Practical Hash Function Applications
Hash functions are not just theoretical constructs — they are the invisible infrastructure behind version control, blockchain, API security, and file integrity. Understanding these real-world applications gives you a much deeper appreciation for why hash functions matter.
How Git Uses SHA-1 for Commit Hashes
Every object in a Git repository — commits, trees, blobs (file contents), and tags — is identified by a SHA-1 hash of its content. When you run git log, those 40-character hex strings are SHA-1 hashes. The hash of a commit includes the hash of its parent commit, the author, timestamp, and the tree hash — creating a cryptographic chain where changing any historical commit would change all subsequent commit hashes.
Git is in the process of migrating to SHA-256 (the SHA-256 object format is available in Git 2.29+) because SHA-1 is theoretically vulnerable to collision attacks. However, the practical risk for Git is low because an attacker would need to create a malicious commit with the same hash as a legitimate one — an extremely difficult task even with SHA-1.
How Blockchain Uses SHA-256
Bitcoin and many other blockchains use SHA-256 as their core hashing algorithm. Each block in the chain contains the SHA-256 hash of the previous block, creating an immutable chain. The proof-of-work mining process requires finding a nonce value such that the SHA-256 hash of the block header starts with a certain number of leading zeros — a computationally expensive task that secures the network.
Bitcoin actually applies SHA-256 twice (SHA-256d) for additional security. The double-hashing provides protection against length extension attacks, which are a known weakness of the Merkle-Damgård construction used by SHA-256.
HMAC for API Request Signing
HMAC (Hash-based Message Authentication Code) combines a secret key with a hash function to produce a message authentication code. It verifies both the integrity of a message (it has not been tampered with) and its authenticity (it was created by someone with the secret key).
HMAC-SHA256 in Node.js
const crypto = require("crypto")
// Signing a request (sender)
const secret = "your-shared-secret"
const payload = JSON.stringify({ userId: 123, action: "transfer", amount: 100 })
const signature = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex")
// Verifying a request (receiver)
const expectedSig = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex")
// Use timingSafeEqual to prevent timing attacks
const isValid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSig)
)AWS, Stripe, GitHub webhooks, and many other APIs use HMAC-SHA256 for request signing. Always use crypto.timingSafeEqual() for signature comparison — regular string comparison is vulnerable to timing attacks that can leak information about the expected signature.
Verifying File Integrity with Checksums
When you download software, the publisher often provides a SHA-256 checksum alongside the download. After downloading, you compute the SHA-256 hash of the file and compare it to the published checksum. If they match, the file is intact and has not been tampered with.
Verify a file checksum
# Linux/macOS
sha256sum downloaded-file.tar.gz
# Compare output to the published checksum
# PowerShell (Windows)
Get-FileHash downloaded-file.zip -Algorithm SHA256
# Node.js
const crypto = require("crypto")
const fs = require("fs")
const hash = crypto.createHash("sha256")
hash.update(fs.readFileSync("file.zip"))
console.log(hash.digest("hex"))Rainbow Tables and Why Salting Prevents Them
A rainbow table is a precomputed lookup table that maps hash values back to their original inputs. An attacker with a rainbow table can instantly reverse a hash — for example, looking up the MD5 hash of "password123" and finding the original string in milliseconds.
Salting defeats rainbow tables by adding a unique random value (the salt) to each password before hashing. Even if two users have the same password, their hashes will be completely different because their salts are different. An attacker would need to build a separate rainbow table for every possible salt value — computationally infeasible.
Modern password hashing algorithms like bcrypt, Argon2, and scrypt automatically generate and store a unique salt for each password. The salt is stored alongside the hash in the database — it does not need to be secret, only unique.
Generate Hashes Online
🔑 Hash Generator
Generate SHA-1, SHA-256, SHA-512, and MD5 hashes instantly in your browser
Choosing the Right Hash Algorithm
With so many hash algorithms available, picking the right one for your use case is critical. Using SHA-256 for password hashing is a security mistake. Using bcrypt for file checksums is unnecessarily slow. This section provides a practical decision framework that tells you exactly which algorithm to use for each scenario, with code examples and a comparison table.
Decision Framework
| Use Case | Algorithm | Why | Speed |
|---|---|---|---|
| Checksums (non-security) | MD5 | Fast, widely supported, fine when collision resistance is not needed | ~3 GB/s |
| File integrity verification | SHA-256 | Collision-resistant, standard for download verification | ~1 GB/s |
| Digital signatures | SHA-512 | Larger output, stronger security margin for long-term signatures | ~1.5 GB/s on 64-bit |
| Password hashing | Argon2id | Memory-hard, GPU-resistant, winner of Password Hashing Competition | ~0.5s per hash |
| Password hashing (legacy) | bcrypt | Time-tested, adjustable cost factor, widely supported | ~0.3s per hash |
| API authentication | HMAC-SHA256 | Combines secret key with hash for message authentication | ~1 GB/s |
| Content-addressable storage | SHA-256 | Deterministic, collision-resistant content identification | ~1 GB/s |
MD5: Checksums Only, Never Security
MD5 produces a 128-bit (32 hex character) hash and is extremely fast. It is perfectly acceptable for non-security purposes like cache keys, ETags, data deduplication checks, and verifying file transfers where intentional tampering is not a concern. However, MD5 is cryptographically broken — collisions can be generated in seconds on modern hardware. Never use MD5 for anything where an attacker might craft malicious inputs.
// MD5 for cache busting (acceptable)
const crypto = require("crypto")
const cacheKey = crypto.createHash("md5").update(requestBody).digest("hex")
// → "d41d8cd98f00b204e9800998ecf8427e"
// ❌ NEVER use MD5 for:
// - Password hashing
// - Digital signatures
// - Security-critical integrity checksSHA-256: General-Purpose Integrity
SHA-256 is the workhorse of modern cryptography. It produces a 256-bit (64 hex character) hash with no known practical attacks. Use it for file integrity verification, content addressing, blockchain applications, and any situation where you need a secure, collision-resistant fingerprint of data. It is fast enough for high-throughput applications while remaining cryptographically strong.
// SHA-256 for file integrity
const crypto = require("crypto")
const fs = require("fs")
const hash = crypto.createHash("sha256")
const stream = fs.createReadStream("large-file.zip")
stream.on("data", (chunk) => hash.update(chunk))
stream.on("end", () => {
const checksum = hash.digest("hex")
console.log(checksum)
// → "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
})bcrypt/scrypt/Argon2: Password Hashing
General-purpose hash functions like SHA-256 are designed to be fast — that is exactly what makes them terrible for password hashing. An attacker with a GPU can compute billions of SHA-256 hashes per second, making brute-force attacks trivial. Password hashing algorithms are intentionally slow and memory-intensive to make brute-force attacks economically infeasible.
// ❌ NEVER hash passwords with SHA-256
const hash = crypto.createHash("sha256").update(password).digest("hex")
// An attacker can try billions per second!
// ✅ Use bcrypt (Node.js)
const bcrypt = require("bcrypt")
const saltRounds = 12 // cost factor (2^12 = 4096 iterations)
const hashed = await bcrypt.hash(password, saltRounds)
// → "$2b$12$LJ3m4sFQ.0xZ..." (~0.3s per hash)
// ✅ Use Argon2id (recommended for new applications)
const argon2 = require("argon2")
const hashed = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536, // 64MB memory
timeCost: 3, // 3 iterations
parallelism: 4 // 4 threads
})
// Verify a password
const isValid = await argon2.verify(hashed, password)HMAC: API Authentication
HMAC (Hash-based Message Authentication Code) is not a hash algorithm itself — it is a construction that combines a secret key with a hash function to produce a keyed authentication code. It proves both that the message has not been tampered with and that it was created by someone who knows the secret key. HMAC-SHA256 is the standard for webhook signatures, API request signing, and session token generation.
// Verifying a GitHub webhook signature
const crypto = require("crypto")
function verifyWebhook(payload, signature, secret) {
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex")
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)
}
// Usage
const isValid = verifyWebhook(
req.body,
req.headers["x-hub-signature-256"],
process.env.WEBHOOK_SECRET
)SHA-512: Digital Signatures and Long-Term Security
SHA-512 produces a 512-bit (128 hex character) hash. It is actually faster than SHA-256 on 64-bit processors because it operates on 64-bit words natively. Use SHA-512 for digital signatures, certificate signing, and applications where you need a larger security margin for long-term protection (documents that must remain tamper-proof for decades).
In practice, SHA-256 is sufficient for most applications today. Choose SHA-512 when you need the extra security margin, when working with protocols that require it (like Ed25519 signatures which use SHA-512 internally), or when performance on 64-bit hardware is a priority.
Hash Algorithm Comparison Table
Here's the definitive comparison I wish someone had given me when I was starting out. Bookmark this — it'll save you from choosing the wrong algorithm:
| Algorithm | Output Size | Speed | Security | Use Case |
|---|---|---|---|---|
| MD5 | 128 bits (32 hex) | Very fast | ❌ Broken (collisions found) | Legacy checksums only — never for security |
| SHA-1 | 160 bits (40 hex) | Fast | ❌ Broken (SHAttered attack) | Git commits — avoid for new systems |
| SHA-256 | 256 bits (64 hex) | Fast | ✅ Secure | File integrity, data signing, blockchain |
| SHA-512 | 512 bits (128 hex) | Fast (64-bit) | ✅ Secure | Digital signatures, certificates |
| SHA-3 | Variable | Moderate | ✅ Secure | Post-quantum readiness, NIST standard |
| BLAKE3 | 256 bits | Very fast | ✅ Secure | File hashing, streaming, high-performance |
| bcrypt | 184 bits | Intentionally slow | ✅ Secure | Password hashing (most widely supported) |
| Argon2id | Variable | Intentionally slow | ✅ Secure (winner) | Password hashing (best choice for new apps) |
| HMAC-SHA256 | 256 bits | Fast | ✅ Secure | API signing, webhooks, JWT signatures |
Common Hash Function Mistakes (I've Seen All of These)
After reviewing hundreds of codebases, these are the hash-related mistakes that keep showing up. Learn from other people's production incidents:
⚠️ Using SHA-256 or MD5 for password storage
Impact: Passwords cracked in seconds with rainbow tables or GPU brute force. SHA-256 can hash billions of attempts per second on modern GPUs.
Fix: Use bcrypt, scrypt, or Argon2id with a minimum cost factor of 12.
⚠️ Not using constant-time comparison for signatures
Impact: Timing attacks can leak the correct signature byte-by-byte by measuring response time differences.
Fix: Always use crypto.timingSafeEqual() (Node.js), hmac.compare_digest() (Python), or MessageDigest.isEqual() (Java).
⚠️ Storing passwords with a fixed/shared salt
Impact: If one password is cracked, the attacker can build a rainbow table for all users since they share the same salt.
Fix: Generate a unique random salt per user. bcrypt and Argon2 do this automatically — that's why you should use them.
⚠️ Using MD5 for file integrity verification
Impact: Attackers can create files with identical MD5 hashes (collision attack). A malicious file could pass your integrity check.
Fix: Use SHA-256 or BLAKE3 for file checksums. MD5 collisions have been demonstrated since 2004.
⚠️ Hashing sensitive data without a secret (HMAC)
Impact: Simple hashes can be precomputed. Without a secret key, anyone can generate the same hash for the same input.
Fix: Use HMAC when you need to verify both integrity and authenticity (API signatures, tokens, cookies).
⚠️ Exposing hash outputs in URLs without rate limiting
Impact: If you use hashes as access tokens (e.g., /reset-password?token=sha256hash), brute force is easy without rate limiting.
Fix: Use cryptographically random tokens (not hashes of predictable data) and enforce rate limits + expiration.
Hash Function Interview Questions
These come up in security-focused interviews and system design rounds. Knowing the answers shows you understand cryptography at a practical level:
Q: What makes a hash function 'cryptographic'?
Three properties: pre-image resistance (can't reverse the hash), second pre-image resistance (can't find another input with same hash), and collision resistance (can't find any two inputs with same hash). Non-cryptographic hashes (CRC32, MurmurHash) only guarantee distribution, not security.
Q: Why is bcrypt better than SHA-256 for passwords?
bcrypt is intentionally slow — configurable work factor means you can increase computation time as hardware gets faster. SHA-256 is designed to be fast (billions per second on GPUs), making brute force trivial for short passwords.
Q: Can you decrypt a hash?
No. Hashing is a one-way function — information is lost. You can't reverse it mathematically. You can only compare by hashing a candidate input and checking if it matches.
Q: What is a salt and why does it matter?
A salt is random data prepended to the input before hashing. It ensures identical passwords produce different hashes, defeats rainbow tables, and forces attackers to brute-force each user individually.
Q: What's the difference between encryption and hashing?
Encryption is reversible (with a key) — you get the original data back. Hashing is irreversible — you can never recover the original input. Use encryption when you need the data back, hashing when you only need to verify.
Summary
The rule that'll save you from most hash-related mistakes is simple: never use SHA-256 for passwords, and never use bcrypt for checksums. Right tool, right job. SHA-256 is fast and that's exactly what makes it terrible for passwords — attackers love fast. Bcrypt is slow on purpose, which is perfect for passwords but absurd for verifying a file download. Internalize that distinction and you're ahead of most developers I've worked with.
