Skip to content
Retour au blog
Sécurité

Échec du déchiffrement AES : clé, IV, mode et padding

Échec du déchiffrement AES ? Une mauvaise clé lève une erreur de padding, un mauvais IV n'abîme que le premier bloc. Déboguez le vôtre gratuitement en ligne.

16 min de lecture

Échec du déchiffrement AES : clé, IV, mode et padding

Quand vos journaux annoncent un échec du déchiffrement AES, le message que vous avez sous les yeux décrit probablement le mauvais problème. Quatre bugs sans rapport entre eux produisent des symptômes presque identiques, et le plus fréquent d’entre eux, une mauvaise clé, se signale par une erreur de padding.

Voici le classement qui fait gagner le plus de temps, en partant d’un déchiffrement CBC qui lève BadPaddingException :

  1. Les octets de la clé diffèrent d’un côté à l’autre. Cause la plus probable, et de loin.
  2. La dérivation de clé diffère. Même phrase secrète, KDF ou nombre d’itérations différent, donc octets de clé différents.
  3. Le texte chiffré a été abîmé en transit : tronqué, base64 malmené, ou passé par un aller-retour dans un encodage texte.
  4. L’IV est faux. Ça arrive, mais ça ne lève pas d’erreur de padding. Seize octets sont corrompus, et rien ne s’affiche.

Cet ordre n’a rien d’arbitraire. CBC vérifie le padding (remplissage) en toute dernière étape du déchiffrement, une fois la clé appliquée et la chaîne remontée : le padding est donc une somme de contrôle sur tout ce qui précède, et il échoue bruyamment quel que soit l’élément amont qui a cassé. Les mesures sont plus bas ; vous pouvez aussi coller tout de suite votre texte chiffré dans l’outil de déchiffrement AES et dérouler la bissection de la section 9.

Tout ce qui suit a été mesuré sur java 1.8.0_162, node v25.8.2 et openssl 3.6.2. Les valeurs par défaut changent d’une version à l’autre : les numéros de version font partie du résultat.

1. Commencez par ce que votre erreur élimine vraiment

Un message d’échec AES ne dit presque rien de la cause, mais beaucoup sur ce qu’elle ne peut pas être. Servez-vous-en pour éliminer des branches, pas pour en choisir une. Ces messages n’existent qu’en anglais, quelle que soit la locale de votre machine : cherchez la chaîne exacte plutôt que sa traduction. Le vocabulaire francophone, lui, flotte (déchiffrement ou décryptage, bourrage ou remplissage pour le padding, vecteur d’initialisation ou IV), alors que les bibliothèques ne connaissent qu’un seul jeu de noms.

Ce que vous voyezCe que ça élimineCe qui reste possible
BadPaddingException, bad decrypt, wrong final block lengthGCM ; une erreur d’IV seule ; un échec de décodagemauvaise clé, mauvais KDF, texte chiffré tronqué, octets de l’IV pris pour du texte chiffré, mode incompatible, schéma de padding incompatible
GCM Authentication failed, Unsupported state or unable to authenticate datale padding ; toute théorie impliquant une sortie partiellemauvaise clé, mauvais nonce, tag détaché ou mal placé, longueur de tag erronée, AAD différentes
Aucune exception, la sortie est illisibletous les modes authentifiésECB, CTR, un CBC qui a eu de la chance, mode incompatible, mauvais IV

BadPaddingException, bad decrypt, wrong final block length

Le même événement dans trois écosystèmes : Java, OpenSSL et .NET. Il se déclenche à la fin d’un déchiffrement CBC ou ECB, quand le dernier bloc de texte clair ne se termine pas par un motif PKCS#7 valide.

L’information utile est négative : arriver jusque-là signifie que votre base64 ou votre hexadécimal s’est décodé et que le nombre d’octets était un multiple non nul de 16 ; le transport n’a donc pas déchiqueté les données, et vous n’êtes pas en GCM. wrong final block length fait exception. Là, le compte n’était pas un multiple de 16, ce qui désigne une troncature plutôt que la clé : passez directement à la section 8.

GCM Authentication failed et compagnie

GCM compare le tag avant de livrer le moindre octet de texte clair, comme l’exige NIST SP 800-38D. Cela le rend honnête là où l’erreur de padding ne l’est pas : quelque chose dans le tuple (clé, nonce, texte chiffré, données authentifiées supplémentaires, tag) ne correspond pas à ce qu’a utilisé le chiffreur. Il ne peut pas vous dire lequel, et il ne le fera jamais : restreindre ce champ-là ne fait délibérément pas partie de son travail. La section 6 traite de l’élément qui casse le plus souvent d’un langage à l’autre : la position du tag, pas sa valeur.

Aucune erreur, mais la sortie est illisible

C’est le cas dangereux, parce qu’un tableau de bord l’enregistre comme un succès. CTR ne lève jamais d’exception, ECB non plus. CBC n’en lève que lorsque le motif d’octets final échoue au contrôle de padding, et avec une mauvaise clé cet octet est de fait aléatoire : environ une tentative sur 256 tombe sur 0x01 et passe la validation. Un peu moins de 0,4 % des déchiffrements CBC à mauvaise clé « réussissent ». Le charabia a une forme, cependant, et cette forme nomme le bug : les sections 4 et 5 contiennent les deux empreintes à retenir.

2. L’erreur la plus trompeuse d’AES

Une seule mesure suffit à réordonner les priorités de débogage de la plupart des gens. Clé 0123456789abcdef, IV entièrement à zéro, AES/CBC/PKCS5Padding, texte clair hello world, sur java 1.8.0_162 avec le fournisseur SunJCE intégré au JDK :

ScénarioModificationRésultat mesuré
AClé fausse d’1 octet (dernier caractère fX)lève javax.crypto.BadPaddingException: Given final block not properly padded. Le padding lui-même n’a jamais été malformé ; l’erreur est complètement trompeuse
BClé correcte, IV faux d’1 octetaucune exception, le texte clair hello world est revenu sous la forme iello world. Seul l’octet correspondant du premier bloc a été endommagé
CClé correcte, déchiffrement du texte chiffré CBC avec AES/ECBa réussi silencieusement, aucune exception. Une incompatibilité de mode n’a aucune obligation de lever quoi que ce soit

Le scénario A fait perdre des après-midi entières. Le scénario C envoie des données corrompues en production.

Pourquoi une mauvaise clé produit une erreur de padding

Rien n’allait de travers du côté du padding. Le chiffreur a ajouté cinq octets 0x05 pour amener hello world à seize, a chiffré ce bloc, et celui-ci se trouve intact dans votre texte chiffré.

L’échec se produit à la sortie. Le déchiffrement CBC fait tourner le chiffrement par blocs à l’envers, applique un XOR entre chaque résultat et le bloc de texte chiffré précédent, et seulement ensuite lit la fin du dernier bloc pour décider combien d’octets retirer. Avec la mauvaise clé, le chiffrement produit seize octets de bruit, et le bruit ne se termine presque jamais par un motif PKCS#7 valide. La bibliothèque rapporte ce qu’elle a vu, un padding invalide, ce qui est vrai et inutile.

Lisez BadPaddingException comme « le texte clair que j’ai reconstruit ne se termine pas comme se termine un texte clair paddé ». La raison la plus probable pour que votre reconstruction soit fausse, c’est la clé : voilà pourquoi une recherche sur aes decrypt wrong key et une recherche sur une bad padding exception vous amènent dans les mêmes fils de discussion, les deux symptômes n’en font qu’un. Note de conception au passage : n’exposez jamais cette distinction à un appelant, parce que c’est précisément de la différence entre « padding invalide » et « padding valide, contenu faux » que se nourrit une attaque par oracle de padding (Vaudenay, EUROCRYPT 2002).

Ce que GCM fait autrement

GCM inverse l’ordre : il vérifie le tag avant de produire le moindre texte clair, si bien qu’il n’existe aucune fenêtre où des octets partiellement corrects circulent. Un échec GCM ne vous laisse jamais vous demander si la sortie est réelle, puisqu’il n’y a pas de sortie. GCM n’a d’ailleurs aucun padding, puisque c’est un mode compteur sous le capot : la longueur du texte chiffré égale celle du texte clair. Une erreur de padding dans un système que vous croyiez en GCM prouve donc que le système n’est pas en GCM, en général une configuration retombée sur CBC.

3. Les deux côtés utilisent-ils les mêmes octets de clé ?

AES ne voit pas votre chaîne de clé. Il voit 16, 24 ou 32 octets. Deux systèmes peuvent avoir un matériel de clé identique dans un fichier de configuration et malgré tout diverger, parce qu’« identique » est une propriété du texte, pas des octets.

Les trois façons de transformer une chaîne de clé en octets

Passez la chaîne littérale 0123456789abcdef à trois bibliothèques différentes :

as hex          -> 8 bytes    (invalid AES key length)
as base64       -> 12 bytes   (invalid AES key length)
as raw UTF-8    -> 16 bytes   (valid AES-128)

Seize caractères, trois nombres d’octets. Le cas est vicieux précisément parce qu’il est valide dans les trois lectures : chaque caractère appartient à la fois à l’alphabet hexadécimal et à l’alphabet base64, et seize caractères, c’est une longueur légale pour les deux décodeurs, donc rien n’échoue au moment de l’analyse.

Le guide de dépannage de la signature JWT invalide contient la matrice complète, bibliothèque par bibliothèque, de la façon dont chaque écosystème interprète une chaîne de secret ; pour AES, en deux mots : notez dans quel encodage se trouve votre matériel de clé, et faites décoder explicitement des deux côtés. La forme HMAC du même bug mord les récepteurs de webhooks, sujet traité dans le guide de vérification de signature webhook.

AES est strict : exactement 16, 24 ou 32 octets

C’est là qu’AES diffère de la primitive que la plupart des développeurs rencontrent en premier. HMAC accepte n’importe quelle longueur de clé : la RFC 2104 hache tout ce qui dépasse la taille de bloc et complète par des zéros tout ce qui est plus court, si bien qu’un générateur HMAC accepte sans broncher un secret de 7 octets comme de 700 octets. AES n’a que trois longueurs de clé légales et rejette tout le reste avant de traiter le moindre bloc.

Cette rigueur est un cadeau : une erreur de longueur est le seul échec AES qui nomme sa propre cause au lieu de se cacher derrière le padding. Notre outil le formule ainsi : Key must be 16, 24, or 32 bytes (AES-128/192/256). Les pièges qui produisent une mauvaise longueur :

  • Un saut de ligne final issu de KEY=$(cat key.txt) ou d’echo "$KEY". Utilisez printf et echo -n. Une espace finale collée depuis l’interface d’un gestionnaire de secrets produit le même effet.
  • Un préfixe 0x copié depuis un débogueur : trente-quatre caractères qui ne sont plus de l’hexadécimal valide.
  • Des caractères non ASCII. contraseña fait 10 caractères et 11 octets en UTF-8 : une phrase secrète « de 32 caractères » comportant une seule lettre accentuée pèse 33 octets.

SecretKeySpec et le jeu de caractères par défaut de la plateforme

Java en possède une variante qui n’apparaît qu’après le déploiement. "my secret".getBytes() sans argument utilise le jeu de caractères par défaut de la plateforme, lequel provenait, avant le JDK 18, de la propriété file.encoding, donc du système et de la locale de la machine. Un portable en UTF-8 et un conteneur en ANSI_X3.4-1968 produisent des octets différents pour tout caractère non ASCII. Le JEP 400 a fait d’UTF-8 le défaut dans le JDK 18, ce qui corrige le code neuf et rien d’autre.

// faux : les octets dépendent de la machine
SecretKeySpec ks = new SecretKeySpec(secret.getBytes(), "AES");

// juste : les octets ne dépendent de rien
SecretKeySpec ks = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "AES");

Si votre code fonctionne en local, échoue sur le serveur avec une erreur de padding, et que la phrase secrète contient le moindre caractère hors ASCII, vérifiez ce point en premier.

4. L’IV : où il se range et à quoi ressemble un mauvais IV

Un aes iv mismatch est la panne que l’on soupçonne en premier et que l’on diagnostique en dernier, parce qu’elle ne se comporte pas comme les autres. Elle est silencieuse et elle est locale.

Un mauvais IV corrompt exactement un bloc

Reprenez le scénario B. Clé correcte, IV faux d’un octet :

hello world   ->   iello world

Aucune exception, un caractère. Écrivez l’étape CBC du premier bloc et c’est évident : P1 = D(C1) XOR IV. L’IV est appliqué en XOR directement sur le premier bloc de texte clair et ne touche rien d’autre : inverser un bit de l’IV inverse le même bit du texte clair, à la même position. Ici h (0x68) est devenu i (0x69), donc le premier octet de l’IV a bougé d’exactement 0x01.

L’empreinte. En CBC, si les 16 premiers octets sont illisibles et que tout le reste est propre, l’IV est faux et la clé bonne. Si tous les blocs sont illisibles, c’est la clé qui est fausse. Cette seule observation sépare les deux causes les plus courantes sans changer une ligne de code, et l’outil de déchiffrement AES affiche les octets décodés pour que vous puissiez la lire directement.

Pourquoi rien n’a été levé. hello world fait 11 octets, donc un seul bloc, et le padding PKCS#7 occupe les octets 11 à 15 de ce bloc. L’octet d’IV modifié était l’octet 0 : la zone de padding est restée intacte et a été validée. Corrompez un octet d’IV en position 11 ou au-delà et vous obtenez cette fois une erreur de padding, autre chemin par lequel l’erreur de padding vous ment.

Trois conventions de transmission

Il n’existe aucun standard pour l’emplacement de l’IV, seulement trois habitudes qui s’accordent mal entre elles.

En préfixe. iv || ciphertext, la convention la plus répandue et celle par défaut dans nos outils. Les deux côtés doivent s’accorder sur la quantité à retirer : 16 octets pour CBC et CTR, 12 pour GCM. Le bug en miroir, c’est un producteur qui préfixe et un consommateur qui ne retire rien. Les 16 premiers octets du « texte chiffré » sont alors l’IV, tous les blocs se décalent, et vous obtenez une erreur de padding.

Un champ séparé. {"iv": "...", "ciphertext": "..."} est plus propre sur le principe et double le nombre d’endroits où un encodage peut diverger, puisque l’IV a désormais sa propre question base64-ou-hexadécimal.

Une constante figée. En général tout à zéro, codée en dur parce que quelqu’un avait besoin de déterminisme. Elle interopère parfaitement, et c’est justement ce qui la rend dangereuse : en CBC un IV fixe révèle les égalités entre enregistrements, et en GCM réutiliser un nonce sous une même clé révèle le XOR des deux textes clairs et peut exposer la sous-clé GHASH qui authentifie le tag. SP 800-38D est explicite sur l’unicité.

L’option « texte chiffré brut » de l’outil, combinée à une saisie explicite de l’IV, teste les trois conventions sur les mêmes octets en une minute.

L’IV de GCM fait 12 octets, pas 16

Les équipes qui adoptent GCM en modifiant un chemin CBC existant emportent l’IV de 16 octets avec elles, et le résultat échoue sans le moindre indice.

SP 800-38D normalise un IV de 96 bits. D’autres longueurs sont permises, mais elles ne changent pas que la taille : quand l’IV ne fait pas 96 bits, GCM dérive son bloc compteur initial en faisant passer l’IV par GHASH au lieu de l’utiliser directement. Les mêmes 16 octets employés comme nonce produisent donc un flux de chiffrement et un tag complètement différents de ce qu’auraient donné les 12 premiers, et vous récoltez un échec d’authentification générique. Si le texte chiffré vient d’ailleurs et que vous devinez sa disposition, comptez à rebours : le tag est constitué des 16 derniers octets, le nonce presque toujours des 12 premiers.

5. Incompatibilité de mode, y compris la variante silencieuse

Cipher.getInstance("AES"), c’est ECB

Java vous laisse nommer un algorithme sans nommer ni mode ni schéma de padding. Il ne refuse pas et il n’avertit pas. Avec le fournisseur SunJCE intégré au JDK, il comble les blancs avec ECB et PKCS5Padding.

Encore faut-il la bonne expérience pour le prouver : chiffrez 32 octets identiques (deux blocs de A) avec la clé 0123456789abcdef, puis vérifiez si les deux blocs de texte chiffré coïncident. Sur java 1.8.0_162 :

getInstance("AES")           ciphertext = 3bfd04cc0d7ed55358e2cbe19de213833bfd04cc0d7ed55358e2cbe19de21383377222e061a924c591cd9c27ea163ed4
  block1 = 3bfd04cc0d7ed55358e2cbe19de21383
  block2 = 3bfd04cc0d7ed55358e2cbe19de21383   <- blocs identiques = l'empreinte ECB (la structure du texte clair fuit)
getInstance("AES/CBC/PKCS5Padding")  les blocs diffèrent = le chaînage est actif

Identiques octet pour octet. C’est la signature d’ECB, la propriété même qui fait que la fameuse image du pingouin chiffré ressemble encore à un pingouin. L’expérience ne fonctionne qu’avec des blocs de texte clair identiques : seize octets A suivis de seize octets B produisent deux blocs de texte chiffré différents sous ECB aussi, et vous en concluriez à tort que le défaut était CBC.

Ce résultat a une portée limitée : il décrit le fournisseur SunJCE intégré au JDK, dans la version ci-dessus. C’est le fournisseur qui décide de la transformation par défaut, si bien qu’un fournisseur tiers comme BouncyCastle peut résoudre le même raccourci autrement. La généralisation n’est pas « Java veut dire ECB », mais « une chaîne de transformation non qualifiée veut dire ce que votre fournisseur décide, raison pour laquelle on n’en écrit jamais ».

Le mauvais mode peut ne lever aucune erreur

Le scénario C a déchiffré du texte chiffré CBC avec AES/ECB et a renvoyé le bon texte clair sans aucune exception. Cela paraît impossible tant qu’on n’a pas posé le calcul. Le chiffrement CBC du premier bloc s’écrit C1 = E(P1 XOR IV), et le déchiffrement ECB de ce bloc donne D(C1) = P1 XOR IV. L’IV valait ici tout à zéro, donc P1 XOR 0 = P1 et le premier bloc se déchiffre parfaitement. hello world tient sur un bloc : « le premier bloc », c’était tout le message.

La règle générale mérite d’être retenue : avec un IV à zéro, ECB et CBC s’accordent sur le premier bloc et divergent sur tous les suivants. Déchiffrez un long message CBC comme de l’ECB et vous obtenez seize octets propres suivis de bruit, l’exact inverse de l’empreinte du mauvais IV. Deux formes opposées pour deux bugs différents, et aucun message d’erreur dans les deux cas. Les IV à zéro codés en dur sont assez répandus pour que ce ne soit pas une curiosité de laboratoire.

Ce que donne un appel minimal dans chaque langage

ÉcosystèmeAppel minimalMode réellement obtenu
Java (SunJCE)Cipher.getInstance("AES")ECB avec PKCS5Padding, en silence
Node cryptocreateDecipheriv('aes-256-cbc', key, iv)ce que dit la chaîne d’algorithme ; aucun défaut n’existe
Web Cryptocrypto.subtle.decrypt({ name: 'AES-CBC', iv }, ...)nommé explicitement ; ECB n’est pas implémenté du tout
Python cryptographyCipher(algorithms.AES(key), modes.CBC(iv))l’objet mode est obligatoire
PyCryptodomeAES.new(key, AES.MODE_ECB)argument obligatoire, mais ECB est là, dans l’autocomplétion
Go crypto/aesaes.NewCipher(key) renvoie un cipher.Block brutappeler Decrypt sur ce bloc, c’est de l’ECB ; enveloppez-le dans cipher.NewCBCDecrypter ou cipher.NewGCM
CryptoJSCryptoJS.AES.decrypt(ct, "passphrase")CBC, PKCS#7, EVP_BytesToKey avec MD5 (voir section 7)

Les écosystèmes où le mode vit dans une chaîne ou dans un objet ne vous surprennent jamais. Les deux qui proposent un appel « juste AES », Java et Go, sont ceux d’où proviennent les signalements d’ECB accidentel. Dans le doute, rejouez le même texte chiffré dans un autre mode avant de soupçonner la clé.

6. GCM : les mêmes octets, des API différentes

La plupart des échecs aes gcm auth tag entre langages ne sont pas cryptographiques. Les deux côtés ont calculé les mêmes 16 octets et divergent sur l’endroit où ces octets se trouvent.

La mesure

Clé = 32 octets 0123456789abcdef0123456789abcdef, IV = 12 octets à zéro, texte clair hello world, sur node v25.8.2 et java 1.8.0_162 :

Node   ciphertext = a616cd6d7d2328379d41e5                    (11 B)   <- update+final
       authTag    = c87af9f8ad7148e873fa797292c0af3f          (16 B)   <- récupéré à part via getAuthTag()
Java   doFinal()  = a616cd6d7d2328379d41e5c87af9f8ad7148e873fa797292c0af3f   (27 B)   <- texte chiffré et tag déjà concaténés

Node ciphertext || authTag est exactement Java doFinal(), ses 27 octets au complet. Aucune différence d’encodage, rien à négocier : Node vous tend les deux morceaux séparément et Java vous les remet collés. Notez aussi que 11 octets de texte clair ont donné 11 octets de texte chiffré, parce que GCM n’ajoute aucun padding. C’est pourquoi une erreur de padding ne peut jamais provenir d’un véritable chemin GCM.

Concaténé ou séparé, selon l’environnement d’exécution

Environnement d’exécutionAPI de chiffrementOù finit le tag
Node cryptoupdate() + final(), puis getAuthTag()séparé
Java (SunJCE, AES/GCM/NoPadding)doFinal()ajouté à la fin
Go cipher.AEADSeal()ajouté à la fin
Python cryptography, AESGCMencrypt()ajouté à la fin
Python cryptography, Cipher + modes.GCMfinalize(), puis encryptor.tagséparé
Web Cryptocrypto.subtle.encryptajouté à la fin

Node est l’intrus parmi les API de haut niveau, et c’est pourquoi « Node vers n’importe quoi » est la direction d’échec la plus signalée. Si vous ne savez pas laquelle des deux dispositions vous avez sous la main, comparez la position du tag sur vos propres octets. Emballer la sortie de Node pour un consommateur Java, Go, Python ou navigateur :

const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const ct = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const packed = Buffer.concat([ct, cipher.getAuthTag()]);   // correspond maintenant à doFinal()

Déballer un blob concaténé pour Node :

const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(packed.subarray(packed.length - 16));  // doit venir avant final()
const pt = Buffer.concat([
  decipher.update(packed.subarray(0, packed.length - 16)),
  decipher.final(),
]);

La contrainte d’ordre est réelle : appelez setAuthTag() après final() et Node lève Unsupported state or unable to authenticate data même quand chaque octet est correct.

La longueur du tag est variable, l’unité ne l’est pas

GCM autorise des tags de 128, 120, 112, 104 ou 96 bits, 64 et 32 étant réservés aux applications contraintes (SP 800-38D, annexe C). Presque tout le monde utilise 128, et l’ennui vient de la façon dont chaque API le réclame :

  • Java : new GCMParameterSpec(128, iv). Le premier argument est en bits.
  • Web Crypto : { name: 'AES-GCM', iv, tagLength: 128 }. Également en bits, 128 par défaut.
  • Node : createCipheriv(algo, key, iv, { authTagLength: 16 }). En octets.

new GCMParameterSpec(16, iv) est une ligne Java d’apparence tout à fait légale qui demande un tag de 16 bits ; certains JDK la rejettent, et là où elle passe, vous avez échangé votre garantie d’intégrité contre un tirage à une chance sur 65 536. Quand les deux côtés divergent sur la longueur du tag, les longueurs empaquetées diffèrent aussi : le destinataire découpe à la mauvaise frontière et récolte un échec d’authentification qui n’a rien à voir avec la clé.

7. Vous avez une phrase secrète, pas une clé

Si l’un des deux côtés accepte une chaîne tapée par un humain, il y a une fonction de dérivation de clé entre cette chaîne et AES, et une divergence de KDF est invisible. Elle n’échoue jamais. Elle renvoie 32 octets parfaitement valides qui se trouvent être les mauvais 32 octets, et la panne ressort une couche plus bas sous la forme, vous la connaissez, d’une erreur de padding.

PBKDF2 exige que quatre choses s’alignent

  • Le sel. Dans le format OpenSSL Salted__, ce sont 8 octets à l’intérieur du texte chiffré ; dans le format « phrase secrète » de nos outils, c’est un préfixe de 16 octets ; dans les schémas faits maison, c’est fréquemment une constante codée en dur.
  • Les itérations. openssl enc -pbkdf2 utilise 10 000 par défaut. L’OWASP recommande actuellement 600 000 pour PBKDF2-HMAC-SHA256, ce qu’emploie notre mode phrase secrète. Les frameworks choisissent leurs propres nombres.
  • Le hachage. SHA-1 contre SHA-256 contre SHA-512. Le code ancien et certains SDK mobiles utilisent encore SHA-1 par défaut.
  • La longueur de sortie. Trente-deux octets pour AES-256, seize pour AES-128. Certains schémas dérivent la clé et l’IV ensemble en un seul appel plus long, ce qui ne correspond jamais à une simple dérivation de 32 octets.

EVP_BytesToKey, et pourquoi CryptoJS continue de ne pas marcher

cryptojs aes decrypt not working correspond en général à une divergence bien précise. CryptoJS.AES.encrypt(text, "passphrase") n’utilise pas PBKDF2. Il utilise EVP_BytesToKey, la dérivation d’OpenSSL d’avant la 1.1, avec MD5 et une seule itération.

EVP_BytesToKey fait en plus quelque chose que PBKDF2 ne fait pas : il dérive la clé et l’IV à partir de la phrase secrète et du sel en une seule passe. C’est pourquoi un fichier OpenSSL Salted__ ne transporte aucun champ IV séparé, et pourquoi reproduire une sortie CryptoJS avec PBKDF2 plus un IV aléatoire est doublement faux.

Le format se reconnaît à l’œil nu : les 8 octets ASCII Salted__ suivis d’un sel de 8 octets, encodés en base64, commencent toujours par U2FsdGVkX1. Si votre texte chiffré commence ainsi, il est dérivé d’une phrase secrète et il vous faut savoir de quelle dérivation il s’agit ; l’outil de déchiffrement AES détecte le préfixe et bascule entre les trois sans toucher au code.

Pourquoi le même mot de passe donne des clés différentes

« Le mot de passe AES », ça n’existe pas. Chaque bibliothèque a inventé son propre chemin de la chaîne vers la clé :

ProducteurDérivationRésultat pour une même phrase secrète
CryptoJS AES.encrypt(text, pass)EVP_BytesToKey, MD5, 1 itérationclé A
openssl enc 1.0.2 et antérieursEVP_BytesToKey, MD5, 1 itérationclé A
openssl enc 1.1+ sans -pbkdf2EVP_BytesToKey, SHA-256, 1 itérationclé B
openssl enc -pbkdf2PBKDF2-HMAC-SHA256, 10 000 itérationsclé C
Notre mode phrase secrètePBKDF2-HMAC-SHA256, 600 000 itérationsclé D
Java, Python, Goaucun défaut ; c’est vous qui écrivez la dérivationce que vous avez écrit

Quatre clés à partir d’un seul mot de passe, et personne n’a encore commis d’erreur. Le passage de 1.0.2 à 1.1 a changé le condensat par défaut de MD5 vers SHA-256 : voilà pourquoi le texte chiffré produit par de vieux scripts a cessé de se déchiffrer avec la même commande sur une machine plus récente. Si vous avez hérité de données et que plus personne ne se souvient de la chaîne d’outils, essayez les dérivations dans cet ordre. C’est un test à trois branches, pas une recherche.

8. Ce que le transport a fait à vos octets

Un texte chiffré est du binaire uniformément aléatoire, ce qui en fait l’entrée la plus hostile qui soit pour n’importe quel code qui traite les octets comme du texte. Une grande part des échecs AES ne met jamais l’algorithme en cause.

Variantes de base64 et padding manquant

Le base64 standard (RFC 4648 §4) utilise + et / ; la variante URL-safe (§5) utilise - et _. Une chaîne URL-safe confiée à un décodeur standard lève une exception ou, dans les décodeurs permissifs, écarte silencieusement les caractères fautifs et renvoie des octets trop courts et désalignés ; c’est pourquoi Java livre Base64.getUrlDecoder() et Base64.getDecoder() comme deux objets distincts. Certains encodeurs suppriment aussi le = final, certains décodeurs l’exigent, et les chemins de code proches de JWT le retirent par défaut.

Avant de soupçonner la clé, décodez le texte chiffré et confrontez sa longueur au mode :

  • CBC et ECB : un multiple non nul de 16. Toute autre valeur signale une troncature ou un problème de décodage, pas un problème de clé.
  • GCM : la longueur du texte chiffré égale celle du texte clair, plus 16 pour le tag, plus 12 en tête si le nonce est préfixé.
  • CTR : n’importe quelle longueur, donc ce contrôle ne vous apprend rien.

Le décodeur Base64 vous donne le nombre d’octets en un seul collage, souvent la mesure la plus rapide de toute l’enquête.

Sauts de ligne, guillemets typographiques et aller-retour UTF-8

openssl base64 replie sa sortie à 64 colonnes sauf si vous passez -A, et certains décodeurs ignorent les sauts de ligne intégrés quand d’autres les rejettent : le même fichier se décode sur une machine et échoue sur une autre. Copier via une messagerie ou un éditeur de documents transforme les guillemets droits en guillemets courbes et les traits d’union en tirets demi-cadratins, et la différence est presque invisible dans un terminal.

L’irrécupérable, c’est l’aller-retour UTF-8. Si une sortie AES brute est un jour conservée sous forme de chaîne sans avoir été encodée d’abord (new String(cipherBytes) en Java, bytes.decode('utf-8', errors='replace') en Python, un TextDecoder n’importe où), chaque séquence d’octets qui n’est pas de l’UTF-8 valide s’effondre en U+FFFD, et la réencoder vous donne EF BF BD là où se trouvaient vos données. Comme environ la moitié des octets aléatoires sont non ASCII, l’essentiel du texte chiffré est détruit et aucune clé ne le récupère ; le guide de l’encodage UTF-8 et UTF-16 explique pourquoi la perte est à sens unique. Un texte chiffré binaire voyage en base64 ou en hexadécimal, ou il voyage en binaire. Jamais sous forme de chaîne.

Colonnes de base de données

Le stockage inflige les mêmes dégâts, en plus discret. Un texte chiffré écrit dans un VARCHAR(255) alors qu’il dépasse d’un bloc se fait couper, et MySQL hors mode strict le fait sans erreur. C’est dans la queue que vivent le bloc de padding et le tag GCM : une ligne écrite « avec succès » il y a des mois échoue aujourd’hui, et si la coupe est tombée sur une frontière de 16 octets, le contrôle de longueur ci-dessus ne l’attrapera pas non plus. La conversion de jeu de caractères fait le reste : une colonne latin1 qui reçoit des octets UTF-8 réécrit vos données à l’entrée.

Stockez le texte chiffré dans un VARBINARY, un BLOB ou un bytea, ou stockez du base64 dans une colonne texte avec de la marge.

9. Une procédure de bissection qui trouve la cause en cinq minutes

Chaque section ci-dessus resserre une variable. Déroulez-les dans l’ordre face à une implémentation de référence que vous contrôlez et ça converge vite. Les outils en navigateur conviennent bien pour ça : tout s’exécute localement, vos clés et votre texte chiffré ne quittent jamais la page, et vous changez un réglage à la fois pour voir les octets.

  1. Étape 0 : mesurez la forme. Décodez le texte chiffré et notez le nombre d’octets, les premiers octets, et s’il commence par U2FsdGVkX1. Confrontez le compte à la section 8. S’il n’est pas un multiple de 16 et que vous vous croyez en CBC, arrêtez-vous : c’est un bug de transport.

  2. Étape 1 : chiffrez un texte clair connu. Dans l’outil de chiffrement AES, chiffrez une courte chaîne connue avec les paramètres que vous croyez utilisés en production, puis comparez la forme des deux sorties plutôt que leurs valeurs : longueur totale, octets de préfixe, présence d’un en-tête de sel. Un écart signifie que votre hypothèse sur le format ou sur le KDF est fausse, et vous aurez beau bidouiller la clé, ça n’y changera rien.

  3. Étape 2 : passez les dérivations en revue. Pour des données dérivées d’une phrase secrète, lancez PBKDF2 avec le nombre exact d’itérations, puis EVP-SHA256, puis EVP-MD5 dans l’outil de déchiffrement AES. Une seule peut être la bonne. Si aucune ne marche, le bug se situe au-dessus du KDF.

  4. Étape 3 : supprimez toutes les conventions. Passez à une clé brute, activez le texte chiffré brut, fournissez l’IV explicitement. Vous déclarez désormais exactement quels octets sont la clé, l’IV et le texte chiffré, sans rien laisser deviner. Si ça se déchiffre ici mais pas dans votre code, votre bug est un bug de cadrage (un préfixe d’IV non retiré, un tag au mauvais endroit) plutôt qu’un bug cryptographique.

  5. Étape 4 : changez de mode. Essayez CBC, puis CTR, puis GCM sur les mêmes octets. Si CTR renvoie du texte lisible là où CBC a échoué, c’est une incompatibilité de mode, point final.

  6. Étape 5 : lisez le charabia. Premier bloc mauvais et le reste propre : c’est l’IV. Premier bloc propre et le reste mauvais : vous avez déchiffré du CBC comme de l’ECB avec un IV à zéro. Tout mauvais : c’est la clé ou la dérivation.

10. Questions fréquentes

Pourquoi mon code AES fonctionne-t-il en local et échoue-t-il en production ?

Entre le local et la production, l’environnement a changé quelque chose qui n’est pas sous gestion de version. Suspects habituels, dans l’ordre : la clé est arrivée d’une variable d’environnement ou d’un gestionnaire de secrets avec un saut de ligne final ; le jeu de caractères par défaut de la plateforme Java diffère entre le portable et le conteneur, donc getBytes() a produit des octets différents (section 3) ; l’OpenSSL de production est en 1.1+ alors que vos scripts locaux visaient la 1.0.2, ce qui fait passer le condensat d’EVP_BytesToKey de MD5 à SHA-256 ; ou bien une colonne de base de données tronque le texte chiffré dans un seul environnement. Affichez d’abord la longueur de la clé et celle du texte chiffré en octets des deux côtés : ces deux nombres tranchent en général la question.

J’ai chiffré dans Node et je n’arrive pas à déchiffrer en Java. Par où commencer ?

Entre Node et Java, commencez par le tag GCM, la cause la plus fréquente et la moins évidente. Node renvoie le texte chiffré et le tag séparément ; le doFinal() de Java les attend concaténés sous la forme ciphertext || tag, et la section 6 montre que les octets sont identiques par ailleurs. Si vous êtes plutôt en CBC, commencez par la convention d’IV : Node l’a-t-il préfixé, et le côté Java retire-t-il 16 octets avant de déchiffrer ? En troisième vient la clé elle-même, où Buffer.from(k, 'hex') et k.getBytes(StandardCharsets.UTF_8) produisent des longueurs différentes à partir de la même chaîne.

Le PKCS5Padding de Java est-il la même chose que PKCS#7 ?

Pour AES, oui : le PKCS5Padding de Java et PKCS#7 sont la même chose dans les faits. PKCS#5 (RFC 8018) n’est défini que pour des blocs de 8 octets ; PKCS#7 (RFC 5652) généralise le schéma à des tailles de bloc de 1 à 255 octets. Le PKCS5Padding de Java appliqué à un chiffrement par blocs de 16 octets implémente le comportement de PKCS#7, et le nom n’est qu’un reliquat historique : ce n’est donc jamais votre bug. NoPadding, si : il exige un texte clair déjà multiple de 16, et au déchiffrement il vous rend le padding en guise de données, d’où un texte plausible suivi d’octets comme \x05\x05\x05\x05\x05.

Ma clé fait 32 caractères, mais AES dit que la longueur de clé est invalide. Pourquoi ?

Une erreur de longueur signifie que la bibliothèque a reçu un nombre d’octets qui n’est ni 16, ni 24, ni 32. Avec une chaîne de 32 caractères, c’est en général un saut de ligne final (33 octets), un préfixe 0x qui rend la chaîne invalide en hexadécimal, ou un caractère non ASCII occupant deux ou trois octets en UTF-8. La variante la plus dangereuse ne produit aucune erreur du tout : 32 caractères hexadécimaux se décodent en 16 octets valides et 32 caractères base64 en 24 octets valides, deux longueurs AES légales. La bibliothèque les accepte, utilise la mauvaise clé, et vous rend à la place un échec de padding. Vérifiez le nombre d’octets, pas le nombre de caractères.

Le déchiffrement a « réussi » mais la sortie est du charabia. Que s’est-il passé ?

Ce charabia veut dire que le déchiffrement a « réussi » dans un mode qui ne vérifie rien. CTR et ECB ne lèvent jamais d’exception, et CBC n’en lève que lorsque le motif d’octets final échoue au contrôle de padding, ce qu’une mauvaise clé franchit un peu moins de 0,4 % du temps. Lisez la forme : 16 premiers octets corrompus et le reste propre, c’est l’IV ; 16 premiers propres et le reste corrompu, vous avez déchiffré du texte chiffré CBC comme de l’ECB avec un IV à zéro ; corruption uniforme, c’est la clé ou la dérivation. Un texte lisible suivi de quelques octets bizarres, c’est NoPadding sur des données paddées. Le correctif de fond, c’est GCM, pour que « réussi » veuille dire quelque chose.

Puis-je encore déchiffrer si j’ai perdu l’IV ?

Sans l’IV, vous pouvez encore déchiffrer en CBC tout sauf les 16 premiers octets. Les blocs à partir du deuxième se récupèrent par D(C_i) XOR C_{i-1}, et toutes les entrées de ce calcul sont déjà dans le texte chiffré ; seul le premier bloc a besoin de l’IV. Si vous savez en plus comment débute le texte clair, disons un blob JSON commençant par {"userId":, vous pouvez récupérer l’IV directement par D(C1) XOR P1. En CTR, l’IV amorce tout le flux de chiffrement : le perdre, c’est tout perdre. En GCM, le nonce alimente à la fois le compteur et le tag, il n’y a donc pas de récupération partielle.

Puis-je récupérer le texte clair si le tag GCM a été tronqué ou perdu ?

Avec un tag GCM tronqué ou perdu, le texte clair reste récupérable mathématiquement, en pratique au prix d’un effort. GCM est du mode CTR sous le capot : la clé et le nonce suffisent à reproduire le flux de chiffrement. Aucune bibliothèque courante ne le fera pour vous : Java, Go, Python et Web Crypto refusent tous de livrer du texte clair sans tag valide, et c’est voulu. Le contournement consiste à déchiffrer les mêmes octets en AES-CTR avec un bloc compteur initial fixé au nonce de 12 octets suivi de 00000002, là où commence le premier bloc de données de GCM. Vous récupérez les données et vous abandonnez toute garantie d’intégrité : traitez le résultat comme non fiable. Si vous avez toujours les 16 octets du tag et que l’authentification échoue quand même, le tag n’est pas manquant et votre bug est ailleurs sur cette page. Emmenez-le dans l’outil de déchiffrement AES et reprenez à l’étape 0.

Tags: aes encryption debugging cryptography interoperability

Articles connexes

Voir tous les articles