ECDH permet à deux machines de se mettre d’accord sur un secret partagé. Il ne chiffre rien du tout. C’est là que beaucoup de développeurs se trompent en pensant qu’un échange de clés suffit à protéger des données. Le schéma complet qui transforme cet échange en un vrai chiffrement à clé publique s’appelle ECIES, pour Elliptic Curve Integrated Encryption Scheme. Ce tutoriel montre comment l’implémenter de bout en bout avec le module node:crypto de Node.js, sans dépendance externe, en combinant ECDH, HKDF et AES-256-GCM. Vous repartirez avec un petit projet fonctionnel capable de chiffrer et déchiffrer des messages pour un destinataire identifié par sa seule clé publique.
Qu’est-ce que le chiffrement ECIES et pourquoi l’utiliser en 2026
ECIES n’est pas un algorithme figé dans une norme unique comme AES ou SHA-256. C’est une famille de constructions hybrides qui assemblent trois briques : un échange de clés sur courbe elliptique (ECDH), une fonction de dérivation de clé (le plus souvent HKDF) et un chiffrement authentifié (typiquement AES-GCM). Le principe tient en une phrase : on génère une clé éphémère à usage unique, on calcule un secret partagé avec la clé publique du destinataire, on dérive une clé symétrique de ce secret, puis on chiffre les données réelles avec cette clé. Le destinataire refait exactement le même calcul avec sa clé privée pour retrouver la même clé symétrique.
Ce détour par un chiffrement symétrique n’est pas un contournement, c’est la méthode standard. Les courbes elliptiques ne chiffrent pas directement des données arbitraires de façon pratique et sûre comme RSA peut le faire avec OAEP. ECIES contourne cette limite en réutilisant ECDH uniquement pour fabriquer une clé, puis en laissant AES-256-GCM faire le travail de chiffrement proprement dit. L’intérêt pour un projet en 2026 est double : des clés bien plus courtes que RSA pour un niveau de sécurité équivalent, et des opérations qui restent rapides même sur des appareils contraints. C’est ce schéma, sous des formes variées, que l’on retrouve dans Signal, dans les boîtes à outils de chiffrement d’Ethereum pour les messages hors chaîne, ou dans des bibliothèques de messagerie chiffrée.
Node.js ne fournit pas de fonction ecies() toute faite. Le module node:crypto expose toutes les briques nécessaires (génération de clés, ECDH, HKDF, AES-GCM) mais c’est à vous d’assembler le protocole. C’est justement ce que ce tutoriel va construire, étape par étape, avec un code que vous pourrez réutiliser tel quel dans un projet réel.
Pour une équipe française ou européenne, l’intérêt dépasse la simple curiosité technique. Un traitement de données personnelles chiffré avec un schéma à clé publique comme ECIES permet de limiter l’accès au texte en clair à la seule partie qui détient la clé privée statique, ce qui facilite la démonstration de mesures techniques appropriées dans un dossier de conformité RGPD. ECIES n’est pas une exigence réglementaire en soi, mais c’est un outil concret pour répondre à l’obligation de sécuriser les données par conception, notamment quand plusieurs services doivent chiffrer des messages pour un même destinataire sans jamais partager de secret symétrique commun entre eux.
Prérequis : versions et outils nécessaires
Avant de commencer, vérifiez que votre environnement correspond à ces versions. Le tutoriel a été rédigé et testé avec Node.js 24.20.0, la version LTS publiée le 26 août 2026, qui embarque OpenSSL 3.5. Node.js 22 reste une branche LTS active en octobre 2026 et fonctionnera aussi, les API utilisées ici étant stables depuis plusieurs versions majeures.
- Node.js 22 LTS ou 24.20.0 LTS (vérifiez avec
node -v) - npm 10.x ou supérieur, livré avec Node.js
- Un terminal et un éditeur de code (VS Code, Zed, ou équivalent)
- Des bases en JavaScript asynchrone et en manipulation de
Buffer - Aucune dépendance externe : tout repose sur le module intégré
node:crypto - Environ 90 minutes pour suivre l’ensemble des 13 étapes et tester le projet final
Notez bien un point de vocabulaire qui prête souvent à confusion : OpenSSL 3.5, intégré à Node.js 24, est le moteur cryptographique sous-jacent, pas la version de Node.js elle-même. Cette distinction compte si vous devez justifier vos choix auprès d’une équipe sécurité ou dans un audit de conformité.
Ce tutoriel n’utilise aucune bibliothèque tierce et ne requiert donc aucun fichier de verrouillage de dépendances à auditer. C’est un choix délibéré : en s’appuyant uniquement sur node:crypto, vous réduisez la surface d’attaque liée à la chaîne d’approvisionnement logicielle, un point d’attention croissant depuis les multiples incidents touchant des paquets npm compromis ces dernières années. Si vous décidez malgré tout, en fin de parcours, de migrer vers une bibliothèque ECIES packagée comme eciesjs pour un projet de production, les concepts et le format d’enveloppe que vous allez construire ici restent les mêmes : seule l’implémentation change de main.
Comment fonctionne ECIES : ECDH, HKDF et AES-256-GCM expliqués
Pour comprendre ce que vous allez coder, il faut visualiser le flux complet. Côté expéditeur : générer une paire de clés éphémère, calculer le secret ECDH avec la clé publique du destinataire, dériver une clé AES via HKDF, chiffrer le message avec AES-256-GCM, puis transmettre quatre éléments au destinataire : la clé publique éphémère, le vecteur d’initialisation (IV ou nonce), le texte chiffré, et le tag d’authentification généré par GCM. Côté destinataire : récupérer la clé publique éphémère reçue, calculer le même secret ECDH avec sa propre clé privée statique, dériver la même clé AES avec HKDF, puis déchiffrer et vérifier le tag.
La clé qui rend ce schéma sûr est l’éphémérité : une nouvelle paire de clés est générée à chaque chiffrement, ce qui évite de réutiliser un même secret ECDH deux fois. Sans cette précaution, deux messages chiffrés avec le même secret dérivé exposeraient leur relation mathématique, un défaut classique qui a déjà compromis des implémentations maison d’ECIES.
Comparez ce flux à ce que ferait RSA-OAEP pour un besoin équivalent. RSA chiffre directement un petit bloc de données (en pratique, une clé symétrique) avec la clé publique du destinataire, sans étape d’échange préalable. ECIES demande une étape de plus, l’ECDH, mais ce détour rapporte deux avantages concrets : des clés bien plus compactes pour un niveau de sécurité comparable, et une génération de nouvelle paire éphémère quasi gratuite en temps de calcul, alors que générer une nouvelle paire RSA à chaque message serait beaucoup trop coûteux pour un usage à haute fréquence. C’est cette différence de coût qui explique pourquoi la plupart des protocoles de messagerie chiffrée moderne, Signal en tête, ont convergé vers des schémas de type ECIES plutôt que vers RSA pour le chiffrement de messages individuels.
| Composant | Rôle dans ECIES | API Node.js utilisée |
|---|---|---|
| Courbe elliptique | Fournit la paire de clés et l’opération ECDH | generateKeyPairSync, diffieHellman |
| Clé éphémère | Clé à usage unique générée à chaque chiffrement | generateKeyPairSync |
| HKDF | Transforme le secret ECDH brut en clé symétrique uniforme | hkdfSync |
| AES-256-GCM | Chiffre les données et produit un tag d’authentification | createCipheriv, createDecipheriv |
Étape 1 : choisir la courbe elliptique adaptée à votre cas d’usage
Quatre courbes reviennent systématiquement dans les discussions sur ECIES : P-256, P-384, X25519 et secp256k1. Elles n’offrent pas les mêmes garanties ni la même compatibilité, et le choix dépend surtout de votre contrainte la plus forte : conformité réglementaire, performance, ou interopérabilité avec un écosystème existant.
P-256, aussi appelée secp256r1 ou prime256v1, travaille sur un corps premier de 256 bits et offre environ 128 bits de sécurité. C’est le choix de compatibilité par excellence : elle est supportée par la Web Crypto API des navigateurs, par la plupart des autorités de certification TLS, et par les environnements soumis à des exigences FIPS. P-384 (secp384r1) monte à environ 192 bits de sécurité et se justifie quand une politique de conformité l’exige explicitement, par exemple pour des données classifiées à un niveau supérieur.
X25519, construite sur Curve25519, offre également environ 128 bits de sécurité mais avec une arithmétique plus simple, pensée pour réduire les erreurs d’implémentation et limiter les risques d’attaques par canal auxiliaire. C’est la courbe que recommandent la plupart des guides techniques récents pour un nouveau système sans contrainte FIPS, et elle est au cœur de protocoles largement déployés comme TLS moderne, SSH et Signal, comme le détaille le guide ECC de tools.tdsay.cn. Enfin secp256k1 offre un niveau comparable mais reste associée à l’écosystème blockchain, Ethereum en tête : ce n’est pas la courbe à choisir par défaut pour un ECIES généraliste, sauf si votre système doit justement s’interfacer avec des clés déjà existantes sur cette courbe.
| Courbe | Niveau de sécurité | Recommandée pour | API Node.js |
|---|---|---|---|
| P-256 (secp256r1) | ~128 bits | TLS, certificats, environnements FIPS | createECDH('prime256v1') |
| P-384 (secp384r1) | ~192 bits | Conformité renforcée | createECDH('secp384r1') |
| X25519 | ~128 bits | Nouveaux systèmes, usage général | generateKeyPairSync('x25519') |
| secp256k1 | ~128 bits | Interopérabilité blockchain | createECDH('secp256k1') |
Dans Node.js, P-256, P-384 et secp256k1 passent par l’API historique createECDH() quand la version d’OpenSSL embarquée les expose. X25519 suit un chemin différent : il faut le traiter comme un type de paire de clés avec generateKeyPairSync('x25519'), puis appeler diffieHellman() plutôt que createECDH(). Ce tutoriel utilise X25519 comme choix par défaut et montre comment basculer vers P-256 si votre contexte l’exige.
Étape 2 : initialiser le projet Node.js
Créez un nouveau dossier et initialisez un projet Node.js minimal. Aucune dépendance npm n’est nécessaire : tout ce que nous utilisons vit dans le module intégré node:crypto.
mkdir ecies-nodejs && cd ecies-nodejs
npm init -y
mkdir src
node -e "console.log(process.version)"
# doit afficher v22.x.x ou v24.20.0
Ouvrez le fichier package.json généré et ajoutez "type": "module" pour pouvoir utiliser la syntaxe import dans les fichiers qui suivent. Tout le code de ce tutoriel fonctionne aussi en CommonJS si vous préférez require, seule la syntaxe d’import change.
Étape 3 : générer la paire de clés du destinataire
La paire de clés du destinataire est statique : elle est générée une fois, la clé publique est partagée, et la clé privée reste secrète et sert à déchiffrer tous les messages reçus. Créez un fichier src/keygen.js.
import { generateKeyPairSync } from 'node:crypto';
export function generateRecipientKeyPair() {
const { publicKey, privateKey } = generateKeyPairSync('x25519');
return { publicKey, privateKey };
}
// Export PEM pour stockage ou transmission
export function exportKeyPair(publicKey, privateKey) {
return {
publicKeyPem: publicKey.export({ type: 'spki', format: 'pem' }),
privateKeyPem: privateKey.export({ type: 'pkcs8', format: 'pem' }),
};
}
Testez rapidement ce module : node -e "import('./src/keygen.js').then(m => console.log(m.exportKeyPair(...Object.values(m.generateRecipientKeyPair()))))" doit afficher deux blocs PEM, l’un commençant par -----BEGIN PUBLIC KEY----- et l’autre par -----BEGIN PRIVATE KEY-----. Conservez la clé privée dans un coffre à secrets ou un fichier protégé, jamais dans votre dépôt Git.
Étape 4 : créer la clé éphémère et réaliser l’échange ECDH
C’est le cœur du protocole côté expéditeur. À chaque chiffrement, une nouvelle paire de clés éphémère est générée, puis diffieHellman() calcule le secret partagé entre la clé privée éphémère et la clé publique du destinataire.
import { generateKeyPairSync, diffieHellman } from 'node:crypto';
export function computeSharedSecret(recipientPublicKey) {
const ephemeral = generateKeyPairSync('x25519');
const sharedSecret = diffieHellman({
privateKey: ephemeral.privateKey,
publicKey: recipientPublicKey,
});
return {
sharedSecret,
ephemeralPublicKeyRaw: ephemeral.publicKey.export({
type: 'spki',
format: 'der',
}),
};
}
Le secret partagé obtenu n’est jamais utilisé directement comme clé de chiffrement. C’est une erreur fréquente : un secret ECDH brut n’a pas une distribution statistique parfaitement uniforme sur tout l’espace des clés AES, et l’utiliser sans dérivation affaiblit la sécurité théorique du schéma. L’étape suivante corrige cela.
Étape 5 : dériver la clé symétrique avec HKDF-SHA256
HKDF (HMAC-based Key Derivation Function) transforme le secret ECDH brut en une clé de 32 octets, adaptée à AES-256. Node.js expose cette primitive via hkdfSync, disponible directement dans node:crypto depuis plusieurs versions majeures.
import { hkdfSync } from 'node:crypto';
export function deriveEncryptionKey(sharedSecret, info = 'ecies-v1') {
const salt = Buffer.alloc(0); // pas de sel dédié ici, info suffit à séparer les usages
const derived = hkdfSync(
'sha256',
sharedSecret,
salt,
Buffer.from(info, 'utf8'),
32, // 32 octets = 256 bits pour AES-256
);
return Buffer.from(derived);
}
Le paramètre info sert d’étiquette de contexte : il garantit que deux usages différents de la même paire de clés (par exemple chiffrement de messages contre chiffrement de fichiers) ne produisent jamais la même clé dérivée, même si le secret ECDH sous-jacent était identique par accident.
Étape 6 : chiffrer les données avec AES-256-GCM
Avec une clé dérivée de 32 octets en main, le chiffrement proprement dit suit le schéma classique d’AES-GCM : un nonce aléatoire de 12 octets, le chiffrement du texte clair, et récupération du tag d’authentification de 16 octets.
import { createCipheriv, randomBytes } from 'node:crypto';
export function encryptWithAesGcm(key, plaintext, aad = null) {
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', key, iv);
if (aad) cipher.setAAD(aad);
const ciphertext = Buffer.concat([
cipher.update(Buffer.from(plaintext, 'utf8')),
cipher.final(),
]);
const authTag = cipher.getAuthTag();
return { iv, ciphertext, authTag };
}
Le nonce de 12 octets doit être unique pour chaque chiffrement effectué avec une même clé. Ici, comme une nouvelle clé éphémère est générée à chaque appel, le risque de collision est négligeable, mais gardez ce principe en tête si vous réutilisez cette fonction dans un contexte où la même clé AES sert à plusieurs messages.
Étape 7 : assembler l’enveloppe ECIES complète
Il est temps d’assembler les étapes précédentes dans une seule fonction encrypt() qui prend la clé publique du destinataire et un message, et renvoie une enveloppe prête à transmettre.
import { computeSharedSecret } from './ecdh.js';
import { deriveEncryptionKey } from './kdf.js';
import { encryptWithAesGcm } from './aes.js';
export function eciesEncrypt(recipientPublicKey, plaintext) {
const { sharedSecret, ephemeralPublicKeyRaw } =
computeSharedSecret(recipientPublicKey);
const key = deriveEncryptionKey(sharedSecret);
const { iv, ciphertext, authTag } = encryptWithAesGcm(key, plaintext);
return {
ephemeralPublicKey: ephemeralPublicKeyRaw.toString('base64'),
iv: iv.toString('base64'),
ciphertext: ciphertext.toString('base64'),
authTag: authTag.toString('base64'),
};
}
Cette enveloppe à quatre champs est tout ce dont le destinataire a besoin. Elle ne contient aucune information permettant de deviner la clé privée du destinataire ni le secret ECDH : seule la possession de la clé privée statique permet de refaire le calcul inverse.
Étape 8 : déchiffrer l’enveloppe côté destinataire
Le déchiffrement suit exactement le chemin inverse : importer la clé publique éphémère reçue, recalculer le secret ECDH avec la clé privée statique, dériver la même clé AES, puis déchiffrer et vérifier le tag.
import { createPublicKey, diffieHellman, createDecipheriv } from 'node:crypto';
import { deriveEncryptionKey } from './kdf.js';
export function eciesDecrypt(recipientPrivateKey, envelope) {
const ephemeralPublicKey = createPublicKey({
key: Buffer.from(envelope.ephemeralPublicKey, 'base64'),
format: 'der',
type: 'spki',
});
const sharedSecret = diffieHellman({
privateKey: recipientPrivateKey,
publicKey: ephemeralPublicKey,
});
const key = deriveEncryptionKey(sharedSecret);
const decipher = createDecipheriv(
'aes-256-gcm',
key,
Buffer.from(envelope.iv, 'base64'),
);
decipher.setAuthTag(Buffer.from(envelope.authTag, 'base64'));
const plaintext = Buffer.concat([
decipher.update(Buffer.from(envelope.ciphertext, 'base64')),
decipher.final(),
]);
return plaintext.toString('utf8');
}
Si le tag ne correspond pas, soit parce que les données ont été altérées, soit parce que la mauvaise clé privée a été utilisée, l’appel à decipher.final() lève une exception. C’est le comportement attendu : AES-GCM ne renvoie jamais silencieusement un texte clair corrompu.
Étape 9 : encoder et transporter le message chiffré
L’enveloppe produite à l’étape 7 est déjà sérialisable en JSON, ce qui la rend facile à transmettre sur HTTP, dans un message de file d’attente, ou à stocker en base. Voici à quoi ressemble une sortie réelle du programme :
{
"ephemeralPublicKey": "MCowBQYDK2VuAyEAeV3x9b2k...",
"iv": "k3F9pQABzT2RmX1a",
"ciphertext": "8hT2wq1ZxYv9uJm3Nc==",
"authTag": "p7Xk2Qm9Lw4VbRz1Yc3Nfg=="
}
Pour transporter cette structure sur un canal qui n’accepte pas le JSON (un en-tête HTTP par exemple), concatenez les quatre champs décodés en Buffer dans un ordre fixe, préfixé par leurs longueurs, puis encodez le tout en base64url. L’important est que l’expéditeur et le destinataire s’accordent sur un format unique et documenté : c’est une source d’erreurs d’interopérabilité bien plus fréquente que les erreurs de cryptographie elles-mêmes.
Étape 10 : ajouter des données authentifiées additionnelles (AAD)
AES-GCM permet d’authentifier des métadonnées qui ne sont pas chiffrées mais dont on veut garantir l’intégrité, comme un identifiant de destinataire ou un numéro de version de protocole. On appelle cela l’AAD, pour Additional Authenticated Data.
const aad = Buffer.from(JSON.stringify({ v: 1, to: 'user_482' }), 'utf8');
const { iv, ciphertext, authTag } = encryptWithAesGcm(key, plaintext, aad);
// Côté déchiffrement, la même AAD doit être fournie avant decipher.final()
decipher.setAAD(aad);
Si l’AAD fournie au déchiffrement diffère, même d’un seul octet, de celle utilisée au chiffrement, la vérification du tag échoue. C’est un moyen efficace de lier cryptographiquement un message chiffré à son contexte d’usage, sans alourdir le volume de données réellement chiffrées.
Étape 11 : construire le projet complet, une messagerie chiffrée en ligne de commande
Rassemblons tout dans un petit outil CLI fonctionnel. Avant d’écrire le point d’entrée, vérifiez que votre dossier src/ contient bien les six modules construits dans les étapes précédentes. Voici la structure finale du projet :
ecies-nodejs/
├── package.json
├── src/
│ ├── keygen.js (étape 3 : génération des clés statiques)
│ ├── ecdh.js (étape 4 : clé éphémère et secret partagé)
│ ├── kdf.js (étape 5 : dérivation HKDF)
│ ├── aes.js (étape 6 : chiffrement AES-256-GCM)
│ ├── encrypt.js (étape 7 : assemblage de l'enveloppe)
│ ├── decrypt.js (étape 8 : déchiffrement)
│ └── cli.js (étape 11 : point d'entrée)
└── test/
└── ecies.test.js (étape 12)
Créez maintenant src/cli.js, qui expose trois commandes : générer une paire de clés, chiffrer un message, et le déchiffrer.
import { writeFileSync, readFileSync } from 'node:fs';
import { generateRecipientKeyPair, exportKeyPair } from './keygen.js';
import { eciesEncrypt } from './encrypt.js';
import { eciesDecrypt } from './decrypt.js';
import { createPrivateKey, createPublicKey } from 'node:crypto';
const [, , command, ...args] = process.argv;
if (command === 'keygen') {
const { publicKey, privateKey } = generateRecipientKeyPair();
const { publicKeyPem, privateKeyPem } = exportKeyPair(publicKey, privateKey);
writeFileSync('recipient_public.pem', publicKeyPem);
writeFileSync('recipient_private.pem', privateKeyPem);
console.log('Clés générées : recipient_public.pem, recipient_private.pem');
} else if (command === 'encrypt') {
const [message] = args;
const publicKey = createPublicKey(readFileSync('recipient_public.pem'));
const envelope = eciesEncrypt(publicKey, message);
writeFileSync('message.json', JSON.stringify(envelope, null, 2));
console.log('Message chiffré dans message.json');
} else if (command === 'decrypt') {
const privateKey = createPrivateKey(readFileSync('recipient_private.pem'));
const envelope = JSON.parse(readFileSync('message.json', 'utf8'));
console.log('Message en clair :', eciesDecrypt(privateKey, envelope));
} else {
console.log('Usage : node src/cli.js [keygen|encrypt "message"|decrypt]');
}
Testez le flux complet en trois commandes :
node src/cli.js keygen
node src/cli.js encrypt "Rendez-vous demain a 15h au bureau"
node src/cli.js decrypt
# Message en clair : Rendez-vous demain a 15h au bureau
Vous disposez désormais d’un outil autonome, sans aucune dépendance npm, capable de chiffrer un message pour quiconque possède une clé publique X25519 et de le déchiffrer avec la clé privée correspondante.
Étape 12 : écrire des tests avec des vecteurs connus
Un test de bout en bout qui chiffre puis déchiffre le même message et vérifie l’égalité est le strict minimum. Ajoutez aussi un test négatif qui confirme que la modification d’un seul octet du ciphertext fait échouer le déchiffrement.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { generateRecipientKeyPair } from '../src/keygen.js';
import { eciesEncrypt } from '../src/encrypt.js';
import { eciesDecrypt } from '../src/decrypt.js';
test('round-trip chiffrement/déchiffrement', () => {
const { publicKey, privateKey } = generateRecipientKeyPair();
const envelope = eciesEncrypt(publicKey, 'texte secret');
const result = eciesDecrypt(privateKey, envelope);
assert.equal(result, 'texte secret');
});
test('rejette un ciphertext altéré', () => {
const { publicKey, privateKey } = generateRecipientKeyPair();
const envelope = eciesEncrypt(publicKey, 'texte secret');
const corrupted = { ...envelope, ciphertext: envelope.ciphertext.slice(0, -4) + 'AAAA' };
assert.throws(() => eciesDecrypt(privateKey, corrupted));
});
Lancez ces tests avec le runner intégré de Node.js, disponible nativement depuis les versions LTS récentes : node --test. Aucune bibliothèque de test externe n’est nécessaire pour ce niveau de couverture.
Étape 13 : choisir la configuration de production selon votre contexte
Avant de déployer ce code, posez-vous trois questions : devez-vous interopérer avec des systèmes existants qui imposent déjà une courbe particulière, êtes-vous soumis à une contrainte FIPS ou réglementaire, et quel est le volume de messages à chiffrer par seconde. Ces trois réponses déterminent le choix final de courbe et de taille de clé dérivée.
- Nouveau système sans contrainte particulière : X25519 + HKDF-SHA256 + AES-256-GCM, le trio recommandé par défaut.
- Interopérabilité avec TLS, Web Crypto API ou certificats X.509 existants : P-256.
- Exigence de conformité à un niveau de sécurité supérieur à 128 bits : P-384.
- Système devant dialoguer avec des clés existantes sur une chaîne compatible Ethereum : secp256k1, en gardant en tête que ce n’est pas un choix à généraliser.
Le choix d’OpenSSL embarqué compte aussi : Node.js 24 s’appuie sur OpenSSL 3.5, dont le support est annoncé jusqu’en avril 2030 et qui ajoute la prise en charge de standards post-quantiques du NIST dans les distributions concernées, un point à surveiller si votre feuille de route sécurité anticipe une migration post-quantique dans les prochaines années.
Pièges fréquents à éviter avec ECIES en Node.js
Certaines erreurs reviennent systématiquement dans les implémentations maison d’ECIES. Les connaître à l’avance évite des heures de débogage et, surtout, des failles de sécurité silencieuses.
- Réutiliser la clé éphémère entre plusieurs messages : chaque appel à
eciesEncrypt()doit générer une nouvelle paire éphémère, sans exception. - Utiliser le secret ECDH brut comme clé AES sans passer par HKDF : le secret partagé n’a pas les propriétés statistiques requises pour servir directement de clé symétrique.
- Mélanger
createECDH()et X25519 : X25519 doit passer pargenerateKeyPairSync('x25519')etdiffieHellman(), pas par l’API historiquecreateECDH()qui ne le supporte pas de la même façon. - Oublier de transmettre le tag d’authentification : sans lui, le déchiffrement AES-GCM est tout simplement impossible, et certains développeurs l’omettent par erreur en pensant que le ciphertext suffit.
- Ignorer les exceptions de déchiffrement : attraper l’erreur levée par
decipher.final()sans la traiter, par exemple en renvoyant un message par défaut, masque une tentative d’altération ou une mauvaise clé et casse les garanties de sécurité du protocole. - Coder la taille de la clé dérivée en dur sans vérifier l’algorithme symétrique visé : AES-256 exige 32 octets, pas 16 ni 24 ; une confusion ici produit une clé tronquée silencieusement acceptée par certaines bibliothèques.
Dépannage : problèmes courants et solutions
Voici les messages d’erreur et comportements inattendus les plus signalés lors de l’implémentation d’ECIES avec node:crypto, avec leur cause probable et la correction à appliquer.
| Symptôme | Cause probable | Solution |
|---|---|---|
Erreur ERR_OSSL_EVP_BAD_DECRYPT au déchiffrement | Tag d’authentification manquant ou incorrect transmis à setAuthTag() | Vérifiez l’ordre d’encodage/décodage base64 des quatre champs de l’enveloppe |
diffieHellman() renvoie une erreur de type de clé | Tentative de mélanger une clé X25519 avec une clé générée par createECDH() | Utilisez systématiquement generateKeyPairSync('x25519') pour les deux clés |
| Résultat du déchiffrement différent entre deux exécutions | Le nonce (IV) n’a pas été transmis ou a été régénéré côté destinataire | Transmettez toujours l’IV généré au chiffrement, ne le recréez jamais côté destinataire |
| Clé dérivée trop courte pour AES-256-GCM | Longueur demandée à hkdfSync fixée à 16 au lieu de 32 | Passez explicitement 32 en dernier argument de hkdfSync |
TypeError: ephemeralPublicKey is not a valid key | La clé a été exportée en PEM mais importée en DER, ou l’inverse | Harmonisez le format d’export/import (spki/der recommandé pour le transport) |
| Performances dégradées sous forte charge | Génération répétée de paires de clés éphémères sur un chemin critique synchrone | Déportez la génération de clés sur un worker thread pour les volumes élevés |
| Message AAD rejeté au déchiffrement | L’AAD fournie à setAAD() diffère, même légèrement, de celle utilisée au chiffrement | Sérialisez l’AAD de façon déterministe (ordre des clés JSON fixe) avant chiffrement |
| Clé privée illisible après redémarrage | Export PEM corrompu par un mauvais encodage de fichier (UTF-16 au lieu d’UTF-8) | Forcez l’encodage UTF-8 explicite lors de l’écriture et de la lecture des fichiers PEM |
Conseils avancés : rotation des clés, HSM et transition post-quantique
Une fois le schéma de base maîtrisé, plusieurs ajustements renforcent la solidité d’un déploiement réel. D’abord la rotation : même si la clé éphémère change à chaque message, la clé privée statique du destinataire reste fixe pendant toute sa durée de vie. Planifiez une rotation périodique de cette clé statique, avec une période de transition où l’ancienne et la nouvelle clé publique coexistent pour éviter de casser les messages en transit.
Pour des environnements à forte exigence de sécurité, envisagez de stocker la clé privée statique dans un module matériel de sécurité (HSM) plutôt que dans un fichier PEM sur disque. Le module node:crypto peut s’interfacer avec certains moteurs PKCS#11 selon la configuration OpenSSL de votre système, ce qui permet de ne jamais exposer la clé privée en mémoire applicative.
Enfin, gardez un œil sur l’évolution post-quantique. Les courbes elliptiques classiques comme celles utilisées ici restent considérées comme sûres contre des ordinateurs classiques, mais l’écosystème cryptographique évolue vers des mécanismes hybrides combinant ECDH avec des algorithmes résistants aux ordinateurs quantiques, comme le documente le suivi public de l’état des algorithmes cryptographiques tenu par la communauté metaeffekt. OpenSSL 3.5, embarqué dans Node.js 24, pose déjà les bases de cette transition dans certaines distributions. Concevez votre format d’enveloppe ECIES de façon à pouvoir ajouter un champ de version sans casser la compatibilité, cela vous fera gagner un temps précieux le jour où vous devrez migrer vers un schéma hybride post-quantique.
Sur le plan des performances, la nature même de l’arithmétique de X25519, plus simple que celle des courbes NIST classiques, en fait un choix couramment cité comme rapide à l’exécution, ce que confirme l’analyse comparative publiée par SecureW2 sur la cryptographie à courbe elliptique. Si vous migrez un système existant construit sur P-256 vers X25519, prévoyez une phase de double support pendant laquelle les deux formats d’enveloppe sont acceptés en lecture.
Côté suivi des versions, consultez régulièrement les notes de publication Node.js, par exemple celles de la version 24.20.0, qui précisent les mises à jour d’OpenSSL embarquées et les éventuels correctifs touchant le module crypto. Pour les équipes utilisant une distribution d’entreprise, les notes de version équivalentes publiées par Red Hat pour Node.js 24 détaillent aussi les correctifs de sécurité appliqués en amont des versions communautaires.
Un dernier point mérite attention si votre équipe gère plusieurs destinataires à grande échelle, par exemple pour chiffrer des notifications individuelles pour des milliers d’utilisateurs. Évitez de recalculer une paire de clés éphémère par appel bloquant sur le thread principal si votre service traite un volume important de messages en parallèle. Node.js reste mono-thread pour l’exécution JavaScript, et bien que les opérations cryptographiques de node:crypto s’appuient sur des primitives natives rapides, un pic de charge peut tout de même saturer la boucle d’événements. Dans ce cas, répartissez le travail de chiffrement sur plusieurs worker_threads ou sur plusieurs processus gérés par un orchestrateur, chacun important les mêmes modules keygen.js, ecdh.js, kdf.js et aes.js construits plus haut, sans avoir à dupliquer la logique métier.
Foire aux questions sur ECIES en Node.js
ECIES est-il plus sûr que RSA-OAEP pour chiffrer des données ?
Les deux offrent des garanties de sécurité comparables lorsqu’ils sont correctement implémentés. L’avantage pratique d’ECIES tient à la taille des clés : une clé X25519 de 256 bits offre un niveau de sécurité équivalent à une clé RSA bien plus longue, ce qui réduit la bande passante et le temps de calcul, en particulier sur mobile ou IoT.
Puis-je utiliser ECIES pour chiffrer des fichiers volumineux ?
Oui, mais ne chiffrez pas tout le fichier directement avec le schéma présenté ici. Utilisez ECIES pour chiffrer une clé de fichier aléatoire, puis chiffrez le fichier lui-même avec cette clé en mode streaming via AES-256-GCM par blocs, pour éviter de charger tout le fichier en mémoire.
Faut-il une bibliothèque tierce comme eciesjs plutôt que du code maison ?
Pour un projet de production à fort enjeu, une bibliothèque maintenue et auditée réduit le risque d’erreur d’implémentation. Ce tutoriel a une visée pédagogique : il vous donne les bases pour comprendre, auditer et déboguer n’importe quelle implémentation d’ECIES, maison ou tierce.
Pourquoi ne pas simplement utiliser ECDH puis AES sans passer par HKDF ?
Parce que le secret ECDH brut n’est pas garanti uniforme sur tout l’espace des clés AES. HKDF applique une extraction puis une expansion cryptographique qui produit une clé aux propriétés statistiques adaptées à un usage symétrique, à un coût de calcul négligeable.
X25519 est-il compatible avec les certificats X.509 classiques ?
Les certificats X.509 pour l’authentification TLS reposent généralement sur P-256 ou RSA pour des raisons de compatibilité avec l’écosystème des autorités de certification. X25519 est largement supporté pour l’échange de clés TLS lui-même, mais moins systématiquement pour les certificats de signature.
Que se passe-t-il si deux expéditeurs chiffrent pour le même destinataire en même temps ?
Rien de problématique : chaque chiffrement génère sa propre clé éphémère, donc son propre secret ECDH et sa propre clé AES dérivée. Les deux enveloppes sont totalement indépendantes même si elles visent la même clé publique de destinataire.
Node.js 22 peut-il exécuter ce code aussi bien que Node.js 24 ?
Oui. Toutes les API utilisées, generateKeyPairSync, diffieHellman, hkdfSync, createCipheriv/createDecipheriv, sont stables sur les deux branches LTS actives en 2026. Seule la version d’OpenSSL embarquée diffère légèrement entre les deux.
Comment vérifier que mon implémentation ne contient pas de faille évidente ?
Relisez la section sur les pièges fréquents de ce tutoriel, faites passer votre code par un test négatif qui altère volontairement le ciphertext ou l’AAD, et si le projet a un enjeu de sécurité réel, faites auditer le code par une tierce partie spécialisée en cryptographie appliquée avant toute mise en production.
