Web Crypto API : tutoriel complet pour le chiffrement de fichiers
Maîtrisez la Web Crypto API pour le chiffrement de fichiers côté navigateur : AES-GCM, RSA-OAEP et gestion des clés en JavaScript.
La Web Crypto API permet de chiffrer des fichiers directement dans le navigateur via les méthodes natives de window.crypto.subtle, sans aucune bibliothèque externe. Pour le chiffrement de fichiers, le pipeline standard consiste à dériver une clé à partir d'un mot de passe via PBKDF2 (210 000 itérations, SHA-256), puis à chiffrer les octets du fichier avec AES-256-GCM en utilisant un IV de 96 bits et un tag d'authentification de 128 bits. Les workflows à clé publique utilisent RSA-OAEP avec des clés de 4096 bits pour envelopper la clé symétrique. L'API est disponible sur HTTPS dans chaque navigateur moderne et s'exécute dans le backend crypto natif plutôt qu'en JavaScript pur — un point particulièrement important pour la conformité RGPD, car le serveur ne voit jamais le texte en clair.
Pourquoi SubtleCrypto surpasse les bibliothèques JavaScript pures
window.crypto.subtle appelle le backend crypto natif audité du navigateur, généralement BoringSSL dans Chromium ou CommonCrypto sur Safari. Par rapport aux options JavaScript pures comme CryptoJS ou sjcl, SubtleCrypto est 30 à 80 fois plus rapide pour AES-256-GCM, évite les canaux auxiliaires temporels dans les interpréteurs JavaScript, et n'ajoute aucun octet au téléchargement. Le compromis est une API basée sur des Promises qui n'opère que sur des objets ArrayBuffer et CryptoKey, ce qui implique beaucoup de conversions entre Uint8Array, Blob et ReadableStream. Pour les fichiers de plus de 100 Mo, cette mécanique compte plus que la vitesse brute de chiffrement.
Dériver une clé à partir d'un mot de passe avec PBKDF2
N'utilisez jamais un mot de passe directement comme clé AES. Importez d'abord le mot de passe comme matériel brut, puis dérivez une clé de 256 bits :
async function deriveKey(password, salt) {
const enc = new TextEncoder();
const material = await crypto.subtle.importKey(
'raw', enc.encode(password), 'PBKDF2', false, ['deriveKey']
);
return crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: 210000, hash: 'SHA-256' },
material,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
Les recommandations OWASP 2026 préconisent au moins 600 000 itérations avec SHA-256, bien que 210 000 reste acceptable pour les contextes à faible risque. Générez un sel frais de 16 octets par fichier avec crypto.getRandomValues et stockez-le avec le texte chiffré. Argon2id serait plus solide mais n'est pas encore exposé par SubtleCrypto.
Chiffrer un fichier avec AES-GCM
AES-GCM vous donne confidentialité et authenticité en un seul passage. La règle critique : ne réutilisez jamais une paire (clé, IV) :
async function encryptFile(file, key) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const plaintext = await file.arrayBuffer();
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv, tagLength: 128 },
key,
plaintext
);
return { iv, ciphertext };
}
Pour un fichier de 2 Go, file.arrayBuffer() allouera tout le tampon, ce qui fait souvent planter Mobile Safari. Découpez le fichier en chunks de 4 Mo, chiffrez chacun avec un IV unique dérivé d'un compteur concaténé à un préfixe aléatoire, et préfixez un octet de version et le sel pour que le déchiffreur sache à quoi il a affaire.
Traitement en flux de grands fichiers via TransformStream
Pour éviter l'explosion mémoire, enveloppez le chiffrement dans un TransformStream et faites passer le fichier à travers :
const chunkSize = 4 * 1024 * 1024;
const encryptor = new TransformStream({
async transform(chunk, controller) {
const iv = nextIV(counter++);
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, chunk);
controller.enqueue(new Uint8Array([...iv, ...new Uint8Array(ct)]));
}
});
await file.stream()
.pipeThrough(sliceByChunks(chunkSize))
.pipeThrough(encryptor)
.pipeTo(uploadSink);
file.stream() retourne un ReadableStream<Uint8Array> qui lit depuis le disque de manière paresseuse. Le découpeur produit des chunks de taille fixe pour que les tags GCM s'alignent de façon prévisible. La mémoire de pointe reste sous 20 Mo même pour un upload de 10 Go.
Envelopper la clé symétrique avec RSA-OAEP
Quand vous devez partager un fichier avec un destinataire spécifique, générez sa paire de clés RSA une fois et publiez la clé publique :
const keypair = await crypto.subtle.generateKey(
{ name: 'RSA-OAEP', modulusLength: 4096,
publicExponent: new Uint8Array([1,0,1]), hash: 'SHA-256' },
true, ['wrapKey', 'unwrapKey']
);
Générez une clé AES-GCM pour le fichier, puis enveloppez-la :
const wrapped = await crypto.subtle.wrapKey(
'raw', fileKey, keypair.publicKey,
{ name: 'RSA-OAEP' }
);
Les clés RSA de 4096 bits offrent environ 150 bits de sécurité jusqu'en 2030 selon NIST SP 800-57. Si vous avez besoin de la confidentialité persistante ou d'une résistance post-quantique, associez RSA-OAEP à ECDH sur P-384 ou migrez vers ML-KEM (Kyber) une fois que le groupe de travail WebCrypto l'aura intégré.
Stocker les clés en sécurité dans IndexedDB
Les objets CryptoKey sont non-extractibles par défaut, ce qui signifie que vous pouvez les persister dans IndexedDB sans jamais exposer les octets bruts à JavaScript :
const db = await openDB('keystore', 1);
await db.put('keys', keypair.privateKey, 'user-signing-key');
Les navigateurs sérialisent la clé via l'algorithme de clone structuré et gardent les octets réels dans le backend crypto. Un script compromis peut appeler encrypt ou decrypt avec la clé stockée mais ne peut pas en lire le matériel. C'est une mesure de durcissement significative par rapport au stockage de clés en base64 dans localStorage.
Gérer les erreurs que l'API lance
SubtleCrypto lance OperationError pour les échecs de déchiffrement authentifié — ce qui signifie généralement que le texte chiffré a été altéré, que l'IV est incorrect, ou que l'utilisateur a tapé le mauvais mot de passe. Il lance DataError quand le tampon d'entrée a la mauvaise longueur, NotSupportedError quand l'algorithme n'est pas implémenté, et InvalidAccessError quand la clé n'a pas été importée avec les bons indicateurs d'usage. Enveloppez toujours le déchiffrement dans try/catch, affichez un message neutre « le fichier n'a pas pu être déchiffré », et évitez de révéler si le tag ou la structure a échoué.
Pièges concrets à éviter
Firefox sur Android plafonne les itérations deriveKey à environ 1 million avant que le thread UI ne se fige pendant plusieurs secondes — exécutez la dérivation de clé dans un Worker dédié. Safari sous 16.4 ne prend pas en charge crypto.subtle.verify avec le rembourrage PSS. Chrome plafonne les appels getRandomValues au-dessus de 64 Ko par invocation, donc bouclez si vous avez besoin de plus d'entropie. Et les transferts ArrayBuffer via postMessage sont sans copie mais détachent l'original, ce qui surprend souvent les développeurs.
HexaTransfer utilise exactement ce pipeline AES-GCM + PBKDF2 pour chaque upload, avec les clés dérivées dans un Worker et le texte chiffré streamé vers le stockage sans que le serveur ne voie jamais le texte en clair.
Assembler le tout
Un flux d'upload chiffré minimal : générez sel et IV avec getRandomValues, dérivez une clé AES-GCM à partir du mot de passe de l'utilisateur via PBKDF2, faites passer le fichier à travers un TransformStream qui chiffre chaque chunk de 4 Mo, préfixez un petit en-tête contenant la version, le sel et le nombre de chunks, puis envoyez le résultat à votre serveur. Au téléchargement, inversez le processus chunk par chunk en capturant OperationError comme signal de mauvais mot de passe ou de corruption. La Web Crypto API vous fournit tout ce dont vous avez besoin, et l'implémentation native du navigateur surpassera toute alternative JavaScript d'un ordre de grandeur.
Essayez-le sur https://hexatransfer.com — gratuit, sans compte, 10 Go maximum.
Envoyez vos fichiers volumineux en toute sécurité avec le chiffrement de bout en bout
Transférez des fichiers jusqu'à 10 Go gratuitement avec le chiffrement de bout en bout. Aucun compte requis. Vos fichiers sont chiffrés dans votre navigateur avant l'envoi — personne d'autre ne peut les lire.
Envoyer un fichier