Calcula hashes de archivos en el navegador con Web Crypto
Para calcular el hash de un archivo en el navegador, lee sus bytes con
file.arrayBuffer(), pásalos a crypto.subtle.digest() y convierte el resultado a
hexadecimal. La página que se muestra a continuación te permite seleccionar un archivo local,
calcular su suma de verificación y, opcionalmente, compararla con la que proporciona quien lo publica.
Lee el archivo localmente sin subirlo.
Compatibilidad y requisitos del navegador
Necesitas un navegador con crypto.subtle y File.prototype.arrayBuffer.
Web Crypto requiere un contexto seguro: usa HTTPS al alojar la
página, o una dirección de bucle local como http://127.0.0.1 para el desarrollo local.
El ejemplo comprueba ambas API antes de habilitar el formulario.
Guarda los dos archivos siguientes en el mismo directorio. No se necesitan paquetes de JavaScript ni un paso de compilación. El comando para servir los archivos localmente usa un shell POSIX y Python 3; también puedes usar un servidor de archivos estáticos existente.
Algoritmos hash compatibles
Usa el mismo algoritmo que la suma de verificación de quien publica el archivo. Este ejemplo usa SHA-256 de forma predeterminada y también ofrece SHA-384 y SHA-512. Sus hashes hexadecimales contienen 64, 96 y 128 caracteres, respectivamente. La API de cálculo de hashes de Web Crypto también admite SHA-1, pero no es adecuado para nuevas comprobaciones de seguridad debido a sus debilidades conocidas frente a colisiones.
Implementación del cálculo de hashes de archivos
Guarda este contenido como index.html. El script diferido se ejecuta después de
que se haya analizado el formulario. Los controles están deshabilitados inicialmente para impedir
el envío del formulario si JavaScript no se carga.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>File checksum</title>
<script src="hash.js" defer></script>
</head>
<body>
<h1>File checksum</h1>
<form id="hashForm">
<fieldset id="hashControls" disabled>
<legend>Hash a local file</legend>
<p>
<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>
</p>
<p>
<label for="fileInput">File (up to 50 MiB)</label>
<input type="file" id="fileInput" required />
</p>
<p>
<label for="expectedHash">Expected hexadecimal checksum (optional)</label>
<input type="text" id="expectedHash" spellcheck="false" autocomplete="off" />
</p>
<button type="submit">Calculate checksum</button>
</fieldset>
</form>
<p id="hashOutput" role="status">Checking browser support…</p>
<noscript>Enable JavaScript to calculate a checksum.</noscript>
</body>
</html>
Guarda este contenido como hash.js:
async function calculateHash(file, algorithm = 'SHA-256') {
if (!(file instanceof File)) {
throw new Error('Input must be a File object')
}
const arrayBuffer = await file.arrayBuffer()
const hashBuffer = await crypto.subtle.digest(algorithm, arrayBuffer)
return Array.from(new Uint8Array(hashBuffer), (byte) =>
byte.toString(16).padStart(2, '0'),
).join('')
}
function setupFileHashing() {
const form = document.getElementById('hashForm')
const controls = document.getElementById('hashControls')
const fileInput = document.getElementById('fileInput')
const algorithmSelect = document.getElementById('algorithmSelect')
const expectedInput = document.getElementById('expectedHash')
const output = document.getElementById('hashOutput')
const digestLengths = { 'SHA-256': 64, 'SHA-384': 96, 'SHA-512': 128 }
if (
!window.isSecureContext ||
!window.crypto?.subtle ||
typeof File === 'undefined' ||
typeof File.prototype.arrayBuffer !== 'function'
) {
output.textContent =
'Use HTTPS or localhost in a browser with Web Crypto and File.arrayBuffer support.'
return
}
controls.disabled = false
output.textContent = 'Choose a file, then calculate its checksum.'
form.addEventListener('input', () => {
output.textContent = 'Inputs changed. Calculate the checksum again.'
})
form.addEventListener('submit', async (event) => {
event.preventDefault()
if (controls.disabled) return
const file = fileInput.files[0]
if (!file) {
output.textContent = 'Choose a file first.'
return
}
if (file.size > 50 * 1024 * 1024) {
output.textContent = 'Choose a file of 50 MiB or smaller.'
return
}
const algorithm = algorithmSelect.value
const expected = expectedInput.value.trim().toLowerCase()
if (
expected !== '' &&
(expected.length !== digestLengths[algorithm] || !/^[0-9a-f]+$/.test(expected))
) {
output.textContent = `Enter ${digestLengths[algorithm]} hexadecimal characters for ${algorithm}.`
return
}
controls.disabled = true
output.textContent = 'Computing checksum…'
try {
const hash = await calculateHash(file, algorithm)
let verdict = 'No expected checksum supplied.'
if (expected !== '') {
verdict = hash === expected ? 'Checksums match.' : 'Checksums do not match.'
}
output.textContent = `${file.name} (${algorithm}): ${hash}. ${verdict}`
} catch (error) {
output.textContent =
error instanceof DOMException && error.name === 'NotReadableError'
? 'Could not read the file. Select it again and retry.'
: 'Could not calculate the checksum. Try a smaller file or another browser.'
} finally {
controls.disabled = false
}
})
}
setupFileHashing()
calculateHash() lee los bytes sin procesar, sin decodificarlos como texto. El nombre
del archivo y el tipo MIME no intervienen en el hash. Deja que el código que la llama gestione los
errores. Un error de lectura del archivo no debe notificarse como un algoritmo no compatible:
la API File puede rechazar una lectura cuando el archivo subyacente
cambia o deja de ser accesible.
El formulario bloquea sus campos durante el cálculo del hash y vuelve a habilitarlos tanto si el
cálculo termina correctamente como si falla. Cambiar un campo borra el resultado anterior. Esto
evita que una suma de verificación parezca corresponder a otro archivo o algoritmo. El mensaje de
procesamiento en curso no muestra un porcentaje porque digest() no proporciona
eventos de progreso.
Verificación de la integridad de los archivos
En una terminal, cambia al directorio que contiene index.html y
hash.js y ejecuta:
printf 'abc' > sample.txt
python3 -m http.server 8000 --bind 127.0.0.1
Abre http://127.0.0.1:8000/, elige sample.txt, mantén SHA-256 seleccionado
y pega esta suma de verificación esperada:
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
Haz clic en Calculate checksum. El resultado incluye el nombre del archivo, el algoritmo, el hash calculado y Checksums match. El comando crea exactamente tres bytes sin un salto de línea al final. Un editor de texto que añada un salto de línea produce una suma de verificación diferente.
Sustituye el primer carácter de la suma de verificación por 0 y vuelve
a calcularla para ver Checksums do not match. Se aceptan caracteres hexadecimales
en mayúsculas y espacios en blanco al principio y al final; se rechazan una longitud incorrecta,
un carácter no hexadecimal o un nombre de archivo pegado. Deja el campo vacío para calcular un
hash sin afirmar que se ha verificado. Detén el servidor con Ctrl+C cuando termines.
Para una descarga real, obtén la suma de verificación esperada de una fuente en la que confíes, como la página autenticada de versiones de quien publica el archivo. Una coincidencia permite comprobar el archivo con respecto a ese valor; no determina quién lo publicó. Un atacante que pueda sustituir tanto el archivo como su suma de verificación puede lograr que la comparación coincida. Del mismo modo, ejecuta la herramienta de verificación desde una página en la que confíes, ya que su JavaScript controla el resultado. Las consideraciones de seguridad de Web Crypto explican por qué los scripts maliciosos comprometen las operaciones criptográficas.
Manejo de archivos grandes
crypto.subtle.digest() no acepta datos de entrada en streaming.
El archivo completo se carga en memoria antes de calcular su hash, y la
operación de cálculo del hash copia los datos de entrada,
así que prevé más memoria que el tamaño del archivo. Esta página rechaza los archivos de más de
50 MiB antes de leerlos. Ese es un límite de aplicación de ejemplo, no un límite del navegador
ni una garantía de que todos los dispositivos tengan suficiente memoria.
Calcular el hash de cada fragmento por separado y concatenar los resultados no produce el hash del archivo completo. Conservar únicamente el hash del último fragmento permite comprobar solo ese fragmento. Para entradas más grandes, elige una implementación de cálculo incremental de hashes o calcula el hash de un flujo de datos en el servidor. Trasladar este ejemplo a un Web Worker no elimina la necesidad de cargar el archivo completo en memoria.
Interpreta una discrepancia
Comprueba que seleccionaste el archivo correcto, copiaste únicamente el hash hexadecimal y elegiste el algoritmo de quien publica el archivo. Una discrepancia puede indicar una descarga truncada, una versión diferente o bytes modificados; la suma de verificación por sí sola no permite distinguir la causa. Calcula el hash del archivo descargado original, ya que abrirlo y guardarlo en otra aplicación puede modificar su contenido.
