Cryptographic Hashing: A Developer's Guide
· Cosyslabs
A cryptographic hash function takes arbitrary input and produces a fixed-length digest that is deterministic, fast to compute, and practically impossible to reverse or fake. Hash functions power digital signatures, password storage, file integrity checks, blockchain, and message authentication. Choosing the wrong function or using a secure function incorrectly causes exploitable vulnerabilities.
Security Properties
A cryptographically secure hash function must satisfy three properties:
Pre-image resistance: Given a hash h, finding any input m such that hash(m) = h must be computationally infeasible. This means you cannot reverse a hash.
Second pre-image resistance: Given an input m1, finding a different input m2 such that hash(m1) = hash(m2) must be computationally infeasible.
Collision resistance: Finding any two different inputs m1 and m2 such that hash(m1) = hash(m2) must be computationally infeasible. Collisions exist in theory (pigeonhole principle) but must be practically undiscoverable.
When any of these properties are broken, the algorithm is cryptographically broken.
SHA-2 Family (Current Standard)
SHA-2 is a family of hash functions designed by the NSA and standardized by NIST in 2001. All variants are currently secure.
SHA-256
The most widely deployed cryptographic hash function:
// Web Crypto API (browser + Node.js 15+)
async function sha256(message) {
const encoded = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest("SHA-256", encoded);
return Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, "0"))
.join("");
}
// SHA-256 of empty string
await sha256("");
// "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
// File hashing
async function hashFile(file) {
const buffer = await file.arrayBuffer();
const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
return Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, "0"))
.join("");
}
import hashlib
# String
digest = hashlib.sha256(b"hello world").hexdigest()
# File (streaming for large files)
def sha256_file(path):
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()
# Command line
echo -n "hello world" | sha256sum
sha256sum filename.zip
# macOS
shasum -a 256 filename.zip
SHA-384 and SHA-512
SHA-512 processes data in 1024-bit blocks vs SHA-256's 512-bit blocks. On 64-bit systems, SHA-512 is often faster than SHA-256 for large inputs:
const hash512 = await crypto.subtle.digest("SHA-512", encoded);
const hash384 = await crypto.subtle.digest("SHA-384", encoded);
SHA-512/256 (SHA-512 truncated to 256 bits) resists length-extension attacks that affect SHA-256. Use it when you need 256-bit output with length-extension resistance.
SHA-3 (Keccak)
SHA-3 uses a fundamentally different construction (sponge function) than SHA-2 (Merkle-Damgård). Both are currently secure — SHA-3 provides algorithm diversity:
import hashlib
digest = hashlib.sha3_256(b"hello world").hexdigest()
digest_512 = hashlib.sha3_512(b"hello world").hexdigest()
# SHAKE — variable-length output
shake = hashlib.shake_256(b"hello world").hexdigest(32) # 32 bytes
SHA-3 is not hardware-optimized on most CPUs (no SHA-NI extensions), making it 3–5x slower than SHA-256 in software. Use SHA-3 for algorithm diversity in high-security contexts, not for performance.
BLAKE3
BLAKE3 is a modern hash function (2020) that is faster than MD5 while being cryptographically secure:
// Using the blake3 npm package
import { hash } from "blake3";
const digest = hash("hello world").toString("hex");
// Returns 256-bit hash at >1 GB/s on modern hardware
import blake3
digest = blake3.blake3(b"hello world").hexdigest()
BLAKE3 is ideal for checksums, file deduplication, and content-addressed storage where SHA-256's speed is a bottleneck.
HMAC: Keyed Message Authentication
A hash alone cannot authenticate a message — anyone can compute SHA-256(message). HMAC adds a secret key:
HMAC(key, message) = hash((key ⊕ opad) || hash((key ⊕ ipad) || message))
HMAC ensures that only parties who know the secret key can produce or verify the MAC:
// Web Crypto API
async function hmacSha256(key, message) {
const keyBytes = new TextEncoder().encode(key);
const msgBytes = new TextEncoder().encode(message);
const cryptoKey = await crypto.subtle.importKey(
"raw", keyBytes,
{ name: "HMAC", hash: "SHA-256" },
false,
["sign", "verify"]
);
const signature = await crypto.subtle.sign("HMAC", cryptoKey, msgBytes);
return Array.from(new Uint8Array(signature))
.map(b => b.toString(16).padStart(2, "0"))
.join("");
}
import hmac, hashlib
mac = hmac.new(
key=b"secret-key",
msg=b"message to authenticate",
digestmod=hashlib.sha256
).hexdigest()
# Constant-time comparison (prevents timing attacks)
is_valid = hmac.compare_digest(received_mac, expected_mac)
HMAC use cases:
- Webhook signature verification (GitHub, Stripe webhooks)
- JWT HS256 signature generation
- API request signing (AWS Signature V4)
- Cookie integrity protection
Webhook Verification
// Verify GitHub webhook signature
async function verifyGithubWebhook(payload, signature, secret) {
const mac = await hmacSha256(secret, payload);
const expected = `sha256=${mac}`;
// Constant-time comparison
if (signature.length !== expected.length) return false;
let diff = 0;
for (let i = 0; i < signature.length; i++) {
diff |= signature.charCodeAt(i) ^ expected.charCodeAt(i);
}
return diff === 0;
}
Password Hashing
General-purpose hash functions (SHA-256, SHA-3, BLAKE3) are too fast for password storage. On a GPU, an attacker can compute billions of SHA-256 hashes per second, making brute-force practical.
Password hashing requires intentionally slow functions with configurable work factors.
bcrypt
import bcrypt from "bcrypt";
const ROUNDS = 12; // 2^12 iterations — adjust as hardware speeds up
// Hash
const hash = await bcrypt.hash("user_password_here", ROUNDS);
// "$2b$12$..." — 60 chars, includes salt and rounds
// Verify
const isValid = await bcrypt.compare("user_password_here", hash);
bcrypt limitations:
- Truncates passwords at 72 bytes
- Cannot be parallelized (intentional)
- Does not support custom memory requirements
Argon2id (Recommended for New Projects)
import argon2 from "argon2";
// Hash
const hash = await argon2.hash("user_password_here", {
type: argon2.argon2id, // Best variant — resists GPU and side-channel attacks
memoryCost: 65536, // 64 MB RAM required per hash
timeCost: 3, // 3 passes
parallelism: 4, // 4 threads
});
// Verify
const isValid = await argon2.verify(hash, "user_password_here");
Argon2id targets 500ms hash time on your production hardware. Adjust memoryCost and timeCost to achieve this on your servers.
PBKDF2 (Legacy / FIPS Compliance)
// Web Crypto API PBKDF2
async function pbkdf2Hash(password, salt) {
const keyMaterial = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
"PBKDF2",
false,
["deriveBits"]
);
const derivedBits = await crypto.subtle.deriveBits(
{
name: "PBKDF2",
salt: new TextEncoder().encode(salt),
iterations: 600000, // OWASP 2023 recommendation
hash: "SHA-256",
},
keyMaterial,
256
);
return Array.from(new Uint8Array(derivedBits))
.map(b => b.toString(16).padStart(2, "0"))
.join("");
}
Digital Signatures
Hash functions are the foundation of digital signatures. The signature process:
- Compute
hash = SHA-256(document) - Sign:
signature = RSA_sign(privateKey, hash) - Verify:
SHA-256(document) === RSA_verify(publicKey, signature)
// Generate key pair
const keyPair = await crypto.subtle.generateKey(
{ name: "ECDSA", namedCurve: "P-256" },
true,
["sign", "verify"]
);
// Sign
const signature = await crypto.subtle.sign(
{ name: "ECDSA", hash: "SHA-256" },
keyPair.privateKey,
new TextEncoder().encode(document)
);
// Verify
const isValid = await crypto.subtle.verify(
{ name: "ECDSA", hash: "SHA-256" },
keyPair.publicKey,
signature,
new TextEncoder().encode(document)
);
File Integrity Verification
Hash digests verify that files have not been corrupted or tampered with:
# Generate checksum file
sha256sum release-v1.0.tar.gz > SHA256SUMS
# Verify
sha256sum -c SHA256SUMS
// Browser — verify downloaded file
async function verifyDownload(fileBuffer, expectedSha256) {
const hashBuffer = await crypto.subtle.digest("SHA-256", fileBuffer);
const actualHash = Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, "0"))
.join("");
return actualHash === expectedSha256.toLowerCase();
}
Length Extension Attacks
SHA-256 and SHA-512 (but not SHA-3 or BLAKE2/3) are vulnerable to length extension attacks. If you know hash(message), you can compute hash(message || padding || extension) without knowing message.
Never construct a MAC as hash(key + message) — use HMAC instead. HMAC is immune to length extension attacks by design.
Broken Algorithms: Quick Reference
| Algorithm | Status | Last Safe Use |
|---|---|---|
| MD5 | Broken (2004) | Never — collisions trivial |
| SHA-1 | Broken (2017) | Never — practical collision demonstrated |
| SHA-256 | Secure | Recommended |
| SHA-512 | Secure | Recommended |
| SHA-3-256 | Secure | Recommended |
| BLAKE3 | Secure | Recommended for high-speed |
| bcrypt | Secure | Passwords only |
| Argon2id | Secure | Preferred for passwords |
Tools
- Hash Generator — SHA-256, SHA-512, SHA-3, MD5 (legacy)
- HMAC Generator — generate and verify HMACs
- JWT Decoder — inspect and verify JWT signatures