Dateien im Browser mit Web Crypto hashen
Um eine Datei im Browser zu hashen, lesen Sie ihre Bytes mit file.arrayBuffer(),
übergeben sie an crypto.subtle.digest() und wandeln das Ergebnis in eine hexadezimale
Darstellung um. Auf der folgenden Seite können Sie eine lokale Datei auswählen, ihre Prüfsumme
berechnen und diese optional mit einer vom Herausgeber bereitgestellten Prüfsumme vergleichen.
Die Seite liest die Datei lokal, ohne sie hochzuladen.
Browserkompatibilität und Voraussetzungen
Sie benötigen einen Browser mit crypto.subtle und File.prototype.arrayBuffer.
Web Crypto erfordert einen sicheren Kontext: Verwenden Sie HTTPS,
wenn Sie die Seite hosten, oder eine Loopback-Adresse wie http://127.0.0.1 für die
lokale Entwicklung. Das Beispiel prüft beide APIs, bevor es das Formular aktiviert.
Speichern Sie die beiden folgenden Dateien im selben Verzeichnis. JavaScript-Pakete oder ein Build-Schritt sind nicht erforderlich. Der Befehl zum lokalen Bereitstellen nutzt eine POSIX-Shell und Python 3; Sie können auch einen vorhandenen Server für statische Dateien verwenden.
Unterstützte Hash-Algorithmen
Verwenden Sie denselben Algorithmus wie für die Prüfsumme des Herausgebers. Dieses Beispiel nutzt standardmäßig SHA-256 und bietet außerdem SHA-384 und SHA-512 an. Ihre hexadezimalen Digests enthalten 64, 96 beziehungsweise 128 Zeichen. Die Web Crypto Digest-API unterstützt auch SHA-1. Dieser Algorithmus ist jedoch aufgrund bekannter Kollisionsschwächen für neue sicherheitsrelevante Prüfungen ungeeignet.
Datei-Hashing implementieren
Speichern Sie dies als index.html. Das verzögert geladene Skript wird ausgeführt,
nachdem das Formular geparst wurde. Die Bedienelemente sind anfangs deaktiviert, damit die Seite das
Formular nicht absenden kann, falls JavaScript nicht geladen wird.
<!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>
Speichern Sie dies als 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() liest die unverarbeiteten Bytes, ohne sie als Text zu decodieren.
Dateiname und MIME-Typ fließen nicht in den Digest ein. Die Fehlerbehandlung bleibt dem aufrufenden
Code überlassen. Ein Fehler beim Lesen der Datei darf nicht als nicht unterstützter Algorithmus
gemeldet werden: Die File API kann einen Lesevorgang ablehnen,
wenn sich die zugrunde liegende Datei ändert oder nicht mehr zugänglich ist.
Das Formular sperrt seine Eingabefelder während des Hashings und aktiviert sie anschließend wieder,
unabhängig davon, ob der Vorgang erfolgreich war oder fehlgeschlagen ist. Wenn Sie eine Eingabe
ändern, wird das vorherige Ergebnis gelöscht. So erscheint eine Prüfsumme nicht als Ergebnis für
eine andere Datei oder einen anderen Algorithmus. Die Statusmeldung enthält keine Prozentangabe,
da digest() keine Fortschrittsereignisse liefert.
Dateiintegrität verifizieren
Wechseln Sie in einem Terminal in das Verzeichnis mit index.html und
hash.js und führen Sie dann Folgendes aus:
printf 'abc' > sample.txt
python3 -m http.server 8000 --bind 127.0.0.1
Öffnen Sie http://127.0.0.1:8000/, wählen Sie sample.txt, lassen Sie
SHA-256 ausgewählt und fügen Sie diese erwartete Prüfsumme ein:
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
Klicken Sie auf Calculate checksum. Das Ergebnis enthält den Dateinamen, den Algorithmus, den berechneten Digest und Checksums match. Der Befehl erzeugt genau drei Bytes ohne abschließenden Zeilenumbruch. Ein Texteditor, der einen Zeilenumbruch hinzufügt, erzeugt eine andere Prüfsumme.
Ersetzen Sie das erste Zeichen der Prüfsumme durch 0 und berechnen Sie
sie erneut, um Checksums do not match. zu sehen. Hexadezimalzeichen in Großbuchstaben
sowie umgebende Leerzeichen werden akzeptiert; eine falsche Länge, ein nicht hexadezimales Zeichen
oder ein eingefügter Dateiname werden abgelehnt. Lassen Sie das Feld leer, um einen Digest zu
berechnen, ohne ihn als verifiziert auszuweisen. Beenden Sie den Server anschließend mit Ctrl+C.
Beziehen Sie bei einem tatsächlichen Download die erwartete Prüfsumme aus einer Quelle, der Sie vertrauen, etwa von der authentifizierten Veröffentlichungsseite des Herausgebers. Eine Übereinstimmung prüft die Datei anhand dieses Werts; sie belegt nicht, wer die Datei veröffentlicht hat. Ein Angreifer, der sowohl die Datei als auch ihre Prüfsumme ersetzen kann, kann den Vergleich erfolgreich ausfallen lassen. Führen Sie das Prüfwerkzeug ebenfalls auf einer Seite aus, der Sie vertrauen, da deren JavaScript das Ergebnis bestimmt. Die Sicherheitshinweise zu Web Crypto erklären, warum bösartige Skripte kryptografische Operationen untergraben.
Große Dateien verarbeiten
crypto.subtle.digest() unterstützt keine Streaming-Eingabe.
Die vollständige Datei wird vor dem Hashing in den Arbeitsspeicher eingelesen, und die
Digest-Operation kopiert ihre Eingabe. Planen Sie daher mehr
Arbeitsspeicher ein, als die Datei groß ist. Diese Seite lehnt Dateien über 50 MiB ab, bevor sie
sie liest. Dies ist eine beispielhafte Anwendungsgrenze, keine Browsergrenze und keine Garantie,
dass jedes Gerät über ausreichend Arbeitsspeicher verfügt.
Wenn Sie Chunks einzeln hashen und ihre Digests aneinanderhängen, ergibt das nicht den Digest der gesamten Datei. Behalten Sie nur den Digest des letzten Chunks, prüfen Sie damit nur diesen Chunk. Wählen Sie für größere Eingaben eine Implementierung für inkrementelles Hashing oder hashen Sie einen Stream auf dem Server. Dieses Beispiel in einen Web Worker zu verlagern beseitigt seinen Speicherbedarf für die vollständige Datei nicht.
Abweichungen interpretieren
Prüfen Sie, ob Sie die gewünschte Datei ausgewählt, nur den hexadezimalen Digest kopiert und den Algorithmus des Herausgebers gewählt haben. Eine Abweichung kann auf einen unvollständigen Download, eine andere Veröffentlichung oder geänderte Bytes hinweisen; die Prüfsumme allein verrät nicht, welcher Fall vorliegt. Hashen Sie die ursprünglich heruntergeladene Datei, da das Öffnen und Speichern in einer anderen Anwendung ihren Inhalt verändern kann.
