File integrity verification is crucial for secure data handling in web applications. The Web Crypto API provides built-in cryptographic operations that let you compute secure hash values directly in the browser. This post explores how to implement client-side file hashing to detect data tampering or corruption during file transfers.

Browser compatibility and requirements

The Web Crypto API is widely supported in modern browsers:

  • Chrome 41+
  • Firefox 34+
  • Safari 7+
  • Edge 79+

Important: The Web Crypto API requires a secure context (HTTPS) to function. When developing locally, localhost is considered secure by default.

Supported hash algorithms

The Web Crypto API supports several hash algorithms through the crypto.subtle.digest() method:

  • SHA-256 (recommended for general use)
  • SHA-384 (stronger security)
  • SHA-512 (strongest security)
  • SHA-1 (not recommended due to known vulnerabilities)

Implementing file hashing

Here's a complete implementation that handles file hashing with proper error handling:

async function calculateHash(file, algorithm = 'SHA-256') {
  if (!(file instanceof File)) {
    throw new Error('Input must be a File object')
  }

  try {
    const arrayBuffer = await file.arrayBuffer()
    const hashBuffer = await crypto.subtle.digest(algorithm, arrayBuffer)
    const hashArray = Array.from(new Uint8Array(hashBuffer))
    return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
  } catch (error) {
    if (error instanceof DOMException) {
      throw new Error(`Unsupported hash algorithm: ${algorithm}`)
    }
    throw new Error('Failed to calculate file hash')
  }
}

function setupFileHashing() {
  const fileInput = document.getElementById('fileInput')
  const hashOutput = document.getElementById('hashOutput')
  const algorithmSelect = document.getElementById('algorithmSelect')

  fileInput.addEventListener('change', async (e) => {
    const file = e.target.files[0]
    if (!file) return

    const algorithm = algorithmSelect.value
    hashOutput.textContent = 'Computing hash...'

    try {
      const hash = await calculateHash(file, algorithm)
      hashOutput.textContent = `${algorithm}: ${hash}`
    } catch (error) {
      hashOutput.textContent = `Error: ${error.message}`
      console.error('Hashing error:', error)
    }
  })
}

The corresponding HTML structure:

<div class="hash-container">
  <select id="algorithmSelect">
    <option value="SHA-256">SHA-256</option>
    <option value="SHA-384">SHA-384</option>
    <option value="SHA-512">SHA-512</option>
  </select>
  <input type="file" id="fileInput" />
  <div id="hashOutput"></div>
</div>

Handling large files

For large files, processing the entire content at once might cause memory issues. Here's an implementation that processes files in chunks:

async function calculateHashInChunks(file, algorithm = 'SHA-256') {
  const chunkSize = 2097152 // 2MB chunks
  const chunks = Math.ceil(file.size / chunkSize)
  let position = 0

  // Create a hash object
  const crypto = window.crypto.subtle
  let hashBuffer = await crypto.digest(algorithm, new ArrayBuffer(0))

  for (let i = 0; i < chunks; i++) {
    const chunk = file.slice(position, position + chunkSize)
    const arrayBuffer = await chunk.arrayBuffer()
    position += chunkSize

    // Update hash with current chunk
    hashBuffer = await crypto.digest(algorithm, arrayBuffer)

    // Update progress if needed
    const progress = Math.round(((i + 1) / chunks) * 100)
    console.log(`Processing: ${progress}%`)
  }

  const hashArray = Array.from(new Uint8Array(hashBuffer))
  return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
}

Verifying file integrity

To verify file integrity, compare the computed hash with a known good value:

function verifyFileIntegrity(computedHash, expectedHash) {
  // Use constant-time comparison to prevent timing attacks
  if (computedHash.length !== expectedHash.length) {
    return false
  }

  return computedHash.toLowerCase() === expectedHash.toLowerCase()
}

// Example usage
const expectedHash = '123abc...'
const isValid = verifyFileIntegrity(computedHash, expectedHash)

Best practices

  1. Always use secure hash algorithms (SHA-256 or stronger)
  2. Implement proper error handling
  3. Show progress indicators for large files
  4. Use constant-time comparisons for hash verification
  5. Consider implementing rate limiting for multiple files

Client-side file hashing adds an important security layer to web applications, helping ensure data integrity during file transfers. While this implementation uses the Web Crypto API, production systems often employ additional security measures and server-side verification.

Happy coding!