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.
npm install @hsblabs/web-stream-extrasLä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:
- Für jede Datei soll ein anderer Schlüssel verwendet werden
- Dieser Schlüssel soll zusammen mit dem Dateieintrag gespeichert werden
- 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 Dateieintragconst 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üsselnconst compressed = plaintext.pipeThrough(new CompressionStream("gzip"));const encrypted = encryptStream(key, compressed);// Verschlüsselten Stream direkt hochladenawait fetch("/upload", { method: "POST", body: encrypted, duplex: "half",});
hsb.horse