Hashing in JavaScript: A Practical Guide to MD5, SHA-256, and SHA-512
Hashing is everywhere in software development — file integrity checks, password storage, digital signatures, data deduplication. But many developers only know how to "call a library" and run into trouble when implementing it in the browser.
This guide covers practical JavaScript implementations of common hash algorithms, including browser-native APIs, Node.js methods, file hashing, and real-world gotchas.
1. Browser-Native: SubtleCrypto
Modern browsers ship with the Web Crypto API, and SubtleCrypto lets you compute SHA hashes without any third-party library.
Basic Usage
async function sha256(message) {
// 1. Encode string as UTF-8 bytes
const msgBuffer = new TextEncoder().encode(message);
// 2. Compute hash
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
// 3. Convert ArrayBuffer to hex string
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map(b => b.toString(16).padStart(2, '0'))
.join('');
return hashHex;
}
sha256('Hello World').then(hash => {
console.log(hash);
// dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f
});
Supported Algorithms
await crypto.subtle.digest('SHA-1', data); // Deprecated
await crypto.subtle.digest('SHA-256', data); // Most common
await crypto.subtle.digest('SHA-384', data);
await crypto.subtle.digest('SHA-512', data);
⚠️ Note: SubtleCrypto supports SHA family only. For MD5, you'll need a third-party library.
2. File Hashing in the Browser
Computing file hashes before upload is a common use case. Convert the File object to an ArrayBuffer:
async function hashFile(file, algorithm = 'SHA-256') {
const buffer = await file.arrayBuffer();
const hashBuffer = await crypto.subtle.digest(algorithm, buffer);
return Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
document.querySelector('input[type="file"]').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
console.log(`File: ${file.name}`);
console.log(`Size: ${(file.size / 1024 / 1024).toFixed(2)} MB`);
const hash = await hashFile(file);
console.log(`SHA-256: ${hash}`);
});
Large File Chunked Hashing
For files over 100MB, reading the entire file into memory can freeze the browser. Use chunked reading:
async function hashLargeFile(file, algorithm = 'SHA-256', chunkSize = 10 * 1024 * 1024) {
const chunks = [];
let offset = 0;
while (offset < file.size) {
const chunk = file.slice(offset, offset + chunkSize);
const buffer = await chunk.arrayBuffer();
chunks.push(new Uint8Array(buffer));
offset += chunkSize;
}
// Merge all chunks — crypto.subtle.digest requires complete data
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
const merged = new Uint8Array(totalLength);
let position = 0;
for (const chunk of chunks) {
merged.set(chunk, position);
position += chunk.length;
}
const hashBuffer = await crypto.subtle.digest(algorithm, merged);
return Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
💡 Gotcha:
crypto.subtle.digest()does NOT support incremental updates. You must pass the complete data. For true incremental hashing, use a library likespark-md5.
3. MD5 Implementation
Browsers don't natively support MD5, but many legacy systems still use it. spark-md5 is the recommended library — it supports incremental computation, perfect for large files:
import SparkMD5 from 'spark-md5';
// Simple string hash
function md5String(str) {
return SparkMD5.hash(str);
}
// Large file incremental MD5
async function md5File(file, chunkSize = 2 * 1024 * 1024) {
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
const chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
const spark = new SparkMD5.ArrayBuffer();
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
resolve(spark.end());
}
};
reader.onerror = reject;
function loadNext() {
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size);
reader.readAsArrayBuffer(blobSlice.call(file, start, end));
}
loadNext();
});
}
4. Hashing in Node.js
Node.js has a built-in crypto module — simpler and more flexible:
const crypto = require('crypto');
// String hash
function sha256(str) {
return crypto.createHash('sha256').update(str, 'utf8').digest('hex');
}
function md5(str) {
return crypto.createHash('md5').update(str, 'utf8').digest('hex');
}
// File hash with streams (great for large files!)
const fs = require('fs');
function hashFileStream(filePath, algorithm = 'sha256') {
return new Promise((resolve, reject) => {
const hash = crypto.createHash(algorithm);
const stream = fs.createReadStream(filePath);
stream.on('data', chunk => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex')));
stream.on('error', reject);
});
}
hashFileStream('./large-file.zip').then(hash => {
console.log('SHA-256:', hash);
});
5. HMAC: Keyed-Hash Message Authentication Code
Regular hashing verifies data integrity. HMAC verifies message authenticity — both generation and verification require a secret key.
// Browser
async function hmacSha256(message, secret) {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign(
'HMAC', key, encoder.encode(message)
);
return Array.from(new Uint8Array(signature))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
// Node.js
function hmacSha256Node(message, secret) {
return crypto
.createHmac('sha256', secret)
.update(message, 'utf8')
.digest('hex');
}
Common use cases: Webhook signature verification, JWT token generation, API request tamper protection.
6. Common Pitfalls
Pitfall 1: Encoding Issues
// ❌ Wrong: passing a string directly to digest()
// crypto.subtle.digest requires a BufferSource
// ✅ Correct: encode with TextEncoder first
const data = new TextEncoder().encode('Hello');
const hash = await crypto.subtle.digest('SHA-256', data);
Pitfall 2: Case Sensitivity
Different libraries may return hex in different cases. Always normalize:
function compareHash(h1, h2) {
return h1.toLowerCase() === h2.toLowerCase();
}
Pitfall 3: SubtleCrypto Requires Secure Context
crypto.subtle is only available under HTTPS or localhost. It will be undefined on plain HTTP pages.
if (typeof crypto !== 'undefined' && crypto.subtle) {
// Safe to use
} else {
// Need HTTPS or a polyfill
}
Pitfall 4: Never Hash Passwords Directly
MD5/SHA-256 of passwords are vulnerable to rainbow table attacks and GPU brute force. Always use salt + slow hash:
async function hashPassword(password, salt) {
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw', encoder.encode(password), 'PBKDF2', false, ['deriveBits']
);
return crypto.subtle.deriveBits(
{
name: 'PBKDF2',
salt: encoder.encode(salt),
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
256
);
}
For production, use bcrypt, scrypt, or Argon2.
Pitfall 5: MD5 Performance on Large Files
Don't use a pure-JS MD5 implementation for large files — it's slow. Use spark-md5 with chunked incremental mode, or Node.js built-in crypto.createHash('md5').
7. Algorithm Selection Guide
| Use Case | Recommended Algorithm | Notes |
|---|---|---|
| File integrity check | SHA-256 | Most common, fast, secure enough |
| Password storage | bcrypt/Argon2 | Salt + slow hash, never MD5/SHA |
| Digital signatures | SHA-256 + RSA | Or SHA-384 for higher security |
| Data dedup/cache keys | MD5/SHA-1 | Acceptable for non-security uses |
| HMAC signatures | HMAC-SHA256 | API signing, webhook verification |
| Blockchain | SHA-256 | Bitcoin standard |
| Legacy compatibility | MD5 | Non-security purposes only |
Tool Recommendation
If you need to quickly compute hashes for text or files, try the free online hash calculator — supports MD5, SHA-1, SHA-256, SHA-384, and SHA-512. All computation happens in your browser; no data is uploaded to any server.
Source code: github.com/jiebang-tools/tools
Summary
- Use
crypto.subtle.digest()in browsers for SHA — no libraries needed - MD5 requires a third-party library (spark-md5) or Node.js built-in crypto
- Use chunked + incremental hashing for large files to avoid memory issues
- SubtleCrypto requires HTTPS (except localhost)
- Never store passwords with plain hashes — always use salt + slow hash (bcrypt/Argon2)
- Use HMAC for message authentication, not plain hashing
Happy coding! 🚀
Top comments (0)