logo hsb.horse
← Retour au blog

Blog

Chiffrer des flux d'octets dans le navigateur avec Web Crypto

Web Crypto fournit de puissantes primitives de chiffrement, mais pas d'interface orientée flux. Voici comment chiffrer un ReadableStream<Uint8Array> avec le sous-chemin encryption de @hsblabs/web-stream-extras — chiffrement de fichiers, gestion de clés par flux et composition dans des pipelines.

Publié:

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.

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

Cela 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 :

  1. utiliser une clé de chiffrement différente par fichier
  2. stocker cette clé à côté de l’enregistrement du fichier
  3. 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 fichier
const 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 chiffrer
const 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",
});

npm · GitHub