Chiffrement de fichiers en JavaScript : tutoriel pas à pas
Chiffrez des fichiers dans le navigateur avec JavaScript : tutoriel pratique sur le chiffrement AES, la dérivation de clé et le traitement sécurisé.
JavaScript peut chiffrer un fichier entièrement dans le navigateur via la Web Crypto API, sans aucune intervention du serveur. Le pipeline standard : lisez le fichier comme un ArrayBuffer, dérivez une clé AES-GCM de 256 bits à partir d'un mot de passe via PBKDF2-SHA256 (600 000 itérations), chiffrez avec un IV de 12 octets aléatoires, et conditionnez le sel, l'IV et le texte chiffré dans un Blob téléchargeable. Chaque navigateur moderne prend en charge cela nativement via window.crypto.subtle, et pour des fichiers jusqu'à 2 à 3 Go l'ensemble s'exécute en moins de dix secondes sur un ordinateur portable moderne sans toucher une seule bibliothèque tierce. Pour les organisations soumises au RGPD, ce pattern signifie que même votre hébergeur — comme votre équipe technique — ne peut pas lire les fichiers des utilisateurs.
Les choix d'algorithmes qui comptent
Choisissez AES-GCM, pas AES-CBC. GCM vous donne le chiffrement authentifié en un seul passage, détectant la falsification avec un tag de 128 bits, tandis que CBC nécessite une étape HMAC séparée que la plupart des tutoriels implémentent mal. Utilisez une clé de 256 bits — l'écart de performances avec 128 bits est négligeable sur le matériel avec AES-NI. Choisissez PBKDF2-SHA256 pour la dérivation de clé basée sur un mot de passe sauf si vous pouvez livrer Argon2id via WebAssembly, qui est plus solide mais ajoute 50 Ko de téléchargement.
Évitez : le mode ECB (fondamentalement cassé), le rembourrage fait main (une décennie d'attaques CBC padding oracle), MD5 ou SHA-1 (brisés par collision), et tout ce qui vient du package crypto-js sans comprendre qu'il fait du CBC avec PKCS7 par défaut.
Lire un fichier en mémoire
L'API File vous donne trois façons d'obtenir des octets :
const buf = await file.arrayBuffer(); // fichier entier
const stream = file.stream(); // en streaming
const text = await file.text(); // décodé UTF-8
Pour les fichiers au-dessus de 500 Mo, arrayBuffer() échoue souvent sur Mobile Safari. Streamez plutôt :
async function* chunks(file, size = 4 * 1024 * 1024) {
for (let off = 0; off < file.size; off += size) {
yield new Uint8Array(await file.slice(off, off + size).arrayBuffer());
}
}
Chaque slice lit paresseusement depuis le disque, donc la mémoire de pointe reste bornée.
Dériver une clé à partir du mot de passe utilisateur
Ne passez jamais un mot de passe brut à encrypt. Dérivez d'abord une clé :
async function deriveKey(password, salt) {
const enc = new TextEncoder();
const material = await crypto.subtle.importKey(
'raw', enc.encode(password), { name: 'PBKDF2' }, false, ['deriveKey']
);
return crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: 600000, hash: 'SHA-256' },
material,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
Générez un sel frais de 16 octets par fichier avec crypto.getRandomValues(new Uint8Array(16)). Stockez le sel avec le texte chiffré — réutiliser un sel sur plusieurs fichiers annule l'intérêt de PBKDF2. La fiche de référence de stockage des mots de passe OWASP de décembre 2026 recommande actuellement 600 000 itérations pour PBKDF2-SHA256, ce qui se traduit par environ 500 ms de dérivation de clé sur un téléphone moyen.
Chiffrer le fichier
Avec une clé en main, le chiffrement est un seul appel subtle.encrypt par tampon :
async function encryptBuffer(key, plaintext) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv, tagLength: 128 },
key,
plaintext
);
return { iv, ciphertext };
}
GCM échoue catastrophiquement si vous réutilisez une paire (iv, clé) — la collision du keystream révèle les deux textes en clair. Un IV aléatoire de 96 bits donne environ 2^48 chiffrements sûrs avec une clé, ce qui est correct pour le chiffrement de fichiers. Si vous chiffrez beaucoup de chunks avec la même clé, dérivez l'IV à partir d'un compteur plus un préfixe aléatoire de 32 bits.
Conditionner la sortie
Le déchiffreur a besoin du sel, de l'IV et du texte chiffré. Conditionnez-les dans un seul blob avec un petit en-tête :
function pack(salt, iv, ciphertext) {
const magic = new TextEncoder().encode('ENC1');
return new Blob([magic, salt, iv, new Uint8Array(ciphertext)]);
}
Versionnez l'en-tête (ENC1, ENC2…) pour pouvoir migrer les algorithmes plus tard sans casser les anciens fichiers. Proposez un téléchargement via :
const url = URL.createObjectURL(packed);
const a = document.createElement('a');
a.href = url; a.download = `${file.name}.enc`; a.click();
URL.revokeObjectURL(url);
Déchiffrer les fichiers
Le déchiffrement inverse le processus et lance OperationError si le mot de passe est incorrect ou si le fichier a été altéré :
async function decryptFile(blob, password) {
const buf = await blob.arrayBuffer();
const view = new Uint8Array(buf);
const magic = new TextDecoder().decode(view.slice(0, 4));
if (magic !== 'ENC1') throw new Error('Format inconnu');
const salt = view.slice(4, 20);
const iv = view.slice(20, 32);
const ct = view.slice(32);
const key = await deriveKey(password, salt);
const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct);
return new Blob([pt]);
}
Affichez un seul message d'erreur — « impossible de déchiffrer, mauvais mot de passe ou fichier corrompu » — plutôt que de distinguer entre l'échec du tag et l'échec structurel. Cela supprime la fuite d'information analogue à l'oracle de rembourrage vers les attaquants.
Chiffrer les grands fichiers sans manquer de RAM
Pour tout ce qui dépasse 500 Mo, n'appelez pas arrayBuffer() sur le fichier entier. Chiffrez des chunks indépendamment avec des IV uniques dérivés d'un compteur :
function ivForChunk(baseIV, index) {
const iv = new Uint8Array(baseIV);
const view = new DataView(iv.buffer);
view.setUint32(8, index, false);
return iv;
}
Faites passer le fichier à travers un TransformStream, chiffrez chaque chunk de 4 Mo, et écrivez les résultats dans un WritableStream pointant vers le disque via l'API File System Access. La mémoire de pointe reste près de 10 Mo même pour un fichier de 20 Go. Le format chunké doit enregistrer la taille et le nombre de chunks dans son en-tête pour que le déchiffreur puisse réassembler correctement.
Erreurs classiques qui arrivent en production
Trois schémas apparaissent dans les vraies revues de code crypto JavaScript :
Premièrement, stocker la clé brute dans localStorage par commodité. localStorage est synchrone, limité à l'origine, et lisible par n'importe quel XSS. Utilisez plutôt une CryptoKey non-extractible dans IndexedDB.
Deuxièmement, utiliser Math.random() pour les IV ou les sels. Math.random() est prévisible ; utilisez toujours crypto.getRandomValues.
Troisièmement, supposer que subtle.encrypt est à temps constant. C'est le cas dans les implémentations natives des navigateurs, mais tout wrapper JavaScript autour de celui-ci presque certainement pas. Gardez votre propre code hors du chemin critique.
HexaTransfer applique exactement ce pipeline — PBKDF2 à 600 000 itérations, AES-256-GCM, chunks streamés — pour que le serveur ne stocke que du texte chiffré opaque.
Tester l'implémentation
Écrivez un harnais de test qui effectue un aller-retour sur un blob aléatoire de 10 Mo, mute un octet, et vérifie que le déchiffrement lance une erreur. Ajoutez du fuzzing sur les en-têtes malformés et les textes chiffrés tronqués — le bug courant est l'absence de vérification des bornes sur slice() qui plante plutôt que de rejeter. Évaluez les performances sur un iPhone SE, un Android moyen et un Chromebook ; tout ce qui prend plus de 2 secondes de dérivation de clé est trop lent pour les utilisateurs mobiles. Faites auditer par une seconde paire d'yeux avant de passer en production — le code crypto semble simple et se casse de façon subtile que les tests manquent.
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