Hash Functions Explained

SHA-256, MD5, SHA-512 — what they are and when to use each

SecurityMarch 10, 20268 min readBy Keyur Patel

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

Input: "hello"
Hash: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
Input: "hello." (one dot added)
Hash: d9014c4624844aa5bac314773d6b689ad467fa4e1d1a50a1b8a99d5a95f72ff5

Comparing Hash Algorithms

AlgorithmOutput SizeSpeedSecurityStatus
MD5128 bits (32 hex)Very fastBroken❌ Deprecated
SHA-1160 bits (40 hex)FastWeak❌ Deprecated
SHA-256256 bits (64 hex)FastStrong✅ Recommended
SHA-512512 bits (128 hex)ModerateVery strong✅ Recommended
SHA-3VariableModerateVery 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 CaseAlgorithmWhySpeed
Checksums (non-security)MD5Fast, widely supported, fine when collision resistance is not needed~3 GB/s
File integrity verificationSHA-256Collision-resistant, standard for download verification~1 GB/s
Digital signaturesSHA-512Larger output, stronger security margin for long-term signatures~1.5 GB/s on 64-bit
Password hashingArgon2idMemory-hard, GPU-resistant, winner of Password Hashing Competition~0.5s per hash
Password hashing (legacy)bcryptTime-tested, adjustable cost factor, widely supported~0.3s per hash
API authenticationHMAC-SHA256Combines secret key with hash for message authentication~1 GB/s
Content-addressable storageSHA-256Deterministic, 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 checks

SHA-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:

AlgorithmOutput SizeSpeedSecurityUse Case
MD5128 bits (32 hex)Very fast❌ Broken (collisions found)Legacy checksums only — never for security
SHA-1160 bits (40 hex)Fast❌ Broken (SHAttered attack)Git commits — avoid for new systems
SHA-256256 bits (64 hex)Fast✅ SecureFile integrity, data signing, blockchain
SHA-512512 bits (128 hex)Fast (64-bit)✅ SecureDigital signatures, certificates
SHA-3VariableModerate✅ SecurePost-quantum readiness, NIST standard
BLAKE3256 bitsVery fast✅ SecureFile hashing, streaming, high-performance
bcrypt184 bitsIntentionally slow✅ SecurePassword hashing (most widely supported)
Argon2idVariableIntentionally slow✅ Secure (winner)Password hashing (best choice for new apps)
HMAC-SHA256256 bitsFast✅ SecureAPI 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.