Chiffrer des données avant même qu’elles ne quittent le navigateur de l’utilisateur change la donne pour la confidentialité. Plutôt que de faire confiance à un serveur distant pour protéger des fichiers sensibles, le code tourne directement dans la machine du visiteur, via une interface standardisée que presque aucun développeur français n’exploite encore à son plein potentiel : l’API Web Crypto. Ce tutoriel construit un projet complet, de zéro, avec du JavaScript natif, sans bibliothèque externe, sans serveur Node.js et sans npm install.
À la fin de ce guide, vous aurez une mini-application web qui chiffre un fichier avec AES-256-GCM, dérive la clé d’un mot de passe avec PBKDF2, signe le résultat avec ECDSA P-256 et calcule une empreinte SHA-256 pour vérifier l’intégrité. Le tout tourne dans Chrome, Firefox, Edge ou Safari, sans transmettre la clé ni le mot de passe à qui que ce soit. Treize étapes, environ 75 minutes, et un code que vous pourrez réutiliser dans vos propres projets.
Pourquoi chiffrer côté navigateur avec l’API Web Crypto
L’API Web Crypto (interface SubtleCrypto, accessible via window.crypto.subtle) est une spécification du W3C qui expose des primitives cryptographiques directement dans le moteur JavaScript du navigateur. Le document de référence, Web Cryptography API Level 2, reste au stade d’Editor’s Draft daté du 11 août 2026, ce qui signifie que certaines extensions évoluent encore, mais le socle de l’API (AES-GCM, ECDSA, PBKDF2, SHA-256) est stable et implémenté depuis plusieurs années dans tous les navigateurs majeurs.
L’intérêt pour une audience française et européenne est direct. Le RGPD pousse vers la minimisation des données : si un service ne reçoit jamais la clé de déchiffrement, une fuite côté serveur ne révèle rien d’exploitable. Des outils comme ProtonMail, Bitwarden ou Signal Desktop s’appuient sur des variantes de ce principe pour le chiffrement de bout en bout. Construire sa propre brique de chiffrement côté client permet de comprendre exactement ce qui se passe sous le capot, plutôt que de dépendre d’une bibliothèque tierce dont on ne maîtrise pas le code.
Deux algorithmes font tout le travail dans ce tutoriel. AES-256-GCM assure la confidentialité et l’intégrité en un seul passage grâce à son tag d’authentification. ECDSA sur courbe P-256 ajoute une signature numérique qui prouve l’origine des données, un complément utile quand plusieurs utilisateurs échangent des fichiers chiffrés et doivent vérifier qui les a réellement produits.
Cas d’usage concrets pour une équipe produit
Avant de rentrer dans le code, il vaut la peine de lister quelques scénarios réels où cette approche change l’architecture d’un produit. Un éditeur de notes chiffrées type Standard Notes ou Joplin chiffre chaque note sur l’appareil de l’utilisateur avant synchronisation, de sorte que le serveur ne stocke que des blobs illisibles. Un outil de partage de documents juridiques entre un cabinet d’avocats et son client peut chiffrer le PDF dans le navigateur avant upload, avec une clé dérivée d’un mot de passe partagé hors bande, par téléphone par exemple.
Un formulaire de collecte de données médicales ou financières, soumis à des obligations renforcées sous le RGPD, peut chiffrer chaque champ sensible côté client avant la soumission du formulaire, de façon à ce que même une base de données mal configurée ou un dump accidentel ne révèle rien d’exploitable sans la clé. Dans tous ces cas, le principe reste identique : réduire ce que le serveur voit, sans renoncer à une expérience utilisateur fluide dans un navigateur standard.
Prérequis : navigateurs, contexte sécurisé et outils
Aucune installation lourde n’est nécessaire. Voici ce qu’il faut avant de commencer.
- Un navigateur récent : Chrome, Firefox, Edge ou Safari en version actuelle. Chrome a activé Ed25519 par défaut dans
crypto.subtledepuis la version 137 (mi-2025), un bon repère pour juger de la maturité de l’API sur ce moteur. - Un éditeur de code (VS Code, Zed, ou même un éditeur de texte simple).
- Un serveur HTTP local. Python 3 (déjà installé sur la plupart des systèmes) ou la commande
npx servesuffisent. - Un contexte sécurisé :
crypto.subtlen’est accessible que sur HTTPS ou surlocalhost. Ouvrir le fichier HTML directement en double-cliquant (protocolefile://) ne fonctionnera pas dans la plupart des navigateurs. - Des bases en JavaScript asynchrone : toutes les méthodes de
SubtleCryptoretournent desPromise, doncasync/awaitsera utilisé partout.
Comptez environ 75 minutes pour suivre les treize étapes et tester chaque brique. Le projet final tient dans trois fichiers : une page HTML, une feuille de style minimale et un script JavaScript d’environ 250 lignes.
Étape 1 : Initialiser le projet et lancer un serveur local
Créez un dossier chiffrement-navigateur contenant trois fichiers : index.html, style.css et crypto.js. La structure HTML de base reste volontairement simple, elle servira de support à l’interface construite plus tard.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Chiffrement navigateur - API Web Crypto</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Démo Web Crypto API</h1>
<div id="app"></div>
<script src="crypto.js"></script>
</body>
</html>
Lancez ensuite un serveur local depuis ce dossier. Sous Python 3 :
python3 -m http.server 8000
# puis ouvrez http://localhost:8000 dans le navigateur
Le protocole http://localhost est traité comme un contexte sécurisé par les navigateurs, au même titre que HTTPS. C’est la seule raison d’être de ce serveur à ce stade : sans lui, crypto.subtle restera inaccessible.
Étape 2 : Vérifier la disponibilité de crypto.subtle
Avant d’écrire la moindre ligne de logique métier, ajoutez une vérification défensive. Elle évite des erreurs cryptiques plus tard et affiche un message clair si le contexte n’est pas sécurisé.
function verifierSupportCrypto() {
if (!window.isSecureContext) {
throw new Error("Contexte non sécurisé : ouvrez la page via HTTPS ou localhost.");
}
if (!window.crypto || !window.crypto.subtle) {
throw new Error("L'API Web Crypto (crypto.subtle) n'est pas disponible dans ce navigateur.");
}
console.log("API Web Crypto disponible.");
}
verifierSupportCrypto();
Cette fonction protège aussi contre un cas fréquent en entreprise : un intranet encore servi en HTTP simple, où crypto.subtle vaudra undefined sans qu’aucune erreur explicite ne soit levée par défaut.
Étape 3 : Générer un sel et dériver une clé avec PBKDF2
Un mot de passe humain n’est jamais directement utilisable comme clé AES. Il faut le faire passer par une fonction de dérivation. PBKDF2 est le choix natif de l’API Web Crypto (Argon2id, plus robuste face au GPU, n’y est pas implémenté nativement et demanderait une bibliothèque WebAssembly séparée).
async function deriverCleDepuisMotDePasse(motDePasse, sel) {
const encodeur = new TextEncoder();
const cleBase = await crypto.subtle.importKey(
"raw",
encodeur.encode(motDePasse),
"PBKDF2",
false,
["deriveKey"]
);
return crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: sel,
iterations: 310000, // recommandation OWASP 2026 pour PBKDF2-HMAC-SHA256
hash: "SHA-256"
},
cleBase,
{ name: "AES-GCM", length: 256 },
false, // la clé dérivée n'est pas exportable
["encrypt", "decrypt"]
);
}
const sel = crypto.getRandomValues(new Uint8Array(16));
Le choix de 310000 itérations suit la recommandation publique de l’OWASP pour PBKDF2-HMAC-SHA256 en 2026. Le sel, lui, doit être généré aléatoirement à chaque chiffrement et stocké en clair avec les données chiffrées : il ne protège pas un secret, il empêche la réutilisation de tables précalculées contre plusieurs utilisateurs partageant un mot de passe identique.
Étape 4 : Chiffrer des données avec AES-256-GCM
AES-GCM combine confidentialité et intégrité dans une seule opération. Contrairement à AES-CBC, il n’exige pas de bourrage (padding) manuel et produit un tag d’authentification qui détecte toute modification des données chiffrées.
async function chiffrerTexte(texteClair, cle) {
const encodeur = new TextEncoder();
const iv = crypto.getRandomValues(new Uint8Array(12)); // 96 bits, taille recommandée par NIST SP 800-38D
const donneesChiffrees = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv },
cle,
encodeur.encode(texteClair)
);
return { iv, donneesChiffrees };
}
Le vecteur d’initialisation (IV) fait 12 octets, la taille que recommande le NIST dans SP 800-38D pour AES-GCM. Il n’a pas besoin d’être secret, mais il ne doit jamais être réutilisé avec la même clé : c’est le piège numéro un de ce tutoriel, détaillé plus bas.
Étape 5 : Déchiffrer les données et gérer les erreurs
Le déchiffrement suit la même logique en sens inverse. L’API lève une exception OperationError si le tag d’authentification ne correspond pas, ce qui signale soit une mauvaise clé, soit des données corrompues ou modifiées.
async function dechiffrerTexte(donneesChiffrees, cle, iv) {
try {
const decodeur = new TextDecoder();
const donneesClaires = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: iv },
cle,
donneesChiffrees
);
return decodeur.decode(donneesClaires);
} catch (erreur) {
if (erreur.name === "OperationError") {
throw new Error("Déchiffrement impossible : mauvais mot de passe ou données altérées.");
}
throw erreur;
}
}
Cette gestion d’erreur transforme un message technique opaque en information utile pour l’utilisateur final, sans révéler si le problème vient du mot de passe ou d’une corruption des données, ce qui éviterait de donner des indices à un attaquant qui testerait plusieurs mots de passe.
Étape 6 : Générer une paire de clés ECDSA P-256
La signature numérique répond à un besoin différent du chiffrement : prouver qui a produit un fichier, pas le garder secret. ECDSA sur la courbe P-256 reste le choix le mieux supporté nativement dans crypto.subtle, avant même Ed25519 qui n’a été activé par défaut que plus récemment (Chrome 137).
async function genererPaireDeCles() {
return crypto.subtle.generateKey(
{
name: "ECDSA",
namedCurve: "P-256"
},
true, // clés exportables, nécessaire pour partager la clé publique
["sign", "verify"]
);
}
const paireDeCles = await genererPaireDeCles();
// paireDeCles.privateKey et paireDeCles.publicKey
Le paramètre extractable (ici true) détermine si la clé pourra sortir du moteur cryptographique du navigateur via exportKey. Pour la clé privée d’une application réelle de gestion de secrets, ce paramètre devrait rester à false afin que même un script malveillant injecté dans la page ne puisse jamais en extraire la valeur brute.
Étape 7 : Signer les données chiffrées
Signer les données déjà chiffrées (plutôt que le texte en clair) garantit que la signature couvre exactement ce qui sera transmis ou stocké, IV compris si on l’inclut dans le message signé.
async function signerDonnees(donnees, clePrivee) {
const signature = await crypto.subtle.sign(
{ name: "ECDSA", hash: "SHA-256" },
clePrivee,
donnees
);
return new Uint8Array(signature);
}
La signature ECDSA P-256 avec SHA-256 produit un résultat de 64 octets. Elle n’ajoute pas de confidentialité supplémentaire, uniquement une preuve d’origine vérifiable par n’importe qui disposant de la clé publique correspondante.
Étape 8 : Vérifier une signature numérique
La vérification est symétrique à la signature, mais utilise la clé publique au lieu de la clé privée. Elle retourne un simple booléen, sans lever d’exception en cas d’échec.
async function verifierSignature(donnees, signature, clePublique) {
const estValide = await crypto.subtle.verify(
{ name: "ECDSA", hash: "SHA-256" },
clePublique,
signature,
donnees
);
return estValide; // true ou false
}
À ce stade, le projet dispose déjà des trois briques essentielles : dérivation de clé, chiffrement authentifié et signature. L’étape suivante ajoute une vérification d’intégrité indépendante, utile quand on veut comparer un fichier téléchargé à une empreinte publiée séparément.
Étape 9 : Calculer une empreinte SHA-256
async function calculerEmpreinte(buffer) {
const hachage = await crypto.subtle.digest("SHA-256", buffer);
const octets = Array.from(new Uint8Array(hachage));
return octets.map(o => o.toString(16).padStart(2, "0")).join("");
}
// Exemple d'utilisation sur un fichier
const empreinte = await calculerEmpreinte(await monFichier.arrayBuffer());
console.log(empreinte);
// Sortie attendue : une chaîne hexadécimale de 64 caractères
// ex. 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
Cette empreinte sert de double vérification : même si un attaquant parvenait à falsifier un fichier sans casser la signature (par exemple en remplaçant complètement la paire de clés), comparer le SHA-256 publié à celui recalculé côté client ajoute une couche de contrôle supplémentaire, économique en calcul.
Étape 10 : Stocker des clés non extractibles avec IndexedDB
Un détail méconnu de l’API Web Crypto : les objets CryptoKey peuvent être stockés directement dans IndexedDB grâce à l’algorithme de clonage structuré, même quand ils ne sont pas exportables. Cela permet de persister une clé entre deux sessions sans jamais exposer sa valeur brute en JavaScript.
function ouvrirBaseCles() {
return new Promise((resoudre, rejeter) => {
const requete = indexedDB.open("coffre-cles", 1);
requete.onupgradeneeded = () => {
requete.result.createObjectStore("cles");
};
requete.onsuccess = () => resoudre(requete.result);
requete.onerror = () => rejeter(requete.error);
});
}
async function sauvegarderCle(cle, identifiant) {
const base = await ouvrirBaseCles();
const transaction = base.transaction("cles", "readwrite");
transaction.objectStore("cles").put(cle, identifiant);
}
Cette approche évite de redériver la clé à chaque chargement de page tout en gardant extractable: false, un compromis que peu de tutoriels sur le sujet mentionnent alors qu’il règle une vraie friction pratique.
Étape 11 : Construire l’interface de chiffrement de fichiers
Passons du code isolé à une interface utilisable. L’objectif : glisser un fichier, saisir un mot de passe, obtenir un fichier chiffré téléchargeable.
async function gererFichierDepose(fichier, motDePasse) {
const tailleMax = 50 * 1024 * 1024; // 50 Mo, limite raisonnable en mémoire
if (fichier.size > tailleMax) {
throw new Error("Fichier trop volumineux pour un chiffrement en mémoire. Découpez-le en blocs.");
}
const contenu = await fichier.arrayBuffer();
const sel = crypto.getRandomValues(new Uint8Array(16));
const cle = await deriverCleDepuisMotDePasse(motDePasse, sel);
const iv = crypto.getRandomValues(new Uint8Array(12));
const chiffre = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, cle, contenu);
// Assemblage : sel + iv + données chiffrées, dans un seul Blob téléchargeable
const paquet = new Blob([sel, iv, chiffre]);
return paquet;
}
Le fichier de sortie contient le sel et l’IV en préfixe, suivis des données chiffrées. Au déchiffrement, il suffit de relire les 16 premiers octets pour le sel et les 12 suivants pour l’IV avant de traiter le reste comme le texte chiffré.
Étape 12 : Exporter et importer une clé publique au format JWK
Pour qu’un destinataire puisse vérifier une signature, il a besoin de la clé publique, sous un format qu’il pourra réimporter dans son propre navigateur. Le format JWK (JSON Web Key) est le plus lisible et le plus simple à transmettre.
async function exporterClePublique(clePublique) {
return crypto.subtle.exportKey("jwk", clePublique);
}
async function importerClePublique(jwk) {
return crypto.subtle.importKey(
"jwk",
jwk,
{ name: "ECDSA", namedCurve: "P-256" },
true,
["verify"]
);
}
// Exemple de sortie JWK pour une clé publique P-256
// { "crv": "P-256", "ext": true, "key_ops": ["verify"],
// "kty": "EC", "x": "f83OJ3D2xF1...", "y": "x_FEzRu9m36H..." }
Notez que seule la clé publique est exportée ici. La clé privée, elle, reste dans IndexedDB avec extractable: false dès qu’un usage réel de ce projet dépasse le cadre d’un simple test local.
Étape 13 : Assembler et tester le projet complet
Il reste à relier chaque brique dans un flux unique. Voici le squelette complet de crypto.js, qui regroupe les fonctions précédentes et les connecte à une interface minimale.
document.getElementById("app").innerHTML = `
<input type="file" id="fichier">
<input type="password" id="motdepasse" placeholder="Mot de passe">
<button id="chiffrer">Chiffrer et télécharger</button>
<p id="statut"></p>
`;
document.getElementById("chiffrer").addEventListener("click", async () => {
const statut = document.getElementById("statut");
try {
verifierSupportCrypto();
const fichier = document.getElementById("fichier").files[0];
const motDePasse = document.getElementById("motdepasse").value;
if (!fichier || !motDePasse) {
statut.textContent = "Sélectionnez un fichier et saisissez un mot de passe.";
return;
}
const paquetChiffre = await gererFichierDepose(fichier, motDePasse);
const url = URL.createObjectURL(paquetChiffre);
const lien = document.createElement("a");
lien.href = url;
lien.download = fichier.name + ".chiffre";
lien.click();
statut.textContent = "Fichier chiffré et téléchargé avec succès.";
} catch (erreur) {
statut.textContent = "Erreur : " + erreur.message;
}
});
Testez avec un petit fichier texte. Le résultat attendu : un fichier .chiffre téléchargé, dont la taille correspond à peu près au fichier d’origine plus 44 octets (16 pour le sel, 12 pour l’IV, 16 pour le tag d’authentification GCM). Ouvrir ce fichier dans un éditeur de texte classique affiche des octets illisibles, ce qui confirme que le chiffrement a fonctionné.
Structure finale du projet
À ce stade, le projet complet tient dans trois fichiers, sans dépendance à installer. Voici l’arborescence finale et le rôle de chaque fichier.
chiffrement-navigateur/
├── index.html # Structure de la page, charge crypto.js
├── style.css # Mise en forme minimale du formulaire
└── crypto.js # Toutes les fonctions des étapes 2 à 13 :
# dérivation PBKDF2, chiffrement AES-GCM,
# signature ECDSA, empreinte SHA-256,
# stockage IndexedDB, interface utilisateur
Ce découpage reste volontairement plat. Pour un projet plus ambitieux, séparer crypto.js en modules ES (chiffrement.js, signature.js, stockage.js) avec des imports type="module" dans le HTML améliore la lisibilité sans changer la logique cryptographique exposée dans ce tutoriel. L’ensemble du code reste exécutable hors ligne une fois servi une première fois, puisqu’aucune des opérations de crypto.subtle ne nécessite d’appel réseau.
Web Crypto API face à Node.js crypto et libsodium
L’API Web Crypto n’est pas la seule option pour manipuler de la cryptographie en JavaScript. Le module crypto de Node.js couvre un terrain similaire côté serveur, et libsodium (souvent utilisé via le paquet libsodium-wrappers) propose des primitives modernes comme XChaCha20-Poly1305 absentes de la spécification W3C. Le tableau suivant compare les trois options sur les critères qui comptent pour un choix d’architecture.
| Critère | API Web Crypto | Node.js crypto | libsodium |
|---|---|---|---|
| Environnement d’exécution | Navigateur uniquement | Serveur Node.js | Navigateur et serveur (via WASM) |
| Installation requise | Aucune, native | Aucune, native | Dépendance npm externe |
| Chiffrement authentifié | AES-GCM | AES-GCM, ChaCha20-Poly1305 | XChaCha20-Poly1305 |
| Signatures disponibles | ECDSA, RSA-PSS, Ed25519 (récent) | ECDSA, Ed25519, RSA | Ed25519 |
| Dérivation de clé | PBKDF2, HKDF | PBKDF2, scrypt, Argon2 (via module tiers) | Argon2id natif |
| Courbe elliptique par défaut | P-256 | Configurable | Curve25519 |
| Cas d’usage typique | Chiffrement côté client, formulaires sensibles | API backend, chiffrement au repos | Applications cross-platform, zéro confiance serveur |
Le choix dépend surtout d’où s’exécute le code critique. Un formulaire qui doit chiffrer un numéro de sécurité sociale avant envoi n’a pas d’autre option raisonnable que l’API Web Crypto puisqu’il tourne dans le navigateur. Un service backend qui chiffre des sauvegardes utilisera plus naturellement Node.js crypto ou, pour Argon2id natif, libsodium.
Beaucoup d’équipes finissent en réalité par combiner les trois. L’API Web Crypto chiffre les données sensibles avant qu’elles ne quittent le poste de l’utilisateur, Node.js crypto re-chiffre l’ensemble au repos sur le serveur avec une clé gérée séparément, et libsodium intervient côté mobile quand l’application native doit interagir avec les mêmes données chiffrées en dehors d’un navigateur. Aucune des trois bibliothèques ne remplace les deux autres : elles couvrent des étages différents d’une même architecture de sécurité en profondeur.
Support des algorithmes selon les navigateurs
Le socle AES-GCM, ECDSA, PBKDF2 et SHA-256 est disponible depuis longtemps dans tous les navigateurs modernes. Les courbes modernes (Ed25519, X25519) suivent un calendrier plus récent et moins uniforme. Le tableau ci-dessous résume ce qui est confirmé début octobre 2026.
| Algorithme | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
| AES-GCM | Oui | Oui | Oui | Oui |
| ECDSA P-256 | Oui | Oui | Oui | Oui |
| PBKDF2 | Oui | Oui | Oui | Oui |
| SHA-256 / digest | Oui | Oui | Oui | Oui |
| Ed25519 | Oui, par défaut depuis Chrome 137 | Support ajouté, vérifier la version installée | Support ajouté, vérifier la version installée | Basé sur Chromium, suit Chrome |
Pour Ed25519 et X25519, la prudence reste de mise : les documents qui décrivent ces courbes dans l’écosystème Web Crypto (Secure Curves, Modern Algorithms) restent des rapports du Web Platform Incubator Community Group, pas des standards W3C au sens strict. Avant de baser une fonctionnalité critique sur ces courbes, un test de détection de fonctionnalité (comme celui de l’étape 2) reste indispensable, idéalement complété par un repli automatique vers ECDSA P-256.
Dans la pratique, une stratégie de repli (fallback) reste la seule approche fiable tant que le support des courbes modernes n’est pas uniforme. Le code peut tester la disponibilité d’Ed25519 avec un appel generateKey encapsulé dans un try/catch et basculer automatiquement vers ECDSA P-256 en cas d’échec, sans jamais interrompre le parcours utilisateur. Cette logique défensive coûte une dizaine de lignes de code et évite des rapports de bugs venant d’utilisateurs sur des navigateurs plus anciens ou des navigateurs embarqués (WebView Android, par exemple) dont le moteur de rendu accuse parfois plusieurs versions de retard sur le Chrome grand public.
Les 7 pièges les plus fréquents
- Réutiliser un IV avec la même clé AES-GCM. C’est l’erreur la plus grave possible avec ce mode : deux chiffrements avec le même couple clé/IV permettent de retrouver le flux de clé par simple XOR des deux chiffrés. Générez toujours un nouvel IV aléatoire pour chaque opération, jamais un IV fixe ou incrémenté manuellement sans rigueur.
- Confondre ArrayBuffer, Uint8Array et chaîne base64. L’API Web Crypto manipule des
ArrayBuffer, pas des chaînes. Oublier une conversion (TextEncoder/TextDecoderpour le texte,btoa/atobpour l’affichage) provoque des erreurs de type ou des données corrompues silencieusement. - Choisir un nombre d’itérations PBKDF2 trop faible. D’anciens tutoriels circulent encore avec 1 000 ou 10 000 itérations, des valeurs obsolètes face à la puissance de calcul actuelle. Alignez-vous sur les 310 000 itérations recommandées par l’OWASP pour SHA-256 en 2026, ou davantage si votre budget de calcul le permet.
- Rendre une clé exportable sans raison. Mettre
extractable: truepar réflexe sur une clé privée annule une partie de l’intérêt de l’API : n’importe quel script tiers compromis sur la page pourrait alors en extraire la valeur. Réserveztrueaux clés publiques ou aux cas où l’export est une exigence fonctionnelle explicite. - Chiffrer de gros fichiers entièrement en mémoire. Charger un fichier de plusieurs centaines de mégaoctets via
arrayBuffer()peut saturer la mémoire de l’onglet et planter la page. Au-delà de quelques dizaines de mégaoctets, un découpage en blocs chiffrés séparément devient nécessaire. - Ignorer les écarts de compatibilité entre navigateurs. Un algorithme disponible dans Chrome n’est pas automatiquement présent avec les mêmes paramètres dans Safari. Tester uniquement sur un seul navigateur en développement masque des régressions qui n’apparaissent qu’en production.
- Stocker le mot de passe en clair pour “simplifier” le débogage. Même temporairement dans une variable globale ou un
console.logoublié, un mot de passe en clair dans le code de production finit presque toujours par fuiter via les outils de développement ou un journal d’erreurs.
Dépannage : 10 erreurs courantes et leurs solutions
- “crypto.subtle is undefined” → La page n’est pas servie en contexte sécurisé. Vérifiez que l’URL commence par
https://ouhttp://localhost, jamais parfile://. - OperationError lors du déchiffrement AES-GCM → Le tag d’authentification ne correspond pas. Cause la plus fréquente : mauvais mot de passe, mauvais sel récupéré, ou données tronquées pendant le transfert.
- DataError sur importKey → Le format des données ne correspond pas au format déclaré (“raw”, “jwk”, “pkcs8”, “spki”). Vérifiez que l’encodage utilisé à l’export correspond exactement à celui attendu à l’import.
- InvalidAccessError sur sign ou encrypt → La clé a été créée avec un tableau
keyUsagesqui n’inclut pas l’opération demandée. Une clé générée avec["verify"]uniquement ne pourra jamais signer. - NotSupportedError sur un algorithme → Vérifiez la casse exacte du nom d’algorithme (“AES-GCM” et non “aes-gcm”) et la version du navigateur, surtout pour Ed25519 ou X25519 sur des versions plus anciennes.
- Le fichier déchiffré est corrompu alors que le mot de passe est correct → Le sel ou l’IV lus en préfixe ne correspondent probablement pas aux bonnes longueurs (16 octets pour le sel, 12 pour l’IV dans ce projet). Un décalage d’un seul octet suffit à tout casser.
- “The operation is insecure” dans Safari → Certaines versions de Safari appliquent des règles de contexte sécurisé plus strictes, notamment en navigation privée. Testez hors navigation privée avant de conclure à un bug de code.
- ArrayBuffer détaché après un transfert vers un Web Worker → Une fois un
ArrayBuffertransféré viapostMessageavec l’option de transfert, l’original devient inutilisable dans le thread d’origine. Clonez les données si elles doivent encore servir après l’envoi. - Performances dégradées sur mobile → PBKDF2 à 310 000 itérations peut prendre une à deux secondes sur un téléphone d’entrée de gamme. Affichez un indicateur de chargement pour éviter l’impression d’un gel de l’interface.
- Clé non retrouvée après rechargement de la page → Si la clé n’a pas été persistée dans IndexedDB (étape 10), elle est recréée en mémoire à chaque chargement, donc différente de la précédente. Vérifiez la logique de sauvegarde avant de soupçonner l’API elle-même.
Astuces avancées pour aller plus loin
Une fois le projet de base fonctionnel, plusieurs pistes permettent de le faire évoluer vers un usage production.
Déplacer les opérations lourdes vers un Web Worker évite de bloquer l’interface pendant le chiffrement d’un gros fichier. L’API crypto.subtle est accessible depuis un Worker exactement comme depuis le thread principal, ce qui rend la migration simple : il suffit de transférer le fichier via postMessage et de renvoyer le résultat chiffré une fois l’opération terminée.
Pour le chiffrement de fichiers volumineux, un découpage en blocs de quelques mégaoctets, chacun chiffré avec un IV distinct dérivé d’un compteur, permet un chiffrement en flux (streaming) qui ne charge jamais l’intégralité du fichier en mémoire. Les API ReadableStream et TransformStream du navigateur s’y prêtent naturellement.
HKDF, disponible nativement dans crypto.subtle au même titre que PBKDF2, convient mieux quand la clé source est déjà une donnée à haute entropie (comme le résultat d’un échange Diffie-Hellman) plutôt qu’un mot de passe humain. Combiner ECDH pour l’échange de clé et HKDF pour la dérivation ouvre la voie à une messagerie chiffrée de bout en bout entièrement construite avec l’API Web Crypto, sans aucune dépendance externe.
Enfin, ajouter un en-tête Content-Security-Policy strict sur le serveur qui héberge la page réduit la surface d’attaque XSS, un vecteur qui, s’il compromettait la page, permettrait à un script malveillant d’intercepter les mots de passe avant même qu’ils n’atteignent les fonctions de chiffrement.
Pour une protection encore plus poussée, l’association avec WebAuthn permet de déverrouiller une clé de chiffrement stockée localement grâce à l’empreinte digitale ou au visage de l’utilisateur, plutôt qu’avec un mot de passe qu’il faut mémoriser et qui reste vulnérable au phishing. Le principe : la clé de chiffrement reste scellée dans IndexedDB, et seule une authentification biométrique réussie via navigator.credentials.get() déclenche son utilisation en mémoire. Cette combinaison commence à apparaître dans des gestionnaires de mots de passe et des applications de prise de notes chiffrées destinées au grand public.
Conformité RGPD et bonnes pratiques pour la France et l’Europe
Le chiffrement côté client s’inscrit directement dans la logique de minimisation des données portée par le RGPD. Quand une clé ou un mot de passe ne transite jamais vers un serveur, ce serveur ne peut ni la perdre, ni la divulguer lors d’une fuite, ni être contraint légalement de la fournir puisqu’il ne la détient pas. Cela ne dispense toutefois pas d’un registre de traitement ni d’une analyse d’impact si l’application manipule des données sensibles au sens de l’article 9 du RGPD.
Un point de vigilance mérite d’être souligné pour les équipes françaises : le chiffrement côté navigateur protège les données en transit et au repos sur le serveur, mais pas contre un poste client déjà compromis par un malware, ni contre un mot de passe faible choisi par l’utilisateur final. Documentez cette limite dans toute analyse de risque, en complément des mesures organisationnelles habituelles (sensibilisation, gestion des mots de passe, mises à jour régulières des navigateurs sur le parc de l’entreprise).
Pour une startup ou une PME qui développe un produit destiné au marché européen, documenter ce choix architectural dans la politique de confidentialité présente aussi un avantage commercial réel : pouvoir écrire que les données sensibles sont chiffrées avant de quitter l’appareil de l’utilisateur, et que l’éditeur lui-même ne peut pas les lire, devient un argument de vente face à des clients professionnels de plus en plus attentifs à la souveraineté de leurs données. C’est également un argument qui facilite les discussions avec un délégué à la protection des données (DPO) lors de l’instruction d’un nouveau traitement.
Pour aller plus loin sur les fondations cryptographiques utilisées ici, la documentation technique de référence du MDN Web Docs sur SubtleCrypto détaille chaque méthode et ses paramètres exacts. La spécification officielle reste consultable sur le site du W3C, Web Cryptography API. Pour les recommandations d’itérations PBKDF2 citées plus haut, le OWASP Password Storage Cheat Sheet fait référence. Les paramètres recommandés pour AES-GCM, dont la taille de l’IV, sont détaillés dans la publication NIST SP 800-38D. Enfin, pour une vision d’ensemble des algorithmes post-quantiques qui viendront compléter ce socle dans les prochaines années, le site de l’ENISA publie des guides réguliers à destination des développeurs européens.
Questions fréquentes
L’API Web Crypto fonctionne-t-elle sans connexion internet ?
Oui. Une fois la page chargée (fichiers HTML, CSS, JS mis en cache ou servis localement), toutes les opérations cryptographiques s’exécutent entièrement dans le navigateur, sans appel réseau.
Peut-on utiliser l’API Web Crypto dans une extension de navigateur ?
Oui, les extensions Chrome et Firefox s’exécutent dans un contexte sécurisé et ont accès à crypto.subtle sans configuration supplémentaire.
Faut-il préférer AES-GCM ou ChaCha20-Poly1305 ?
L’API Web Crypto standard n’implémente pas ChaCha20-Poly1305 nativement, ce qui tranche la question par défaut côté navigateur. AES-GCM bénéficie en plus d’une accélération matérielle (AES-NI) sur la quasi-totalité des processeurs récents, ce qui le rend rapide même sans bibliothèque tierce.
Le code JavaScript côté client peut-il être inspecté par un attaquant ?
Oui, entièrement. Le code source est toujours visible dans le navigateur. La sécurité ne repose donc jamais sur le secret de l’algorithme, mais sur le secret de la clé et du mot de passe, qui eux ne quittent jamais la machine de l’utilisateur dans cette architecture.
Quelle est la différence entre crypto.getRandomValues et Math.random ?Math.random() n’offre aucune garantie cryptographique et ne doit jamais servir à générer un sel, un IV ou une clé. crypto.getRandomValues() s’appuie sur le générateur aléatoire sécurisé du système d’exploitation, le seul adapté à un usage cryptographique.
Peut-on chiffrer directement un objet JavaScript (JSON) ?
Pas directement. Il faut d’abord le sérialiser en chaîne avec JSON.stringify, puis l’encoder en ArrayBuffer avec TextEncoder avant de le passer à crypto.subtle.encrypt.
Node.js et le navigateur partagent-ils le même format de clé ?
Pas toujours directement. Les formats d’export comme JWK restent compatibles entre les deux environnements, mais certains paramètres par défaut (longueur de clé, usages autorisés) diffèrent et nécessitent une conversion explicite lors d’un échange entre un backend Node.js et un frontend navigateur.
Cette approche remplace-t-elle un gestionnaire de mots de passe ou un coffre-fort dédié ?
Non. Elle sert à chiffrer des données applicatives spécifiques avant transmission ou stockage. Pour la gestion de mots de passe au quotidien, des outils audités comme Bitwarden ou 1Password restent plus appropriés qu’une implémentation maison.
Que se passe-t-il si l’utilisateur oublie son mot de passe de chiffrement ?
Les données restent définitivement illisibles. Comme la clé est dérivée uniquement du mot de passe et du sel, aucun mécanisme de récupération côté serveur n’existe par construction, puisque le serveur n’a jamais connu ni la clé ni le mot de passe. C’est la contrepartie logique de la confidentialité obtenue : il faut prévoir une stratégie de sauvegarde du mot de passe ou une phrase de récupération séparée avant de déployer ce type de fonctionnalité auprès d’utilisateurs finaux.




