NV ToolkitFormat · Validate · Convert
Guide

SHA-256 vs MD5 vs HMAC: How Hashing Works and Which One to Use

A hash function turns any input into a fixed-size fingerprint. Feed it a 4 GB installer or the two words 'hello world' and SHA-256 returns 32 bytes, the same 32 bytes every time. MD5, SHA-1, SHA-256 and HMAC-SHA256 all do this, and they are routinely confused with each other and with password hashing, which is a different job with different tools. This guide explains what each one guarantees, where the older algorithms broke, and which to use. Every digest shown was computed with Node 22's crypto module.

In short

SHA-256 is the right default hash for integrity checks and content addressing: MD5 and SHA-1 lost collision resistance years ago and should not sign or identify anything an attacker could choose. HMAC adds a secret key so a hash can prove authorship, not just integrity. Passwords need neither alone — use a slow function such as Argon2 or bcrypt.

By NaveenKumar T · Software engineer · builds and maintains NV Toolkit Published 2026-09-10 6 min read

What a cryptographic hash promises

Any function that maps input to a fixed-size output is a hash; a cryptographic hash makes three specific promises. Preimage resistance: given a digest, you cannot find an input that produces it. Second-preimage resistance: given one input, you cannot find a different input with the same digest. Collision resistance: you cannot find any two inputs that share a digest. The third is the strongest promise and the first to fall as an algorithm ages, because the attacker gets to choose both inputs.

Two properties follow from the design. The output is deterministic (the same bytes always produce the same digest), which is what makes hashes useful as fingerprints, cache keys and integrity checks. And it has the avalanche property: changing one bit of input flips about half the output bits, so 'hello world' and 'hello worlD' share no visible relationship. A hash is not encryption: there is no key and no way back. It is also not secret: anyone can compute the SHA-256 of a file, which is exactly why a bare hash proves integrity but never authenticity.

bash
sha256("hello world") = b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
sha256("hello worlD") = 519fd0f684972f4eaa0946cc880198619d11c1f2f026d8f84ab080ad54f72c1a
sha256("")            = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

MD5 and SHA-1: broken for security, fine for checksums

MD5 (1992, 128-bit digest) lost collision resistance in 2004, when Wang and Yu published a practical way to produce two different inputs with the same digest. In 2008 researchers used a chosen-prefix collision to forge a certificate-authority certificate trusted by every browser, and the Flame malware discovered in 2012 used the same class of attack against Microsoft's code-signing chain. Today an MD5 collision costs seconds on a laptop. SHA-1 (1995, 160-bit) followed: the SHAttered attack in 2017 produced two PDFs with the same SHA-1 for about 2^63 computations, and the 2020 'SHA-1 is a Shambles' work made chosen-prefix collisions practical for roughly $45,000 of rented GPU time. Browsers stopped accepting SHA-1 certificates in 2017, and Git is migrating its object model to SHA-256.

What broke is collision resistance, and only that: nobody can reverse an MD5 or find a preimage for a given digest. So the question to ask is whether an attacker gets to choose the input. For detecting accidental corruption in a download, bit rot on a disk, or duplicate files in your own photo library, MD5 and SHA-1 still work and are slightly faster. Anywhere a person could benefit from two inputs sharing a hash (certificates, signatures, content-addressed storage of untrusted files, deduplication across users, commit identity) they are unacceptable, and for new work there is no reason not to use SHA-256 everywhere.

bash
md5("hello world")    = 5eb63bbbe01eeed093cb22bb8f5acdc3                           128 bits, 32 hex chars
sha1("hello world")   = 2aae6c35c94fcfb415dbe95f408b9ce91ee846ed                   160 bits, 40 hex chars
sha256("hello world") = b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9   256 bits, 64 hex chars

SHA-2 and SHA-3: today's defaults

SHA-2 is a family standardised by NIST in 2001–2002: SHA-224, SHA-256, SHA-384 and SHA-512, plus the truncated SHA-512/224 and SHA-512/256. SHA-256 works on 32-bit words and is the default nearly everywhere: TLS certificates, package managers, Bitcoin, code signing. SHA-512 works on 64-bit words and is faster than SHA-256 on 64-bit CPUs that lack hardware acceleration; with the SHA extensions present in current x86 and ARM chips, SHA-256 pulls ahead. No practical attack on any SHA-2 member is known.

SHA-3, standardised in 2015 as FIPS 202, is not a fix for a weakness; it is a deliberately different design (the Keccak sponge) chosen so that a structural break in SHA-2's construction would not take both down at once. SHA3-256 gives the same 256-bit output and is immune by construction to the length-extension problem described in the next section. It is slower in software than SHA-256 and rarer in protocols, so SHA-256 remains the pragmatic default, with SHA-3 as the alternative when a design calls for diversity or for variable-length output (SHAKE128 and SHAKE256).

AlgorithmDigest sizeHex lengthStatus
MD5128 bits32 charsCollision-broken since 2004 — checksums only
SHA-1160 bits40 charsCollision-broken since 2017 — checksums only
SHA-256256 bits64 charsNo known attack — today's default
SHA-512512 bits128 charsNo known attack — faster on 64-bit CPUs
SHA3-256256 bits64 charsNo known attack — immune to length extension
bash
sha512("hello world")   = 309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f
                          989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f
sha3-256("hello world") = 644bcc7e564373040999aac89e7622f3ca71fba1d972fd94a31c3bfbf24e3938

# Same SHA-256 digest, base64 instead of hex (the form Subresource Integrity uses)
sha256("hello world")   = uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=

HMAC, and why hash(key + message) is broken

A hash proves that data is unchanged; it cannot prove who produced it, because anyone can recompute it. A message authentication code adds a secret key so that only key holders can produce a valid tag. The obvious construction, hash(key + message), is broken for MD5, SHA-1, SHA-256 and SHA-512 by length extension. These Merkle–Damgård hashes process input in blocks, and the digest is simply the final internal state. Given hash(key + message) and the length of key + message, an attacker can resume the computation and produce a valid hash(key + message + padding + extra) without ever learning the key: a forged tag for a message they chose.

HMAC (RFC 2104, 1997) fixes this by hashing twice with the key mixed in differently each time: HMAC(K, m) = H((K ⊕ opad) ‖ H((K ⊕ ipad) ‖ m)). The outer hash hides the inner state, so extension is impossible, and the security argument holds even for hashes with known collisions, which is why HMAC-MD5 is not technically broken even though nothing new should use it. HMAC-SHA256 is what signs JWTs with HS256, AWS Signature Version 4, and webhook payloads from Stripe and GitHub. Compare tags with a constant-time function such as crypto.timingSafeEqual, so that response timing does not reveal how many leading bytes were correct.

javascript
const { createHmac, timingSafeEqual } = require("node:crypto");

createHmac("sha256", "secret").update("hello world").digest("hex");
// 734cc62f32841568f45715aeb9f4d7891324e6d948e4c6c60c0621cdac48623a
createHmac("sha256", "secret2").update("hello world").digest("hex");
// 3c3af964bc187dca5576909ec010f39fffcfdaf299ab3b8e0e07d733e77e7212   different key → unrelated tag

// Verifying a webhook signature the right way
function verify(payload, receivedHex, key) {
  const expected = createHmac("sha256", key).update(payload).digest();
  const received = Buffer.from(receivedHex, "hex");
  return received.length === expected.length && timingSafeEqual(received, expected);
}

Passwords are a different problem

Password storage looks like a hashing problem and is not. A general-purpose hash is designed to be fast; a single GPU computes billions of SHA-256 digests per second, so an unsalted SHA-256 of a password is only as strong as the password itself. Every common password's hash is already in a lookup table, and uncommon ones fall to brute force. A salt (a random per-user value stored beside the hash) defeats precomputed tables, but it does nothing to slow a brute-force attack against one user.

Password hashing functions are deliberately slow and, ideally, memory-hungry so that GPUs and ASICs gain little. bcrypt (1999) has a cost parameter that doubles the work with each increment; scrypt (2009) added memory hardness; Argon2 won the Password Hashing Competition in 2015, and Argon2id is the current recommendation, with OWASP's floor being 19 MiB of memory, 2 iterations and 1 lane. PBKDF2-HMAC-SHA256 with 600,000 iterations is the FIPS-friendly fallback. All of them generate and store the salt for you. Use a library (argon2 or bcrypt on npm, passlib in Python) rather than composing primitives yourself, and note that bcrypt silently ignores input beyond 72 bytes. Node ships scrypt in its crypto module, which the example below uses because it needs no dependency.

javascript
const { scryptSync, randomBytes, timingSafeEqual } = require("node:crypto");
const PARAMS = { N: 16384, r: 8, p: 1 };          // 16 MiB of memory per hash

function hashPassword(password) {
  const salt = randomBytes(16);
  const hash = scryptSync(password, salt, 32, PARAMS);
  return salt.toString("hex") + ":" + hash.toString("hex");
}
function verifyPassword(password, stored) {
  const [saltHex, hashHex] = stored.split(":");
  const expected = Buffer.from(hashHex, "hex");
  const got = scryptSync(password, Buffer.from(saltHex, "hex"), expected.length, PARAMS);
  return timingSafeEqual(got, expected);
}

const stored = hashPassword("correct horse battery staple");
// "619449150db1140470c74a67bc861340:47bcf4c4aef14c70783b2824b9df2dce9050ddd910ece85b073efb7a5e2c77ea"
verifyPassword("correct horse battery staple", stored)   // true
verifyPassword("Tr0ub4dor&3", stored)                    // false

Verifying a download

Projects publish a SHA-256 next to each release so you can confirm the file you received is the file they built. Compute it locally and compare: sha256sum on Linux, shasum -a 256 on macOS, Get-FileHash in PowerShell or certutil -hashfile on Windows. Most projects also ship a checksums file listing every artifact, and sha256sum -c checks all of them in one command.

Be clear about what the check proves. If the hash came from the same server as the file, a compromised server can change both, so a match proves only that the download was not corrupted in transit, which is worthwhile but is not authentication. Authenticity requires a signature verified against a key you obtained independently: a GPG-signed checksums file, minisign, or Sigstore for containers and npm packages. A hash copied from the project's own HTTPS page is a reasonable middle ground; a hash pasted in a forum thread is not.

bash
# Linux
sha256sum node-v22.22.2-linux-x64.tar.xz
# macOS
shasum -a 256 node-v22.22.2-darwin-arm64.tar.gz
# Windows, PowerShell
Get-FileHash .\node-v22.22.2-x64.msi -Algorithm SHA256
# Windows, cmd
certutil -hashfile node-v22.22.2-x64.msi SHA256

# Check every file listed in a checksums file (skip the ones you did not download)
sha256sum -c SHASUMS256.txt --ignore-missing

# Node.js signs its checksums file; verify the signature, then the files
gpg --verify SHASUMS256.txt.sig SHASUMS256.txt

Computing hashes in Node and in the browser

Node's crypto module covers every algorithm mentioned here: createHash accepts md5, sha1, sha256, sha384, sha512, sha3-256, blake2b512 and more (crypto.getHashes() lists 52 names in Node 22). For large files, feed a read stream into the hash instead of loading the file into memory. The digest can be returned as hex, base64 or a Buffer.

In the browser, the Web Crypto API exposes crypto.subtle.digest with SHA-1, SHA-256, SHA-384 and SHA-512 (MD5 is deliberately absent), and it only works in secure contexts. It is asynchronous and returns an ArrayBuffer, so converting to hex is a short loop. HMAC goes through crypto.subtle.importKey followed by crypto.subtle.sign. The same calls exist in Node under globalThis.crypto.subtle, so one helper can be shared between server and client without a dependency. Our hash generator runs exactly this API when you want a digest without opening a terminal.

javascript
// Node: hash a string, then a large file as a stream
const { createHash } = require("node:crypto");
const fs = require("node:fs");

createHash("sha256").update("hello world").digest("hex");
// b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9

const h = createHash("sha256");
fs.createReadStream("big.iso")
  .on("data", chunk => h.update(chunk))
  .on("end", () => console.log(h.digest("hex")));

// Browser (and Node 19+ via globalThis.crypto): SHA-256 with Web Crypto
async function sha256Hex(text) {
  const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
  return [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, "0")).join("");
}
await sha256Hex("hello world");
// b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9   — identical to Node

// HMAC-SHA256 with Web Crypto
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode("secret"),
  { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode("hello world"));
// → 734cc62f32841568f45715aeb9f4d7891324e6d948e4c6c60c0621cdac48623a
How this was tested: Every digest and HMAC tag shown in this guide was computed with Node.js 22.22's crypto module on 2026-09-10.

Sources

  1. RFC 6234 — US Secure Hash Algorithms (SHA and SHA-based HMAC and HKDF)
  2. NIST FIPS 180-4 — Secure Hash Standard (SHS)
  3. RFC 2104 — HMAC: Keyed-Hashing for Message Authentication

Frequently asked questions

Is MD5 still safe for checksums?

For detecting accidental corruption, yes: a flipped bit in transit or on disk will change the MD5 with overwhelming probability. It is unsafe whenever someone could deliberately craft a second file with the same digest, so do not use it to verify software from an untrusted mirror, to identify content, or in anything involving signatures.

What is the difference between hashing and encryption?

Encryption is reversible with a key; hashing is one-way and has no key. Encrypted data can be decrypted back to the original, while a hash is a fixed-size fingerprint that cannot be turned back into the input. Use encryption to protect data you need to read again, and hashing to verify data or store passwords.

Why is a SHA-256 hash always 64 characters?

The digest is 256 bits, and hex notation represents 4 bits per character, so 256 / 4 = 64 characters. The same digest is 44 characters in base64 and 32 raw bytes. MD5's 128 bits give 32 hex characters, and SHA-1's 160 bits give 40.

Can two different files have the same SHA-256?

In principle, yes: there are infinitely many inputs and only 2^256 outputs. In practice, finding such a pair would take on the order of 2^128 operations, which is far beyond any computing capacity that exists or is foreseeable. No SHA-256 collision has ever been found, which is why Git, TLS and package managers treat the digest as a unique identity.

Should I use HMAC or a digital signature?

HMAC needs a shared secret, so anyone who can verify a tag can also forge one. That is fine between two parties that trust each other, such as a server and its own cookies or a webhook sender and receiver. When many parties must verify but only one may sign (software releases, tokens checked by several services), use a signature scheme such as Ed25519 or RSA, which is what JWT's RS256 and ES256 algorithms do.

Paste text or drop a file into the Hash Generator to get MD5, SHA-1, SHA-256 and SHA-512 digests side by side.

Compute a hash

Keep working

Related tools