Tutoriel chiffrement côté client : construisez à partir de zéro
Tutoriel étape par étape pour implémenter le chiffrement côté client. Chiffrez les fichiers dans le navigateur avant qu'ils ne quittent l'appareil.
Le chiffrement de fichiers côté client dans le navigateur tient en environ 80 lignes de JavaScript en utilisant l'API Web Crypto. Le schéma : générer une clé AES-256-GCM dans le navigateur, chiffrer le fichier avec un nonce aléatoire de 96 bits, téléverser le texte chiffré via HTTPS/TLS 1.3, et partager l'URL résultante avec la clé intégrée dans l'identifiant de fragment (#key=...) que les navigateurs ne transmettent jamais aux serveurs. Le destinataire déchiffre dans son navigateur en utilisant ce même fragment. Ce tutoriel présente une implémentation fonctionnelle, incluant le découpage pour les gros fichiers, les clés dérivées d'un mot de passe via PBKDF2 à 600 000 itérations, et les pièges qui font échouer les premières tentatives.
L'architecture en un diagramme
[Navigateur expéditeur] [Serveur] [Navigateur destinataire]
Lire fichier → Clé AES (aléatoire) Accepte POST GET texte chiffré
Chiffrer avec AES-256-GCM Stocke blob chiffré Parse clé depuis URL #fragment
POST texte chiffré Pas de clé, pas de clair Déchiffre dans le navigateur
Construire URL avec #key=... Retourne URL de téléch. Sauvegarder fichier sur disque
Le serveur est un simple stockage d'objets binaires. Il ne voit que du texte chiffré et ne peut pas déchiffrer. La clé de déchiffrement vit dans le fragment d'URL, que les navigateurs traitent spécialement : il n'est jamais envoyé dans la ligne de requête HTTP. C'est le fondement de chaque service de transfert de fichiers zéro connaissance, y compris HexaTransfer.
Étape 1 : Générer une clé symétrique
async function generateKey() {
return await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true, // extractable pour pouvoir l'exporter dans l'URL
["encrypt", "decrypt"]
);
}
Le drapeau extractable: true est requis car nous devons sérialiser la clé dans un fragment d'URL. Si vous construisez un flux où la clé vit uniquement en mémoire (par exemple, un outil coller-et-envoyer), mettez-le à false.
Étape 2 : Lire le fichier comme ArrayBuffer
async function readFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(file);
});
}
Cela charge le fichier entier en mémoire. Convient pour les fichiers de moins de 500 Mo. Pour les fichiers plus volumineux, passez directement à la section streaming.
Étape 3 : Chiffrer le buffer
async function encryptFile(key, plaintext) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
plaintext
);
// Préfixer l'IV au texte chiffré pour que le destinataire puisse l'extraire
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
combined.set(iv, 0);
combined.set(new Uint8Array(ciphertext), iv.length);
return combined.buffer;
}
Le nonce (IV) est de 96 bits (12 octets), conformément à NIST SP 800-38D. Il n'est pas secret mais doit être unique par clé. Les nonces aléatoires sont sûrs ici car nous générons une clé fraîche par fichier. Préfixer l'IV au texte chiffré est une convention courante ; le destinataire le sépare avant le déchiffrement.
Étape 4 : Téléverser le texte chiffré
async function uploadCiphertext(ciphertext) {
const response = await fetch("/api/upload", {
method: "POST",
body: ciphertext,
headers: { "Content-Type": "application/octet-stream" },
});
const { fileId } = await response.json();
return fileId;
}
Le serveur reçoit un blob binaire, lui assigne un ID, le stocke, et retourne cet ID. Aucun en-tête ne révèle le nom du fichier, aucun paramètre de requête ne porte la clé. Si le disque dur du serveur est volé demain, un attaquant ne voit que du gibberish.
Étape 5 : Construire l'URL de partage avec la clé dans le fragment
async function buildShareURL(fileId, key) {
const rawKey = await crypto.subtle.exportKey("raw", key);
const keyBase64 = btoa(String.fromCharCode(...new Uint8Array(rawKey)))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
return `${location.origin}/f/${fileId}#${keyBase64}`;
}
L'encodage base64url (avec - et _ au lieu de + et /) évite les problèmes d'échappement d'URL. Le rembourrage = est supprimé pour l'esthétique.
Le fragment (#...) est la magie ici. Quand le destinataire charge l'URL, le navigateur garde le fragment côté client. La requête HTTP GET pour /f/{fileId} n'inclut pas #keyBase64 dans la ligne de requête, donc le serveur ne connaît jamais la clé. Vérifiez vous-même en ouvrant les outils de développement du navigateur sur n'importe quelle URL contenant un fragment et en observant l'onglet Réseau.
Étape 6 : Déchiffrement côté destinataire
async function downloadAndDecrypt() {
const fileId = location.pathname.split("/").pop();
const keyBase64 = location.hash.slice(1);
const rawKey = Uint8Array.from(
atob(keyBase64.replace(/-/g, "+").replace(/_/g, "/")),
c => c.charCodeAt(0)
);
const key = await crypto.subtle.importKey(
"raw", rawKey, "AES-GCM", false, ["decrypt"]
);
const response = await fetch(`/api/download/${fileId}`);
const combined = new Uint8Array(await response.arrayBuffer());
const iv = combined.slice(0, 12);
const ciphertext = combined.slice(12);
const plaintext = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv }, key, ciphertext
);
const blob = new Blob([plaintext]);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "fichier-telecharge";
a.click();
}
Le tag d'authentification de GCM est vérifié pendant decrypt(). Si le texte chiffré a été altéré, l'appel lève OperationError, un mode d'échec propre.
Clés dérivées d'un mot de passe via PBKDF2
Si les utilisateurs fournissent un mot de passe plutôt qu'une clé aléatoire, dérivez la clé AES via PBKDF2 :
async function deriveKey(password, salt) {
const passwordKey = await crypto.subtle.importKey(
"raw", new TextEncoder().encode(password),
"PBKDF2", false, ["deriveKey"]
);
return await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt,
iterations: 600000,
hash: "SHA-256",
},
passwordKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
}
600 000 itérations de PBKDF2-SHA-256 est la référence OWASP 2023. Le sel doit être 16 octets aléatoires et stocké avec le texte chiffré (pas secret, juste doit être unique). Pour du nouveau code, envisagez Argon2id via une bibliothèque comme argon2-browser — il résiste bien mieux aux attaques GPU que PBKDF2.
Streaming pour les gros fichiers
Les fichiers de plus de 500 Mo doivent être découpés. Lisez via File.stream(), chiffrez chaque morceau, téléversez séquentiellement :
async function encryptStream(file, key) {
const reader = file.stream().getReader();
const chunks = [];
let chunkIndex = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
const iv = new Uint8Array(12);
// Encoder l'index du morceau dans le nonce pour garantir l'unicité
new DataView(iv.buffer).setBigUint64(4, BigInt(chunkIndex++));
const ct = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv }, key, value
);
chunks.push({ iv, ct });
}
return chunks;
}
Dériver le nonce de l'index du morceau garantit l'unicité sans suivi d'état. Le réassemblage côté destinataire déchiffre les morceaux dans l'ordre et les concatène.
Pour un vrai AEAD en streaming, crypto_secretstream_xchacha20poly1305 de libsodium via libsodium.js est plus propre et détecte les attaques de troncation. Web Crypto n'a pas de primitive équivalente en 2026.
Tests et pièges
Erreurs courantes à éviter :
- Utiliser
Math.random()pour les clés ou nonces : utilisez toujourscrypto.getRandomValues(). - Réutiliser un nonce avec la même clé : casse la sécurité GCM. Les clés aléatoires par fichier rendent cela sûr ; les flux par morceau nécessitent des nonces uniques par morceau.
- Ne pas vérifier HTTPS :
crypto.subtleest indéfini sur les origines non sécurisées. Testez sur localhost ou avec un certificat auto-signé pendant le développement. - Stocker des clés dans
localStorage: tout XSS sur votre origine peut le lire. Utilisez plutôt le schéma de fragment d'URL, ou des clés non-extractables. - Oublier d'inclure l'IV avec le texte chiffré : le déchiffrement échoue sans message d'erreur utile. Préfixez ou sérialisez toujours ensemble.
- Mal gérer le fragment : ne postez pas accidentellement l'URL (avec fragment) vers un service tiers. Partagez uniquement via des canaux de bout en bout si le fragment est sensible.
Responsabilités côté serveur
Le rôle du serveur dans une architecture de chiffrement côté client est minimal : accepter le POST, stocker le blob, retourner l'ID, servir le GET pour le blob, supprimer à expiration. Pas de crypto. Ce que le serveur doit faire au-delà du stockage :
- Appliquer des limites de taille de fichier (prévenir les abus)
- Limiter le taux des téléversements et téléchargements
- Définir une rétention courte (7 jours est une valeur par défaut raisonnable, comme HexaTransfer)
- Journaliser uniquement ce qui est nécessaire (horodatage de téléversement, pas d'IP si privacy-first)
- Servir sur TLS 1.3 avec HSTS
- En-têtes CORS restreignant les origines si l'API est appelée uniquement depuis vos domaines
Tout assembler
Une application minimale fonctionnelle tient dans un seul fichier HTML plus un backend Express de 50 lignes. Dépendances totales : aucune côté client (Web Crypto est natif), Express plus multer côté serveur. Le chiffrement est aussi fort que la primitive AES-256-GCM car c'est littéralement ce que vous utilisez. Il n'y a pas d'algorithme secret à mal implémenter, seulement des primitives à utiliser correctement.
Les parties les plus difficiles sont les cas limites : gros fichiers, flux mot de passe vers clé, UX du destinataire quand le déchiffrement échoue, gestion des liens expirés avec élégance. La cryptographie de base est directe.
Essayez sur hexatransfer.com — gratuit, sans compte, jusqu'à 10 Go.
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