DEV Community

jiebang-tools
jiebang-tools

Posted on

Hashing in JavaScript: A Practical Guide to MD5, SHA-256, and SHA-512

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
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

⚠️ 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}`);
});
Enter fullscreen mode Exit fullscreen mode

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('');
}
Enter fullscreen mode Exit fullscreen mode

💡 Gotcha: crypto.subtle.digest() does NOT support incremental updates. You must pass the complete data. For true incremental hashing, use a library like spark-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();
  });
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Pitfall 2: Case Sensitivity

Different libraries may return hex in different cases. Always normalize:

function compareHash(h1, h2) {
  return h1.toLowerCase() === h2.toLowerCase();
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
  );
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Use crypto.subtle.digest() in browsers for SHA — no libraries needed
  2. MD5 requires a third-party library (spark-md5) or Node.js built-in crypto
  3. Use chunked + incremental hashing for large files to avoid memory issues
  4. SubtleCrypto requires HTTPS (except localhost)
  5. Never store passwords with plain hashes — always use salt + slow hash (bcrypt/Argon2)
  6. Use HMAC for message authentication, not plain hashing

Happy coding! 🚀

Top comments (0)