Hacher des fichiers dans le navigateur avec Web Crypto
Pour hacher un fichier dans le navigateur, lisez ses octets avec file.arrayBuffer(), transmettez-les à
crypto.subtle.digest(), puis convertissez le résultat en hexadécimal. La page ci-dessous vous permet de
sélectionner un fichier local, de calculer sa somme de contrôle et, si vous le souhaitez, de la comparer à
celle fournie par l’éditeur. Elle lit le fichier localement, sans le téléverser.
Compatibilité des navigateurs et prérequis
Il vous faut un navigateur prenant en charge crypto.subtle et File.prototype.arrayBuffer. Web Crypto exige un
contexte sécurisé : utilisez HTTPS pour
héberger la page, ou une adresse de bouclage telle que http://127.0.0.1 pour le développement local.
L’exemple vérifie la présence des deux API avant d’activer le formulaire.
Enregistrez les deux fichiers ci-dessous dans le même répertoire. Aucun paquet JavaScript ni aucune étape de compilation n’est nécessaire. La commande qui sert les fichiers en local utilise un shell POSIX et Python 3 ; vous pouvez aussi utiliser un serveur de fichiers statiques existant.
Algorithmes de hachage pris en charge
Utilisez le même algorithme que celui de la somme de contrôle de l’éditeur. Cet exemple utilise SHA-256 par défaut et propose aussi SHA-384 et SHA-512. Leurs condensés hexadécimaux comportent respectivement 64, 96 et 128 caractères. L’API digest de Web Crypto prend aussi en charge SHA-1, mais celui-ci ne convient pas aux nouvelles vérifications sensibles en matière de sécurité en raison de faiblesses connues face aux collisions.
Implémentation du hachage de fichiers
Enregistrez ce fichier sous le nom index.html. Le script différé s’exécute une fois le formulaire
analysé. Les contrôles sont désactivés au départ afin que la page ne puisse pas être soumise si le
chargement du JavaScript échoue.
<!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>
Enregistrez ce fichier sous le nom 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() lit les octets bruts sans les décoder en texte. Le nom du fichier et le type MIME
n’entrent pas dans le condensé. La gestion des échecs est laissée à l’appelant. Une erreur de lecture
du fichier ne doit pas être signalée comme un algorithme non pris en charge : l’API File peut rejeter une lecture
lorsque le fichier sous-jacent change ou devient inaccessible.
Le formulaire verrouille ses champs pendant le hachage, puis les réactive en cas de réussite comme
d’échec. Modifier un champ efface le résultat précédent. Cela évite qu’une somme de contrôle semble
appartenir à un autre fichier ou à un autre algorithme. Le message d’attente n’affiche aucun
pourcentage, car digest() ne fournit aucun événement de progression.
Vérification de l’intégrité des fichiers
Dans un terminal, placez-vous dans le répertoire contenant index.html et hash.js, puis exécutez :
printf 'abc' > sample.txt
python3 -m http.server 8000 --bind 127.0.0.1
Ouvrez http://127.0.0.1:8000/, choisissez sample.txt, laissez SHA-256 sélectionné et collez cette somme de
contrôle attendue :
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
Cliquez sur Calculate checksum. Le résultat indique le nom du fichier, l’algorithme, le condensé calculé et Checksums match. La commande crée exactement trois octets, sans saut de ligne final. Un éditeur de texte qui ajoute un saut de ligne produit une somme de contrôle différente.
Remplacez le premier caractère de la somme de contrôle par 0 et relancez le calcul pour voir
Checksums do not match. Les valeurs hexadécimales en majuscules et les espaces en début ou en fin
de valeur sont acceptés ; une longueur incorrecte, un caractère non hexadécimal ou un nom de fichier
collé sont rejetés. Laissez le champ vide pour calculer un condensé sans prétendre qu’il a été vérifié.
Arrêtez le serveur avec Ctrl+C lorsque vous avez terminé.
Pour un véritable téléchargement, obtenez la somme de contrôle attendue auprès d’une source de confiance, comme la page de version authentifiée de l’éditeur. Une correspondance vérifie le fichier par rapport à cette valeur ; elle n’établit pas qui l’a publié. Un attaquant capable de remplacer à la fois le fichier et sa somme de contrôle peut faire réussir la comparaison. De même, exécutez le vérificateur depuis une page de confiance, car son JavaScript contrôle le résultat. Les considérations de sécurité de Web Crypto expliquent pourquoi des scripts malveillants compromettent les opérations cryptographiques.
Gestion des fichiers volumineux
crypto.subtle.digest() n’accepte pas d’entrée en flux.
Le fichier complet est lu en mémoire avant le hachage, et
l’opération digest copie son entrée ;
prévoyez donc davantage de mémoire que la taille du fichier. Cette page rejette les fichiers de plus
de 50 MiB avant de les lire. Il s’agit d’une limite de l’application d’exemple, et non d’une limite du
navigateur ni d’une garantie que chaque appareil dispose de suffisamment de mémoire.
Hacher des blocs séparément puis concaténer leurs condensés ne produit pas le condensé du fichier entier. Ne conserver que le condensé du dernier bloc ne vérifie que ce bloc. Pour des entrées plus volumineuses, choisissez une implémentation de hachage incrémentale ou hachez un flux côté serveur. Déplacer cet exemple dans un Web Worker ne supprime pas son besoin de charger le fichier entier en mémoire.
Interpréter une non-concordance
Vérifiez que vous avez sélectionné le bon fichier, copié uniquement le condensé hexadécimal et choisi l’algorithme de l’éditeur. Une non-concordance peut signifier un téléchargement tronqué, une version différente ou des octets modifiés ; la somme de contrôle seule ne permet pas de savoir de quel cas il s’agit. Hachez le fichier téléchargé d’origine, car l’ouvrir puis l’enregistrer dans une autre application peut modifier son contenu.
