Gere hashes de arquivos no navegador com Web Crypto
Para gerar o hash de um arquivo no navegador, leia os bytes dele com file.arrayBuffer(), passe-os para
crypto.subtle.digest() e converta o resultado para hexadecimal. A página abaixo permite selecionar um
arquivo local, calcular o checksum dele e, opcionalmente, compará-lo com um checksum fornecido pelo
publicador. Ela lê o arquivo localmente, sem fazer upload dele.
Compatibilidade e requisitos do navegador
Você precisa de um navegador com crypto.subtle e File.prototype.arrayBuffer. A Web Crypto exige um
contexto seguro: use HTTPS ao hospedar a página
ou um endereço de loopback como http://127.0.0.1 para desenvolvimento local. O exemplo verifica as
duas APIs antes de habilitar o formulário.
Salve os dois arquivos abaixo no mesmo diretório. Não é preciso nenhum pacote JavaScript nem etapa de build. O comando para servir os arquivos localmente usa um shell POSIX e Python 3; você também pode usar um servidor de arquivos estáticos que já tenha.
Algoritmos de hash suportados
Use o mesmo algoritmo do checksum do publicador. Este exemplo usa SHA-256 por padrão e também oferece SHA-384 e SHA-512. Os digests hexadecimais deles têm 64, 96 e 128 caracteres, respectivamente. A API de digest da Web Crypto também suporta SHA-1, mas ele não é adequado para novas verificações sensíveis à segurança, por causa de fraquezas de colisão conhecidas.
Como implementar o hash de arquivos
Salve isto como index.html. O script com defer é executado depois que o formulário é analisado.
Os controles começam desabilitados para que a página não possa ser enviada se o JavaScript não
carregar.
<!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>
Salve isto 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() lê os bytes brutos sem decodificá-los como texto. O nome do arquivo e o tipo MIME
não entram no digest. A função deixa o tratamento de falhas para quem a chama. Um erro de leitura
do arquivo não deve ser relatado como algoritmo não suportado: a
File API pode rejeitar uma leitura quando o arquivo subjacente muda ou fica
inacessível.
O formulário bloqueia os campos durante o cálculo do hash e os habilita novamente em caso de sucesso
ou falha. Alterar um campo limpa o resultado anterior. Isso evita que um checksum pareça pertencer a
outro arquivo ou algoritmo. A mensagem de processamento não mostra porcentagem porque
digest() não fornece eventos de progresso.
Como verificar a integridade do arquivo
Em um terminal, vá para o diretório que contém index.html e hash.js e execute:
printf 'abc' > sample.txt
python3 -m http.server 8000 --bind 127.0.0.1
Abra http://127.0.0.1:8000/, escolha sample.txt, mantenha SHA-256 selecionado e cole este checksum
esperado:
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
Clique em Calculate checksum. O resultado inclui o nome do arquivo, o algoritmo, o digest calculado e Checksums match. O comando cria exatamente três bytes, sem quebra de linha no final. Um editor de texto que adicione uma quebra de linha produz um checksum diferente.
Substitua o primeiro caractere do checksum por 0 e calcule novamente para ver
Checksums do not match. Hex em maiúsculas e espaços em branco ao redor são aceitos; um
comprimento errado, um caractere não hexadecimal ou um nome de arquivo colado são rejeitados. Deixe
o campo vazio para calcular um digest sem afirmar que ele foi verificado. Pare o servidor com
Ctrl+C ao terminar.
Para um download real, obtenha o checksum esperado de uma fonte em que você confie, como a página de release autenticada do publicador. Quando os checksums coincidem, o arquivo foi verificado em relação a esse valor; isso não comprova quem o publicou. Um invasor capaz de substituir tanto o arquivo quanto o checksum pode fazer a comparação ser bem-sucedida. Da mesma forma, execute o verificador a partir de uma página em que você confie, já que o JavaScript dela controla o resultado. As considerações de segurança da Web Crypto explicam por que scripts hostis comprometem operações criptográficas.
Como lidar com arquivos grandes
crypto.subtle.digest() não aceita entrada em streaming.
O arquivo completo é lido na memória antes do cálculo do hash, e a
operação de digest copia a entrada,
então reserve mais memória do que o tamanho do arquivo. Esta página rejeita arquivos acima de
50 MiB antes de lê-los. Esse é um limite da aplicação de exemplo, não um limite do navegador nem uma
garantia de que todo dispositivo tenha memória suficiente.
Gerar o hash de blocos separadamente e concatenar os digests deles não produz o digest do arquivo inteiro. Manter apenas o digest do último bloco verifica apenas esse bloco. Para entradas maiores, escolha uma implementação de hash incremental ou gere o hash de um stream no servidor. Mover este exemplo para um Web Worker não elimina a exigência de memória para o arquivo inteiro.
Como interpretar uma divergência
Verifique se você selecionou o arquivo pretendido, copiou apenas o digest hexadecimal e escolheu o algoritmo do publicador. Uma divergência pode indicar um download truncado, uma release diferente ou bytes alterados; o checksum sozinho não diz qual é o caso. Gere o hash do arquivo original baixado, já que abri-lo e salvá-lo em outro aplicativo pode alterar o conteúdo dele.
