Zum Inhalt springen
HexaTransfer
Zurück zum Blog
Technische Vertiefungen

JavaScript-Dateiverschlüsselung: Schritt-für-Schritt-Tutorial

Verschlüsseln Sie Dateien im Browser mit JavaScript: Praxis-Tutorial zu AES-Verschlüsselung, Schlüsselableitung und sicherer Dateiverarbeitung.

JavaScript kann eine Datei vollständig im Browser verschlüsseln — ohne Server-Beteiligung und ohne externe Bibliothek. Das BSI stuft AES-256-GCM als zugelassenes Verfahren ein; dieselbe Kryptographie steht seit 2017 nativ über window.crypto.subtle in jedem modernen Browser bereit. Die Standard-Pipeline: die Datei als ArrayBuffer lesen, einen 256-Bit-AES-GCM-Schlüssel aus einem Passwort via PBKDF2-SHA256 ableiten (600.000 Iterationen), mit einem zufälligen 12-Byte-IV verschlüsseln und Salt, IV und Ciphertext in ein herunterladbares Blob verpacken. Für Dateien bis 2–3 GB läuft das in unter zehn Sekunden auf einem modernen Laptop.

Die Algorithmuswahl, die zählt

AES-GCM wählen, nicht AES-CBC. GCM liefert authentifizierte Verschlüsselung in einem Durchgang und erkennt Manipulation mit einem 128-Bit-Tag, während CBC einen separaten HMAC-Schritt benötigt, den die meisten Tutorials falsch implementieren. Einen 256-Bit-Schlüssel verwenden — der Performance-Unterschied gegenüber 128-Bit ist auf Hardware mit AES-NI vernachlässigbar. PBKDF2-SHA256 für passwortbasierte Schlüsselableitung wählen, es sei denn, man kann Argon2id via WebAssembly liefern — das wäre stärker, fügt aber 50 KB Download-Gewicht hinzu.

Folgendes vermeiden: ECB-Modus (fundamental gebrochen), handgestrickte Padding (ein Jahrzehnt CBC-Padding-Oracle-Angriffe), MD5 oder SHA-1 (kollisionsgebrochen) und alles aus dem crypto-js-Package ohne zu verstehen, dass es CBC mit PKCS7 standardmäßig nutzt.

Eine Datei in den Speicher lesen

Die File-API bietet drei Wege zu Bytes:

const buf = await file.arrayBuffer();   // gesamte Datei
const stream = file.stream();           // Streaming
const text = await file.text();         // UTF-8 dekodiert

Für Dateien über 500 MB schlägt arrayBuffer() auf Mobile Safari oft fehl. Stattdessen streamen:

async function* chunks(file, size = 4 * 1024 * 1024) {
  for (let off = 0; off < file.size; off += size) {
    yield new Uint8Array(await file.slice(off, off + size).arrayBuffer());
  }
}

Jedes Slice liest faul von der Disk, sodass der Peak-Speicher begrenzt bleibt.

Einen Schlüssel aus einem Nutzerpasswort ableiten

Niemals ein rohes Passwort in encrypt übergeben. Zuerst einen Schlüssel ableiten:

async function deriveKey(password, salt) {
  const enc = new TextEncoder();
  const material = await crypto.subtle.importKey(
    'raw', enc.encode(password), { name: 'PBKDF2' }, false, ['deriveKey']
  );
  return crypto.subtle.deriveKey(
    { name: 'PBKDF2', salt, iterations: 600000, hash: 'SHA-256' },
    material,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );
}

Einen frischen 16-Byte-Salt pro Datei mit crypto.getRandomValues(new Uint8Array(16)) generieren. Den Salt neben dem Ciphertext speichern — einen Salt über Dateien hinweg wiederzuverwenden macht PBKDF2 wirkungslos. OWASPs Dezember-2026-Passwort-Cheat-Sheet empfiehlt aktuell 600.000 Iterationen für PBKDF2-SHA256, was auf einem Mid-Range-Telefon etwa 500 ms Schlüsselableitung bedeutet.

Die Datei verschlüsseln

Mit einem Schlüssel in der Hand ist Verschlüsselung ein subtle.encrypt-Aufruf pro Buffer:

async function encryptBuffer(key, plaintext) {
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const ciphertext = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv, tagLength: 128 },
    key,
    plaintext
  );
  return { iv, ciphertext };
}

GCM schlägt katastrophal fehl, wenn man ein (iv, key)-Paar wiederholt — die Keystream-Kollision leakt beide Plaintexte. Ein 96-Bit-zufälliger IV gibt ~2^48 sichere Verschlüsselungen mit einem Schlüssel — gut für Dateiverschlüsselung. Wenn viele Chunks mit demselben Schlüssel verschlüsselt werden, den IV aus einem Zähler plus einem zufälligen 32-Bit-Präfix ableiten.

Die Ausgabe verpacken

Der Entschlüsseler benötigt Salt, IV und Ciphertext. In ein einzelnes Blob mit einem kleinen Header packen:

function pack(salt, iv, ciphertext) {
  const magic = new TextEncoder().encode('ENC1');
  return new Blob([magic, salt, iv, new Uint8Array(ciphertext)]);
}

Den Header versionieren (ENC1, ENC2…), sodass Algorithmen später migriert werden können, ohne alte Dateien zu brechen. Download anbieten via:

const url = URL.createObjectURL(packed);
const a = document.createElement('a');
a.href = url; a.download = `${file.name}.enc`; a.click();
URL.revokeObjectURL(url);

Dateien entschlüsseln

Entschlüsselung kehrt den Prozess um und wirft OperationError, wenn das Passwort falsch oder die Datei manipuliert ist:

async function decryptFile(blob, password) {
  const buf = await blob.arrayBuffer();
  const view = new Uint8Array(buf);
  const magic = new TextDecoder().decode(view.slice(0, 4));
  if (magic !== 'ENC1') throw new Error('Unbekanntes Format');
  const salt = view.slice(4, 20);
  const iv = view.slice(20, 32);
  const ct = view.slice(32);
  const key = await deriveKey(password, salt);
  const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct);
  return new Blob([pt]);
}

Eine einzelne Fehlermeldung anzeigen — „Datei konnte nicht entschlüsselt werden, falsches Passwort oder beschädigte Datei" — statt zwischen Tag-Fehler und Strukturfehler zu unterscheiden. Das entfernt einen Padding-Oracle-ähnlichen Informationsleck gegenüber Angreifern.

Große Dateien ohne RAM-Erschöpfung verschlüsseln

Für alles über 500 MB arrayBuffer() auf der gesamten Datei nicht aufrufen. Chunks unabhängig mit eindeutigen IVs verschlüsseln, die aus einem Zähler abgeleitet werden:

function ivForChunk(baseIV, index) {
  const iv = new Uint8Array(baseIV);
  const view = new DataView(iv.buffer);
  view.setUint32(8, index, false);
  return iv;
}

Die Datei durch einen TransformStream leiten, jeden 4-MB-Chunk verschlüsseln und die Ergebnisse in einen WritableStream schreiben, der via File System Access API auf Disk zeigt. Peak-Speicher bleibt nahe 10 MB selbst für eine 20-GB-Datei.

Häufige Fehler, die in Produktion landen

Drei Patterns tauchen in realen Code-Reviews von JavaScript-Krypto auf:

Erstens: den rohen Schlüssel in localStorage für Bequemlichkeit speichern. localStorage ist synchron, Origin-scoped und von jedem XSS lesbar. Stattdessen einen nicht extrahierbaren CryptoKey in IndexedDB verwenden.

Zweitens: Math.random() für IVs oder Salts verwenden. Math.random() ist vorhersehbar; immer crypto.getRandomValues verwenden.

Drittens: annehmen, dass subtle.encrypt constant-time ist. In nativen Browser-Implementierungen ist es das, aber jeder JavaScript-Wrapper darum fast sicher nicht. Eigenen Code aus dem Hot Path heraushalten.

HexaTransfer wendet genau diese Pipeline an — PBKDF2 bei 600.000 Iterationen, AES-256-GCM, gestreamte Chunks — sodass der Server nur opaques Ciphertext speichert. Ende-zu-Ende-Verschlüsselung ohne Kompromisse, DSGVO-konform nach Art. 32 durch technische Schutzmaßnahmen.

Jetzt ausprobieren: https://hexatransfer.com — kostenlos, kein Account, bis zu 10 GB.

Große Dateien sicher mit Ende-zu-Ende-Verschlüsselung senden

Übertragen Sie Dateien bis zu 10 GB kostenlos mit Ende-zu-Ende-Verschlüsselung. Kein Konto erforderlich. Ihre Dateien werden in Ihrem Browser verschlüsselt, bevor sie hochgeladen werden — niemand sonst kann sie lesen.

Datei senden