L’API Web Crypto fournit des primitives cryptographiques puissantes. En revanche, elle n’a pas d’interface orientée flux : encrypt() et decrypt() travaillent sur un ArrayBuffer unique, pas sur un ReadableStream.
Si vous voulez chiffrer de gros fichiers ou des uploads découpés en chunks sans tout mettre en mémoire, il faut normalement écrire vous-même la logique de framing et de découpe en enregistrements. Cet article montre comment éviter cela avec le sous-chemin encryption de @hsblabs/web-stream-extras.
npm install @hsblabs/web-stream-extrasCela fonctionne sur Node.js ≥22, ou dans les navigateurs modernes compatibles Web Streams + Web Crypto.
Chiffrer et déchiffrer un flux
encryptStream et decryptStream prennent et retournent un ReadableStream<Uint8Array>, donc ils s’insèrent dans un pipeline existant sans adaptateur.
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"Le résultat chiffré reste un ReadableStream<Uint8Array> standard. Vous pouvez donc le brancher sur le body d’un fetch, l’écrire via File System Access API ou le passer à n’importe quel consommateur de flux.
Chiffrer un fichier choisi par l’utilisateur
File.stream() retourne déjà un ReadableStream<Uint8Array>, donc il n’y a aucune conversion à faire :
async function encryptFile(file: File, key: Uint8Array): Promise<Uint8Array> { const plaintext = file.stream() as ReadableStream<Uint8Array>; const encrypted = encryptStream(key, plaintext); return readAllBytes(encrypted);}Le résultat n’est qu’un tableau d’octets : libre à vous de l’enregistrer, de l’uploader ou de le repasser dans un autre flux.
Gérer des clés par flux avec webCryptoStream
Dans une vraie application, les besoins ressemblent souvent à ceci :
- utiliser une clé de chiffrement différente par fichier
- stocker cette clé à côté de l’enregistrement du fichier
- protéger chaque clé de fichier avec une clé maître afin que la matière de clé brute n’apparaisse jamais en clair
webCryptoStream encapsule ce modèle. Il prend une CryptoKey AES-GCM non extractible et renvoie un helper capable de créer et gérer des clés par flux sous forme de chaînes base64url.
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);Chiffrer un fichier :
// streamKey est une chaîne base64url — à stocker avec l'enregistrement du fichierconst streamKey = await crypto$.createStreamKey();
const plaintext = file.stream() as ReadableStream<Uint8Array>;const encrypted = await crypto$.encrypt(streamKey, plaintext);const encryptedBytes = await readAllBytes(encrypted);
// Stockage : { encryptedBytes, streamKey }Le déchiffrer plus tard :
const encrypted = readableFromChunks([encryptedBytes]);const decrypted = await crypto$.decrypt(streamKey, encrypted);const result = await readAllBytes(decrypted);La chaîne streamKey est elle-même chiffrée avec la clé maître en AES-GCM. Elle peut donc être stockée en base sans exposer la matière de clé brute en clair.
Ce que la bibliothèque fait et ne fait pas
Ce qu’elle prend en charge en interne :
- découper un flux en clair en enregistrements de taille fixe
- dériver une clé et un nonce uniques par enregistrement via HKDF
- authentifier chaque enregistrement avec AES-GCM et détecter les altérations au déchiffrement
Ce qu’elle ne prend pas en charge — cela reste à la charge de l’application :
- le stockage et la persistance des clés
- la dérivation de clé à partir d’un mot de passe (PBKDF2 / Argon2 doivent être ajoutés par vos soins)
- l’authentification utilisateur et le contrôle d’accès
La bibliothèque reste volontairement au niveau de la transformation de flux.
Composition avec d’autres transforms
Comme l’entrée et la sortie sont toutes deux des ReadableStream<Uint8Array>, la composition reste directe :
// Compresser puis chiffrerconst compressed = plaintext.pipeThrough(new CompressionStream("gzip"));const encrypted = encryptStream(key, compressed);// Uploader directement le flux chiffréawait fetch("/upload", { method: "POST", body: encrypted, duplex: "half",});
hsb.horse