Chiffrement de bout en bout avec Web Crypto API et Node.js
Le chiffrement de bout en bout place la création et la conservation des clés chez les participants, tandis que les serveurs ne relaient que des données illisibles. Dans une application JavaScript, Web Crypto API fournit les primitives cryptographiques du navigateur, et Node.js expose les mêmes outils côté serveur. L’enjeu consiste à définir quelles données doivent réellement rester hors de portée du fournisseur.
Avant de coder, il faut modélier les menaces, définir le modèle de confiance et distribuer les responsabilités entre clients, serveurs et administrateurs. Une implémentation robuste associe un échange de clés authentifié, une dérivation prévisible, un chiffrement authentifié et une politique de rotation. Chiffrer une charge utile avec une clé conservée par le serveur ne protège pas l’utilisateur : ce dernier doit détenir le secret nécessaire au déchiffrement.
Poser la frontière de confiance
Le principe de base est simple : le message est chiffré sur l’appareil de l’expéditeur, traverse le réseau sous forme de cryptogramme, puis est déchiffré chez le destinataire. Le serveur peut distribuer, ordonnancer ou stocker ces enveloppes sans connaître leur contenu. Cette promesse porte sur les données, pas sur toutes les propriétés du service.
Cette propriété exige toutefois une architecture de clés précise. Une identité de longue durée peut être accompagnée de clés prépubliées et de clés à usage unique pour établir plusieurs sessions. Un mécanisme de progression, comme Double Ratchet, limite la réutilisation après une compromission. Les algorithmes de Web Crypto API ne remplacent pas ce protocole : ils fournissent les primitives dont une bibliothèque auditée a besoin.
Le stockage côté serveur peut utiliser PostgreSQL, MongoDB, Cassandra ou Redis pour conserver les comptes, les files et les enveloppes chiffrées. Le comparatif NoSQL permet de comparer les avantages de ces bases selon le volume, la cohérence et les accès. Ce choix ne doit jamais modifier la frontière de confiance : les clés privées et les secrets de récupération restent sous le contrôle des clients.
Construire un échange chiffré avec les primitives natives
Dans un navigateur, crypto.subtle n’est accessible que dans un contexte sécurisé, généralement HTTPS ou localhost. L’API propose notamment ECDH, HKDF, PBKDF2 et AES-GCM. ECDH établit un secret partagé, HKDF le transforme en clé de session liée au contexte, et AES-GCM chiffre puis authentifie chaque message. Le code suivant utilise une clé déjà négociée et un vecteur d’initialisation aléatoire de 96 bits :
const encodeur = new TextEncoder();
async function chiffrerMessage(cle, message, contexte) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const contenu = encodeur.encode(message);
const donneesChiffrees = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv,
additionalData: encodeur.encode(contexte),
},
cle,
contenu,
);
return { iv, donneesChiffrees };
}
async function dechiffrerMessage(cle, iv, donneesChiffrees, contexte) {
const contenu = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv,
additionalData: encodeur.encode(contexte),
},
cle,
donneesChiffrees,
);
return new TextDecoder().decode(contenu);
}
Le champ additionalData associe le cryptogramme à un identifiant de conversation, une version de protocole ou un compteur, ce qui empêche son remplacement dans un autre contexte. Un vecteur d’initialisation ne doit jamais être réutilisé avec la même clé ; en pratique, chaque session dispose de son propre espace de clés. Avant l’envoi, convertissez le tampon et le vecteur en octets sérialisables, car JSON.stringify ne produit pas directement un format binaire exploitable.
Pour un produit réel, préférez MLS ou une bibliothèque reconnue implémentant Signal plutôt qu’un échange artisanal. Les capacités cryptographiques varient entre navigateurs, ce qui peut imposer une couche de compatibilité ou un algorithme négocié. La publication du code, du modèle de menace et des limites favorise la revue ; une conférence TEDxFlint montre comment le partage de savoir peut nourrir cette culture collaborative.
Réutiliser les mêmes garanties avec Node.js
Les versions récentes de Node.js exposent globalThis.crypto et crypto.subtle. Un import explicite de webcrypto depuis node:crypto reste utile dans les environnements plus anciens ou les tests. Le même code peut donc s’exécuter dans un navigateur et dans un service, ce qui simplifie les scénarios hybrides, les tests de contrat et la migration progressive d’une application.
Dans une architecture confidentielle, Node.js ne reçoit pas les clés de session capables de lire les conversations. Il authentifie les participants, transporte des enveloppes opaques, applique les règles de livraison et mémorise les identifiants nécessaires au rejet des rejouements. Il doit surveiller les doublons, les sauts d’identifiants et les messages répétés. Les règles de rétention et les journaux d’exploitabilité évitent de copier accidentellement un contenu en clair.
Les tests cryptographiques doivent couvrir l’altération, les mauvaises clés, le mauvais contexte et la sérialisation réseau. Pour intégrer ces contrôles à chaque modification locale, le guide consacré aux hooks pre-commit avec Husky montre comment automatiser les vérifications. Ajoutez des tests interopérables entre le navigateur et Node.js, car une divergence sur l’encodage des octets suffit à rendre un message illisible.
Sécuriser les clés et limiter les métadonnées
La gestion des clés est souvent plus fragile que le chiffrement lui-même. Générez les clés privées avec extractable: false, conservez-les dans IndexedDB lorsque le navigateur le permet et ne placez jamais de secret brut dans localStorage. Une clé de récupération protégée par un mot de passe exige un sel unique, PBKDF2 avec un coût calibré ou Argon2 via une bibliothèque, ainsi qu’un processus sûr de réinitialisation.
Le chiffrement côté client ne masque ni les participants, ni les horaires, ni les tailles de messages, et il résiste mal à un appareil compromis. Réduisez les métadonnées, limitez la durée de conservation et chiffrez également les sauvegardes. Pour rechercher dans un historique, le comparatif des solutions de recherche aide à choisir un moteur, mais l’indexation centrale expose des corrélations et ne remplace jamais la protection du contenu. Une recherche plein texte nécessite alors un traitement côté client ou une construction spécialisée qui ne révèle que le minimum.
Un test d’intrusion doit considérer un serveur administrateur malveillant, un client volé, une extension navigateur compromise et une base de sauvegarde exposée. Le chiffrement protège le contenu tant que la clé ne quitte pas un environnement fiable. Ajoutez une vérification de l’identité des participants, une transparence des clés lorsque le contexte l’exige et une procédure de signalement des appareils perdus.
Recommandations pour un déploiement fiable
Une mise en production fiable repose sur une checklist courte, vérifiable et reliée aux menaces modélisées.
- Adoptez un protocole géré et audité pour l’échange de clés, et adaptez-le à votre plateforme plutôt que de recréer sa cryptographie.
- Générez une clé distincte par session, protégez les clés privées et excluez-les des sauvegardes en clair.
- Testez l’altération, le rejeu, la sérialisation et l’interopérabilité entre le navigateur et Node.js.
- Minimisez les journaux et les métadonnées, et chiffrez les sauvegardes contenant des enveloppes ou des clés protégées.
- Documentez le modèle de menace, la rotation, la révocation, la récupération et la réponse aux appareils compromis.
Pour passer à l’action, créez un prototype reproductible qui établit une session ECDH, chiffre un message avec AES-GCM, vérifie son intégrité et échoue proprement lors d’une altération. Exécutez-le dans le navigateur et dans Node.js, puis ajoutez un test qui sérialise les octets avant l’envoi. Enregistrez les enveloppes dans un format documenté, mesurez leur taille et partagez le prototype avec l’équipe de sécurité avant d’ouvrir le trafic réel.