Zum Inhalt springen
HexaTransfer
Zurück zum Blog
Verschlusselung & Sicherheit

Clientseitige Verschlüsselung Tutorial: von Grund auf bauen

Schritt-für-Schritt-Tutorial für clientseitige Verschlüsselung in Web-Apps. Dateien im Browser verschlüsseln bevor sie das Gerät verlassen.

Clientseitige Dateiverschlüsselung im Browser erfordert etwa 80 Zeilen JavaScript mit der Web Crypto API. Das Muster: einen AES-256-GCM-Schlüssel im Browser generieren, die Datei mit einem zufälligen 96-Bit-Nonce verschlüsseln, den Chiffretext über HTTPS/TLS 1.3 hochladen und die resultierende URL mit dem Schlüssel im Fragment-Identifier (#key=...) teilen, den Browser nie an Server übertragen. Der Empfänger entschlüsselt im Browser über dasselbe Fragment. Dieses Tutorial führt durch eine funktionierende Implementierung, einschließlich Chunking für große Dateien, passwortabgeleitete Schlüssel via PBKDF2 mit 600.000 Iterationen und die Fallstricke, die erste Versuche scheitern lassen.

Die Architektur auf einen Blick

[Sender-Browser]                       [Server]                      [Empfänger-Browser]
  Datei lesen → AES-Schlüssel          POST entgegennehmen             GET Chiffretext
  Mit AES-256-GCM verschlüsseln        Chiffretext speichern           Schlüssel aus URL-#Fragment lesen
  POST Chiffretext                     Kein Schlüssel, kein Klartext   Im Browser entschlüsseln
  URL mit #key=... bauen               Download-URL zurückgeben        Datei auf Disk speichern

Der Server ist ein einfacher Blob-Speicher. Er sieht nur Chiffretext und kann nicht entschlüsseln. Der Entschlüsselungsschlüssel lebt im URL-Fragment, das Browser besonders behandeln: Es wird nie in der HTTP-Anforderungszeile gesendet. Das ist das Fundament jedes Zero-Knowledge-Dateiübertragungsdienstes einschließlich HexaTransfer.

Schritt 1: Einen symmetrischen Schlüssel generieren

async function generateKey() {
  return await crypto.subtle.generateKey(
    { name: "AES-GCM", length: 256 },
    true, // extrahierbar, damit wir ihn in die URL exportieren können
    ["encrypt", "decrypt"]
  );
}

Das extractable: true-Flag ist erforderlich, weil der Schlüssel in ein URL-Fragment serialisiert werden muss. Bei einem Flow, bei dem der Schlüssel nur im Speicher lebt (z. B. ein Paste-and-Send-Tool), auf false setzen.

Schritt 2: Datei als ArrayBuffer lesen

async function readFile(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = () => reject(reader.error);
    reader.readAsArrayBuffer(file);
  });
}

Das lädt die gesamte Datei in den Speicher. Gut für Dateien unter 500 MB. Für größere Dateien direkt zum Streaming-Abschnitt springen.

Schritt 3: Den Buffer verschlüsseln

async function encryptFile(key, plaintext) {
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const ciphertext = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv },
    key,
    plaintext
  );
  // IV dem Chiffretext voranstellen, damit der Empfänger ihn extrahieren kann
  const combined = new Uint8Array(iv.length + ciphertext.byteLength);
  combined.set(iv, 0);
  combined.set(new Uint8Array(ciphertext), iv.length);
  return combined.buffer;
}

Der Nonce (IV) ist 96 Bit (12 Bytes) gemäß NIST SP 800-38D. Er ist kein Geheimnis, muss aber pro Schlüssel eindeutig sein. Zufällige Nonces sind hier sicher, weil ein frischer Schlüssel pro Datei generiert wird. Den IV dem Chiffretext voranzustellen ist eine gängige Konvention; der Empfänger trennt ihn vor der Entschlüsselung wieder ab.

Schritt 4: Den Chiffretext hochladen

async function uploadCiphertext(ciphertext) {
  const response = await fetch("/api/upload", {
    method: "POST",
    body: ciphertext,
    headers: { "Content-Type": "application/octet-stream" },
  });
  const { fileId } = await response.json();
  return fileId;
}

Der Server empfängt einen Binär-Blob, weist ihm eine ID zu, speichert ihn und gibt diese ID zurück. Keine Header verraten den Dateinamen, keine Query-Parameter tragen den Schlüssel. Wenn die Festplatte des Servers morgen gestohlen wird, sieht ein Angreifer nur Kauderwelsch.

Schritt 5: Die Share-URL mit dem Schlüssel im Fragment bauen

async function buildShareURL(fileId, key) {
  const rawKey = await crypto.subtle.exportKey("raw", key);
  const keyBase64 = btoa(String.fromCharCode(...new Uint8Array(rawKey)))
    .replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
  return `${location.origin}/f/${fileId}#${keyBase64}`;
}

Base64url-Kodierung (mit - und _ statt + und /) vermeidet URL-Escaping-Probleme. Das =-Padding wird für eine saubere Darstellung entfernt.

Das Fragment (#...) ist hier der entscheidende Trick. Wenn der Empfänger die URL lädt, behält der Browser das Fragment clientseitig. Das HTTP-GET für /f/{fileId} enthält #keyBase64 nicht in der Anforderungszeile, sodass der Server den Schlüssel nie erfährt. Das im Browser-Entwicklertools-Netzwerk-Tab bei jeder Fragment-enthaltenden URL nachverfolgen.

Schritt 6: Empfängerseitige Entschlüsselung

async function downloadAndDecrypt() {
  const fileId = location.pathname.split("/").pop();
  const keyBase64 = location.hash.slice(1);
  const rawKey = Uint8Array.from(
    atob(keyBase64.replace(/-/g, "+").replace(/_/g, "/")),
    c => c.charCodeAt(0)
  );
  const key = await crypto.subtle.importKey(
    "raw", rawKey, "AES-GCM", false, ["decrypt"]
  );
  const response = await fetch(`/api/download/${fileId}`);
  const combined = new Uint8Array(await response.arrayBuffer());
  const iv = combined.slice(0, 12);
  const ciphertext = combined.slice(12);
  const plaintext = await crypto.subtle.decrypt(
    { name: "AES-GCM", iv }, key, ciphertext
  );
  const blob = new Blob([plaintext]);
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = "heruntergeladene-datei";
  a.click();
}

GCMs Authentifizierungs-Tag wird während decrypt() geprüft. Wenn der Chiffretext manipuliert wurde, wirft der Aufruf OperationError — ein sauberer Fehlermodus.

Passwortabgeleitete Schlüssel via PBKDF2

Wenn Nutzende ein Passwort anstelle eines zufälligen Schlüssels angeben, den AES-Schlüssel via PBKDF2 ableiten:

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

600.000 Iterationen PBKDF2-SHA-256 ist OWASPs Baseline 2023. Das Salt muss 16 zufällige Bytes sein und zusammen mit dem Chiffretext gespeichert werden (kein Geheimnis, muss aber eindeutig sein). Für neuen Code Argon2id via eine Bibliothek wie argon2-browser in Betracht ziehen — es widersteht GPU-Angriffen wesentlich besser als PBKDF2.

Streaming für große Dateien

Dateien über 500 MB sollten ge-chunked werden. Via File.stream() lesen, jeden Chunk verschlüsseln, sequenziell hochladen:

async function encryptStream(file, key) {
  const reader = file.stream().getReader();
  const chunks = [];
  let chunkIndex = 0;
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    const iv = new Uint8Array(12);
    // Chunk-Index in den Nonce kodieren, um Eindeutigkeit zu garantieren
    new DataView(iv.buffer).setBigUint64(4, BigInt(chunkIndex++));
    const ct = await crypto.subtle.encrypt(
      { name: "AES-GCM", iv }, key, value
    );
    chunks.push({ iv, ct });
  }
  return chunks;
}

Den Nonce aus dem Chunk-Index abzuleiten garantiert Eindeutigkeit ohne State-Tracking. Beim Empfänger werden Chunks der Reihe nach entschlüsselt und verkettet.

Für echtes Streaming-AEAD ist libsodiums crypto_secretstream_xchacha20poly1305 via libsodium.js sauberer und erkennt Abschneideangriffe. Web Crypto hat 2026 kein vergleichbares Primitiv.

Tests und häufige Fehler

Häufige Fehler, die es zu vermeiden gilt:

  • Math.random() für Schlüssel oder Nonces verwenden: immer crypto.getRandomValues().
  • Nonce mit demselben Schlüssel wiederverwenden: bricht GCM-Sicherheit. Per-File-Zufallsschlüssel machen das sicher; Per-Chunk-Flows brauchen eindeutige Nonces pro Chunk.
  • Kein HTTPS: crypto.subtle ist auf unsicheren Ursprüngen undefined. Auf localhost oder mit selbstsigniertem Zertifikat während der Entwicklung testen.
  • Schlüssel in localStorage speichern: jedes XSS im Ursprung kann es lesen. Stattdessen das URL-Fragment-Muster verwenden oder nicht-extrahierbare Schlüssel.
  • IV vergessen, mit dem Chiffretext einzuschließen: Entschlüsselung schlägt ohne nützliche Fehlermeldung fehl. Immer voranstellen oder separat serialisieren.
  • Das Fragment falsch handhaben: die URL (mit Fragment) nicht versehentlich an einen Drittanbieterdienst posten. Nur über Ende-zu-Ende-Kanäle teilen, wenn das Fragment sensibel ist.

Serveraufgaben

Die Aufgaben des Servers in einer clientseitig verschlüsselnden Architektur sind gering: POST entgegennehmen, Blob speichern, ID zurückgeben, GET für den Blob bereitstellen, bei Ablauf löschen. Keine Kryptografie. Was der Server über Speicherung hinaus tun sollte:

  • Dateigrößenbeschränkungen durchsetzen (Missbrauch verhindern)
  • Uploads und Downloads ratenbegrenzen
  • Kurze Aufbewahrungsfristen setzen (7 Tage ist ein vernünftiger Standard, wie bei HexaTransfer)
  • Nur Notwendiges protokollieren (Upload-Zeitstempel, keine IPs bei Privacy-First)
  • Über TLS 1.3 mit HSTS ausliefern
  • CORS-Header, die Ursprünge einschränken, wenn die API nur von eigenen Domains aufgerufen wird

Alles zusammenführen

Eine minimale funktionierende App passt in eine HTML-Datei plus ein 50-zeiliges Express-Backend. Gesamte Abhängigkeiten: keine auf dem Client (Web Crypto ist nativ), Express plus multer auf dem Server. Die Verschlüsselung ist so stark wie das AES-256-GCM-Primitiv — weil das buchstäblich das ist, was verwendet wird. Es gibt keinen geheimen Algorithmus, der falsch gemacht werden könnte, nur die Primitive, die korrekt eingesetzt werden müssen.

Die schwierigsten Teile sind die Grenzfälle: große Dateien, Passwort-zu-Schlüssel-Flows, Empfänger-UX bei fehlgeschlagener Entschlüsselung, sauberer Umgang mit abgelaufenen Links. Die Kernkryptografie ist unkompliziert.

Auf hexatransfer.com testen — kostenlos, ohne Konto, bis 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