Hash files in the browser with Web Crypto
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
This example needs both crypto.subtle and File.prototype.arrayBuffer, available in modern
browsers. Check these features before enabling the hashing UI; support for Web Crypto alone does
not guarantee support for the file-reading API used here.
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 (384-bit digest)
- SHA-512 (512-bit digest)
- 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">
<label for="algorithmSelect">Hash algorithm</label>
<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>
<label for="fileInput">Choose a file to hash</label>
<input type="file" id="fileInput" />
<div id="hashOutput" role="status"></div>
</div>
Call setupFileHashing() after this HTML has loaded, for example from a deferred script.
Handling large files
crypto.subtle.digest() does not support streaming input.
The complete file must fit in memory. Hashing separate chunks independently does not produce the
hash of the complete file; keeping only the last result verifies only the last chunk.
For files that exceed your application's memory budget, use a maintained incremental hashing library or hash a stream on the server. A Web Worker can keep hashing work off the main thread, but moving this example into a worker does not remove its whole-file memory requirement.
Verifying file integrity
To verify file integrity, compare the computed hash with an expected value obtained from a trusted source. An attacker who can replace both the file and the published checksum can make them match.
function verifyFileIntegrity(computedHash, expectedHash) {
// These are public checksums, not secret authentication values.
if (computedHash.length !== expectedHash.length) {
return false
}
return computedHash.toLowerCase() === expectedHash.toLowerCase()
}
// Pass the selected File and a trusted SHA-256 checksum to this helper.
async function verifySelectedFile(file, expectedHash) {
const computedHash = await calculateHash(file, 'SHA-256')
return verifyFileIntegrity(computedHash, expectedHash)
}
Best practices
- Use SHA-256, SHA-384, or SHA-512 rather than SHA-1 for new integrity checks
- Implement proper error handling
- Show an indeterminate busy indicator while Web Crypto computes the digest
- Obtain the expected checksum through a trusted channel
- 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!
