logo hsb.horse
← Zur Blog-Übersicht

Blog

Byte-Streams im Browser mit Web Crypto verschlüsseln

Web Crypto bietet starke kryptografische Primitive, aber kein Stream-Interface. So verschlüsseln Sie ein ReadableStream<Uint8Array> mit dem encryption-Subpath von @hsblabs/web-stream-extras — inklusive Dateiverschlüsselung, streambezogenem Schlüsselmanagement und Pipeline-Komposition.

Veröffentlicht:

Die Web Crypto API stellt starke kryptografische Primitive bereit. Sie hat allerdings kein Interface für Streams: encrypt() und decrypt() arbeiten auf einem einzelnen ArrayBuffer, nicht auf einem ReadableStream.

Wenn große Dateien oder chunkweise Uploads verschlüsselt werden sollen, ohne alles im Speicher zu puffern, müsste man Framing- und Record-Splitting-Logik selbst schreiben. Dieser Artikel zeigt, wie man das mit dem encryption-Subpath von @hsblabs/web-stream-extras überspringen kann.

Terminal window
npm install @hsblabs/web-stream-extras

Läuft unter Node.js ≥22 oder in modernen Browsern mit Web Streams + Web Crypto.


Einen Stream verschlüsseln und entschlüsseln

encryptStream und decryptStream nehmen ein ReadableStream<Uint8Array> entgegen und geben denselben Typ zurück. Dadurch lassen sie sich ohne Adapter in bestehende Pipelines einbauen.

import {
readableFromChunks,
readAllBytes,
stringToBinary,
binaryToString,
} from "@hsblabs/web-stream-extras";
import {
encryptStream,
decryptStream,
} from "@hsblabs/web-stream-extras/encryption";
const key = crypto.getRandomValues(new Uint8Array(32));
const plaintext = readableFromChunks([stringToBinary("secret payload")]);
const encrypted = encryptStream(key, plaintext);
const decrypted = decryptStream(key, encrypted);
console.log(binaryToString(await readAllBytes(decrypted))); // "secret payload"

Auch der verschlüsselte Output ist ein normales ReadableStream<Uint8Array>. Er kann also direkt als fetch-Body verwendet, in die File System Access API geschrieben oder an jeden anderen Stream-Konsumenten weitergereicht werden.


Eine vom Nutzer ausgewählte Datei verschlüsseln

File.stream() liefert bereits ein ReadableStream<Uint8Array>, daher ist keine Umwandlung nötig:

async function encryptFile(file: File, key: Uint8Array): Promise<Uint8Array> {
const plaintext = file.stream() as ReadableStream<Uint8Array>;
const encrypted = encryptStream(key, plaintext);
return readAllBytes(encrypted);
}

Das Ergebnis ist einfach eine Bytefolge — zum Speichern, Hochladen oder Weiterreichen in einen anderen Stream.


Mit webCryptoStream streambezogene Schlüssel verwalten

In echten Anwendungen tauchen oft diese Anforderungen auf:

  1. Für jede Datei soll ein anderer Schlüssel verwendet werden
  2. Dieser Schlüssel soll zusammen mit dem Dateieintrag gespeichert werden
  3. Die Dateischlüssel sollen mit einem Master-Key geschützt werden, damit das rohe Schlüsselmaterial nie im Klartext gespeichert wird

webCryptoStream kapselt dieses Muster. Es nimmt einen nicht exportierbaren AES-GCM-CryptoKey entgegen und liefert einen Helper zurück, der streambezogene Schlüssel als Base64url-Strings erstellt und verwaltet.

import { webCryptoStream } from "@hsblabs/web-stream-extras/encryption";
const masterKey = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"],
);
const crypto$ = webCryptoStream(masterKey);

Datei verschlüsseln:

// streamKey ist ein Base64url-String — Speicherung zusammen mit dem Dateieintrag
const streamKey = await crypto$.createStreamKey();
const plaintext = file.stream() as ReadableStream<Uint8Array>;
const encrypted = await crypto$.encrypt(streamKey, plaintext);
const encryptedBytes = await readAllBytes(encrypted);
// Speicherung: { encryptedBytes, streamKey }

Später entschlüsseln:

const encrypted = readableFromChunks([encryptedBytes]);
const decrypted = await crypto$.decrypt(streamKey, encrypted);
const result = await readAllBytes(decrypted);

Der streamKey-String wird selbst mit dem Master-Key per AES-GCM verschlüsselt. Damit kann er sicher in einer Datenbank liegen, ohne dass rohes Schlüsselmaterial im Klartext auftaucht.


Was die Bibliothek übernimmt und was nicht

Intern erledigt sie:

  • Aufteilen des Klartext-Streams in Records fester Größe
  • Ableitung eines eindeutigen Schlüssels und Nonce pro Record via HKDF
  • Authentifizierung jedes Records mit AES-GCM und Erkennung von Manipulation beim Entschlüsseln

Nicht Teil der Bibliothek — das bleibt Aufgabe der Anwendung:

  • Schlüsselablage und Persistenz
  • Passwortbasierte Schlüsselableitung (PBKDF2 / Argon2 müssen selbst ergänzt werden)
  • Benutzer-Authentifizierung und Zugriffskontrolle

Die Bibliothek bleibt bewusst auf der Ebene von Stream-Transformationen.


Mit anderen Transforms kombinieren

Da Ein- und Ausgabe beide ReadableStream<Uint8Array> sind, lässt sich alles direkt zusammensetzen:

// Erst komprimieren, dann verschlüsseln
const compressed = plaintext.pipeThrough(new CompressionStream("gzip"));
const encrypted = encryptStream(key, compressed);
// Verschlüsselten Stream direkt hochladen
await fetch("/upload", {
method: "POST",
body: encrypted,
duplex: "half",
});

npm · GitHub