Inglorious Astar

Implémentation d'AES : la nitroglycérine

Retour sur les pièges qui jalonnent les sentiers du secret

Public cible :
Dev
-
Temps de lecture :
40
min

Cet article est une reprise de celui que j'ai publié dans MISC n°85.

AES est une solution de chiffrement puissante. Ceci implique notamment qu'elle doit être manipulée avec soin et ne pas être laissée entre les mains de développeurs trop pressés. Comme pour la plupart des chiffrements modernes, les failles résultent essentiellement d'une mauvaise utilisation/implémentation plutôt que d'un problème intrinsèque.

« Ne pas ingérer, ne pas laisser à portée des enfants »

Pour le profane, le chiffrement (quand ce n'est pas le cryptage) se résume à faire passer un « clair » dans une boîte noire, à en retirer le « chiffré » et roulez jeunesse. Pour cela, nombre ont recours à l'algorithme AES réputé pour son haut niveau de sécurité.

Malheureusement, AES peut rapidement devenir votre pire ami si vous ne savez pas ce que vous faites. Si vous utilisez AES et que vous n'avez jamais entendu parlé de « mode CTR » ou de « vecteur d'initialisation »... vous devriez probablement vous abstenir d'utiliser AES .

De l'utilisateur au développeur qui implémente l'algorithme, en passant par ceux qui écrivent les documentations, retour sur les pièges qui jalonnent les sentiers du secret.

AES : résumé de l'épisode précédent

Inutile de s'étendre sur cette super-star de la cryptographie. Néanmoins certaines notions précises nous seront utiles plus tard.
Ce chiffrement est le nouveau standard étasunien (et donc international) de cryptographie symétrique depuis 2001. La plupart des langages en intègrent aujourd'hui des implémentations (plus ou moins fidèles).

Il s'agit d'un chiffrement par bloc. Comme pour tout chiffrement de ce type, il possède plusieurs modes de fonctionnement : CBC, ECB, CTR, etc. Chacun de ces modes possède ses forces et ses faiblesses et le choix de l'un d'eux dépend du contexte d'utilisation.

AES supporte également plusieurs tailles de clés : 128, 196 ou 256 bits. Le choix dépend des besoins de sécurité et des performances.

Trouvez les erreurs :

Au moins 3 erreurs se sont glissées dans cet exemple d'utilisation qui fait appel à l'implémentation AES de MySQL, au travers du framework PHP CodeIgniter :

$input[] = array($db_field, "TO_BASE64(AES _ENCRYPT('" . self::$CI->db->escape_str($db_value) . "', '" . self::$CI->config->config['encryption_key'] . "'))", FALSE);

L'objectif de cette requête est de chiffrer le contenu d'une base de données.CodeIgniter conseille de stocker la clé de chiffrement dans un fichier config.php, ce qu'a suivi notre développeur :

$config["encryption_key"] = "fgEghn9ET7ZRFzDAUcmS5sCQZ2Ar2YHk";

« Par défaut » les deux mots préférés des pentesteurs

Intéressons-nous à l'utilisation par défaut de la fonction AES_ENCRYPT de MySQL :

Prototype : AES_ENCRYPT(str,key_str[,init_vector])
Description : "AES_ENCRYPT() and AES_DECRYPT() implement encryption and decryption of data using the official AES (Advanced Encryption Standard) algorithm, previously known as “Rijndael.”. By default these functions implement AES with a 128-bit key length. AES_ENCRYPT() encrypts the string str using the key string key_str and returns a binary string containing the encrypted output."

For a key length of 128 bits, the most secure way to pass a key to the key_str argument is to create a truly random 128-bit value and pass it as a binary value. For example:

INSERT INTO t VALUES (1,AES_ENCRYPT('text',UNHEX('F3229A0B371ED2D9441B830D21A390C3')));

A passphrase can be used to generate an AES key by hashing the passphrase. For example:

INSERT INTO t VALUES (1,AES_ENCRYPT('text', SHA2('My secret passphrase',512)));

Nous voyons que l'argument init_vector est optionnel. Cela signifie que, par défaut, le mode de chiffrement n'utilise pas de vecteur d'initialisation... hummm ça ne vous met pas la puce à l'oreille ? La suite de la documentation stipule :

The block_encryption_mode system variable controls the mode for block-based encryption algorithms. Its default value is aes-128-ecb, which signifies encryption using a key length of 128 bits and ECB mode. For a description of the permitted values of this variable, see Section 5.1.4, “Server System Variables.

Haaaaa AES ECB, c'était prévisible puisqu’il s’agit du mode le plus simple qui ne nécessite pas de vecteur d’initialisation. Dans ce mode, le chiffrement d’un bloc ne dépend pas des précédents :

ECB

ECB, CBC, OFB et autres noms barbares ne sont pas familiers de la plupart des utilisateurs. Pourtant les différences sont fondamentales et les usages radicalement différents.

Par exemple, le mode ECB, ici présent, implique que deux blocs en clair, identiques, seront transformés en deux blocs chiffrés identiques. Au contraire du mode CBC, dit « chaîné », où le chiffrement d’un bloc fait aussi intervenir le précédent, assurant que l’on ne chiffrera « jamais » deux informations identiques de la même façon :

CBC

Comme un dessin vaut mieux qu’un long discours, voici une illustration (issue de Wikipédia) démontrant le niveau de sécurité du mode ECB appliqué sur des données pouvant être identiques (ici des couleurs) et comparé à n’importe quel autre (comme CBC) qui utilise un vecteur d’initialisation :

CBC vs ECB

Ce n’est certainement pas un mode recommandé pour l'écriture de données en base (où il y a de fortes chances que plusieurs données soient identiques).
Le mode ECB est donc adapté aux cas où l’on ne chiffre jamais deux fois avec la même clé ou lorsque l’on est sûr que l’on n’aura jamais deux informations identiques à chiffrer. Ce qui est rarement le cas. En faire un mode par défaut est donc pernicieux.
Typiquement ce mode est vulnérable aux attaques par clairs choisis. Cette pratique est comparable, en termes de risque, au fait de stocker des mots de passe sous forme de hash mais sans utiliser de sel.

Positionner le mode ECB comme mode par défaut était certes compréhensible, puisque cela évite d’ennuyer l’utilisateur avec un vecteur d’initialisation. Pour autant, ce mode relève d’une utilisation tellement spécifique qu’elle ne conviendra pas, en termes de sécurité, à 90 % des usages.

Les modes de chiffrement à privilégier sont CBC, GCM, CTR avec des vecteurs d’initialisation aléatoires et différents entre chaque chiffrement.

Votre mot de passe doit contenir un chiffre, une majuscule, une minuscule, un hiéroglyphe, un nombre premier et le sang d'une vierge

Ceci était la première erreur et gageons qu'elle touche un grand nombre d'utilisateurs de la fonction AES_ENCRYPT de MySQL.
La deuxième erreur est plus amusante.

Revenons à ce que dit la documentation de MySQL sur le choix de la clé :

For a key length of 128 bits, the most secure way to pass a key to the key_str argument is to create a truly random 128-bit value and pass it as a binary value.

Mais celle de CodeIgniter nous dit :

To take maximum advantage of the encryption algorithm, your key should be 32 characters in length (128 bits). The key should be as random a string as you can concoct, with numbers and uppercase and lowercase letters. Your key should not be a simple text string. In order to be cryptographically secure it needs to be as random as possible. »

Dans notre cas, il semble que le développeur ait davantage suivi les conseils du framework PHP qu'il utilise plutôt que ceux de la base de données puisque sa clé est, rappelons-le :

$config["encryption_key"] = "fgEghn9ET7ZRFzDAUcmS5sCQZ2Ar2YHk";

Bien que cette recommandation soit adaptée pour les fonctions de chiffrement de CodeIgniter (qui appliquent une fonction de hachage sur la clé soumise), nous allons voir qu'elle l'est beaucoup moins pour l'implémentation de MySQL.

Faire feu de tout bois

Il s'avère que la fonction AES_ENCRYPT est à peu près capable de recevoir n'importe quel format ou taille de clé pour effectuer un chiffrement :

mysql> SELECT HEX(AES_ENCRYPT("test",UNHEX("0FE456577AA3C492B")));
8C2FB4D04D658F78E7F76436876C5E38

mysql> SELECT HEX(AES_ENCRYPT("test","0FE456577AA3C492B"));
711DB1FF78523CB456D20031BC3529BF

mysql> SELECT HEX(AES_ENCRYPT("test","MONPASSWORD"));
D952B6E920F1937083CCC848F0D371D0

mysql> SELECT HEX(AES_ENCRYPT("test",SHA2("0FE456577AA3C492B",512)));
701D32973D60142364C79C4BD0CA894D

Pour comprendre comment AES_ENCRYPT va gérer la clé qui lui est soumise, il est nécessaire de se pencher sur son code source.

Nous apprenons que c'est bien une clé binaire de 128 bits qui est utilisée par l'algorithme pour mener les opérations de chiffrement. Une phase de pré-processing est donc menée en amont, afin de transformer toute clé ne se trouvant pas dans un format binaire, en une valeur utilisable.

Si la clé est une chaîne de caractères (cf. les trois derniers exemples ci-dessus), chaque caractère est converti en sa valeur binaire sur un octet (via la table du code ASCII) : « a » = 00111101, plus simplement écrit 0x61 en notation hexadécimale.

Si la clé est trop courte, des 0 seront ajoutés jusqu'à obtenir 128 bits.

Si elle est trop longue, elle sera découpée en sous-parties de 128 bits qui seront « XORées » entre elles. Astucieux n'est-ce pas ? (En réalité c'est pure folie, mais nous y reviendrons)

Ainsi, la clé password sera transformée en ASCII : 70617373776F7264, ce qui représente 8 octets (64 bits). Elle sera donc complétée par 8 autres octets : 0000000000000000 puis finalement interprétée sous forme binaire.
Nous pouvons en effet vérifier que ces deux formats sont équivalents :

mysql> SELECT HEX(AES_ENCRYPT("test","password"));
265780F7532F6077447678F72981E6A2    

mysql> SELECT HEX(AES_ENCRYPT("test",UNHEX("70617373776f72640000000000000000")));
265780F7532F6077447678F72981E6A2

Le fait de recommander d'utiliser des chiffres, majuscules et minuscules conduit donc à ce que chaque octet de la clé puisse prendre 62 valeurs différentes (qui correspondent respectivement aux valeurs ASCII allant de 31 à 39, de 41 à 5A et de 61 à 7A).

Tout cela est très bien, si ce n'est qu'il existe 256 valeurs possibles pour un octet et que le fait de recourir à une clé utilisant majuscules, minuscules et chiffres exclut 194 d'entre elles (par exemple 'FF' n’apparaîtra jamais car ce n'est pas un caractère imprimable de la table ASCII)...
75 % des valeurs possibles ne seront jamais utilisées.

Sachant que pour itérer les 16 octets (128 bits) de la clé AES dans une attaque par force brute il faudrait normalement : 25616 = 340282366920938463463374607431768211456 essais.
Avec une clé sous forme d'une chaîne de 16 caractères (128 bits) le nombre de clés à tester pour avoir parcouru toutes celles possibles est : 6216 = 47672401706823533450263330816.

Donc un attaquant n’aura besoin de tester « que » 0.000000014% des clés possibles !!
Il serait malhonnête d'éluder que cela implique encore quelques milliers de milliards d'années, mais avec une réduction de 99.999999986 % des clés à tester, pour la NSA, c'est quand même les soldes.

Il n'existe pas un cas où le fait d'accepter des chaînes de caractères lorsque l'algorithme travaille au niveau binaire ne soit pas une ineptie. Autoriser ce comportement sans appliquer les traitements nécessaires en aval (hacher la clé et utiliser la forme binaire du condensat) ne peut qu’entraîner les utilisateurs lambda dans un piège en les laissant croire qu'ils recourent à une sécurité maximale.

Mettre un cadenas à vélo sur une Ferrari

« Bien » me direz-vous « la belle affaire, c'est quoi ce développeur qui ne suit pas les bonnes docs, il suffit de suivre les recommandations de MySQL quand on utilise une fonction de MySQL et puis c'est bon… Remboursez, remboursez !! »

Hé hé hé, les recommandations de MySQL, en fait... c'est encore pire. Examinons :

For a key length of 128 bits, the most secure way to pass a key to the key_str argument is to create a truly random 128-bit value and pass it as a binary value. For example: INSERT INTO t VALUES (1,AES_ENCRYPT('text',UNHEX('F3229A0B371ED2D9441B830D21A390C3')));
A passphrase can be used to generate an AES key by hashing the passphrase. For example: INSERT INTO t VALUES (1,AES_ENCRYPT('text', SHA2('My secret passphrase',512))); Do not pass a password or passphrase directly to crypt_str, hash it first. Previous versions of this documentation suggested the former approach, but it is no longer recommended as the examples shown here are more secure.

Le fait d'utiliser SHA2 assure effectivement que quel que soit le mot de passe utilisé, on soumettra au final une clé ayant une forte entropie. Cela évite de laisser les utilisateurs se démener avec des générateurs de nombres aléatoires (comme dans l'ancienne version) que peu savent maîtriser convenablement.

Cependant, dans l'exemple donné par la documentation, il manque un détail crucial : le recours à la fonction UNHEX(). En l'absence de cette fonction, le condensat est interprété comme une chaîne de caractères.
Preuve avec un condensat arbitraire de 256 bits :

# la fonction SHA2 renvoie cette chaine de caractères
mysql> SELECT SHA2("test",256);
9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08

# si on l'utilise pour chiffrer "test2" ça donne ça
mysql> SELECT HEX(AES_ENCRYPT("test2",SHA2("test",256)));
0F5FC068F809D969A70D5229942E3520  

# c'est exactement équivalent à chiffrer avec la chaine de caractères renvoyée par SHA2
mysql> SELECT HEX(AES_ENCRYPT("test2","9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"));
0F5FC068F809D969A70D5229942E3520

# et c'est donc différent d'un chiffrement avec les bits produits par SHA2
mysql> SELECT HEX(AES_ENCRYPT("test2",UNHEX("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08")));
1FD3DC880854A8CD577E8BC4CD54C850

Nous voyons bien qu'utiliser telle quelle la fonction SHA2 revient à utiliser une chaîne de caractères hexadécimaux et non pas une chaîne binaire en hexadécimal.

Le détail fondamental c'est que c'est la valeur binaire de la clé qui a une forte entropie, non pas sa valeur ASCII. Nous avons vu que si l'espace des caractères possibles est restreint à 62 (majuscules, minuscules, chiffres), la chute en complexité est pharaonique.
Ici le condensat est exprimé en hexadécimal soit... 16 caractères différents possibles (les valeurs ASCII de 30 à 39 pour les chiffres et de 61 à 66 pour les lettres).

Intuitivement, on est amené à penser que 9f est le premier octet de la clé et qu'il est une valeur parmi les 256 possibles. En réalité c'est 9 en tant que caractère qui constitue le premier octet et il est une valeur parmi 16.

Pour autant, à ce stade, cela ne pose encore aucun problème. Ce faible nombre de possibilités différentes, par octet, est exactement compensé par la taille de la clé qui est alors deux fois plus longue.
En effet, chaque octet du condensat (9f par exemple) sera codé par deux octets dans la représentation ASCII ('9'=39 et 'f'=66).
Donc nous sommes en présence d'un espace de clés possibles équivalent à 1632 (exactement équivalent à 25616) et non pas 1616 (ce qui serait le cas si une clé hexadécimale de 64 bits avait été soumise).

Tout irait très bien dans le meilleur des codes, s'il n'était notre troisième et ultime erreur.

Trop de clé, tue la clé

Pour déceler la dernière erreur que recèle le code donné en exemple, revenons sur ce que dit la documentation de MySQL sur la taille de clé par défaut :

By default these functions implement AES with a 128-bit key length.

Et nous avions vu que notre développeur a suivi la documentation de CodeIgniter pour construire sa clé ("fgEghn9ET7ZRFzDAUcmS5sCQZ2Ar2YHk").
Mais un détail vous a peut être échappé... Si je vous dis qu'un caractère ASCII pèse 8 bits (1 octet) ?

$ python
>>> 128/8
16
>>> len("fgEghn9ET7ZRFzDAUcmS5sCQZ2Ar2YHk")
32

C'est bien cela. Nous avons une clé de chiffrement de 256 bits (32 caractères) que nous passons à un algorithme AES 128 bits (hashtag yolo).

Apparemment 128 c'est pas tendance

Mais que reprocher à notre développeur ? La documentation de CodeIgniter stipule bien :

To take maximum advantage of the encryption algorithm, your key should be 32 characters in length (128 bits). The key should be as random a string as you can concoct, with numbers and uppercase and lowercase letters. Your key should not be a simple text string. In order to be cryptographically secure it needs to be as random as possible.

Nous devons donc décerner l'Oscar des meilleurs effets spéciaux à CodeIgniter qui a réussi la prouesse « 32 octets = 128 bits ».

De son côté, la documentation de MySQL ne semblait, de toute façon, pas tellement plus encline à soutenir le fait d'utiliser 128 bits de clé pour AES 128 bits (tellement banal) :

A passphrase can be used to generate an AES key by hashing the passphrase. For example: INSERT INTO t VALUES (1,AES_ENCRYPT('text', SHA2('My secret passphrase',512)));

Donc là, on passe carrément une clé de 512 bits, qui, de plus, sera convertie en ASCII via 1024 bits (si vous avez suivi la section précédente). Or nous allons voir que ce n'est pas la longueur de bit qui compte, mais la façon dont on s'en sert.

Chef ça dépasse je fais quoi ? Bricole un truc, sois dérouillard un peu

Mais quel peut être le comportement de AES_ENCRYPT en cas de soumission d’une clé trop longue ? C’est pas triste : la fonction va découper la clé en sous-chaînes de 128 bits qui seront « XORées » entre elles.

Intro

Exemple : la clé hexadécimale suivante (256 bits) : 1010101010101010101010101010101002020202020202020202020202020202 sera transformée en une clé de 128 bits produite en effectuant un XOR sur les deux moitiés, donc : 12121212121212121212121212121212.
Ces deux clés sont donc parfaitement équivalentes. Ce que confirme d’ailleurs MySQL :

mysql> SELECT HEX(AES_ENCRYPT("test",UNHEX("1010101010101010101010101010101002020202020202020202020202020202")));
84553BE0FC75464894ED41AE67821170

mysql> SELECT HEX(AES_ENCRYPT("test",UNHEX("12121212121212121212121212121212")));
84553BE0FC75464894ED41AE67821170

Nous avions conclu la partie précédente en disant que la perte du nombre de possibilités pour chaque octet (16 au lieu de 256) était compensée par la longueur alors augmentée (256 bits au lieu de 128). Nous voyons maintenant que peu importe que l'on donne, 256, 512, 1024 bits, etc., la taille de la clé est forcément ramenée à 128 bits.
Donc il est inutile de surenchérir et de jouer à qui a la plus grosse clé.

C'est là que les bits s'abêtirent

Prenons le cas simplifié d'un condensat de 128 bits : 4d86d081884c7d659a2feaa0cd6ada32 soumis sous forme de chaîne de caractères. Chaque caractère sera interprété en ASCII, ce qui donnera la chaîne binaire suivante : 3464383664303831383834633764363539613266656161306364366164613332 qui fait 256 bits.
La chaîne sera donc coupée en deux et un XOR sera effectué sur ces deux parties :

>>> 0x34643836643038313838346337643635^0x39613266656161306364366164613332
0d050a50015159015b5c020253050507

C'est cette chaîne binaire finale qui sera utilisée comme clé. Nous pouvons le vérifier :

mysql> SELECT HEX(AES_ENCRYPT("test","4d86d081884c7d659a2feaa0cd6ada32"));
00102423C2CDED7FAEB63207B595391C

mysql> SELECT HEX(AES_ENCRYPT("test",UNHEX("0d050a50015159015b5c020253050507")));
00102423C2CDED7FAEB63207B595391C

Or cette chaîne binaire finale est fortement biaisée. Le condensat de départ ne contient que des caractères hexadécimaux, donc 16 possibilités différentes lors de la conversion en valeur ASCII. Les chiffres de 0 à 9 correspondent aux valeurs allant de 30 à 39 et les lettres de 'a' à 'f' correspondent aux valeurs allant de 61 à 66.

Premièrement, les demi-octets impairs ne peuvent valoir que 3 (si c'est un chiffre) ou 6 (si c'est une lettre). Après l'opération de XOR, le demi-octet produit ne peut être que 0 (3 XOR 3 ou 6 XOR 6) ou 5 (3 XOR 6 ou 6 XOR 3).
Ceci implique que sur les 128 bits de la clé, un demi-octet sur deux ne peut valoir que 0 ou 5 (au lieu des 16 valeurs normalement équiprobables).
L'entropie de ces demi-octets est d'un seul bit (en fait 0,999…) au lieu de 4.
Ce seul premier constat fait déjà chuter drastiquement la complexité globale de la clé à 80 bits.

Pour les demi-octets pairs, avant le XOR, ils ne peuvent prendre comme valeur que des chiffres (ce qui exclut les 6 lettres du code hexadécimal) dont certains ont une fréquence d'apparition supérieure aux autres (ceux de 1 à 6). Ceci va sensiblement influer sur les résultats du XOR. Par exemple, la probabilité d'obtenir un 0 sera proche de 1/10 au lieu de 1/16.

Voici le calcul des différentes fréquences d’apparition entre une clé hexadécimale lue en binaire et lue en ASCII :

niddle impairs
niddle pairs

Nous voyons que pour les demi-octets pairs, la différence est moins flagrante et l’ampleur de la perte ne saute pas aux yeux. Nous utiliserons donc l’entropie de Shannon :

>>> dico2={'a': 0.03125, 'c': 0.03125, 'b': 0.03125, 'e': 0.0234375, 'd': 0.03125, 'f': 0.0234375, '1': 0.1015625, '0': 0.109375, '3': 0.09375, '2': 0.09375, '5': 0.09375, '4': 0.09375, '7': 0.1015625, '6': 0.09375, '9': 0.0234375, '8': 0.0234375}
>>> prob = 0.0
>>> for i in dico2:
...         prob += dico2[i]*math.log(dico2[i],2)
...
>>> prob*-1
3.75287733099689

Nous sommes donc à 3.75 bits au lieu de 4. Reporté sur les 16 demi-octets pairs, cela fait 60 bits au lieu de 64. Rappelons que les demi-octers impairs ont un seul bit d'entropie chacun.
Donc nous pouvons quantifier que les biais, produits par le fait d'utiliser un condensat sous forme de chaîne de caractères, induisent une complexité de la clé de 76 bits au lieu de 128.

Ceci ne concerne que le cas où un seul XOR est effectué. Si l'on soumet un condensat de 256 bits, il sera converti en une chaîne de 512 bits (lors de la conversion en ASCII) et l'opération XOR se fera sur quatre sous-chaines. Pour un condensat SHA2 de 512 bits (comme dans l'exemple de MySQL), il y aura un XOR sur 8 sous chaines.

Les calculs montrent que plus il y a de XOR successifs, plus on s'approche d'une entropie de 1 bits pour les demi-octets impairs et de 4 bits (le biais tend à s'annuler) pour les demi-octets pairs. Soit une entropie de clé maximale de 80 bits.

Le nombre de clés possible à tester pour un attaquant est donc de 280 au lieu de 2128. Cela signifie que 99.99999999999964 % des clés n'ont plus besoin d'être testées par un attaquant.

Fort en apparence

Un autre risque de cette pratique est d'utiliser une clé trop longue, d'apparence robuste, qui soit en fait totalement triviale une fois l'opération de XOR accomplie. Par exemple la clé 73873454724314018234819237962392956160 n'est pas géniale :

>>> hex(73873454724314018234819237962392956160)
'0x37938285d99a6d0037938285d99a6d00'

>>> 0x37938285d99a6d00^0x37938285d99a6d00
0

Dans le cas nominal, les chances que les octets d'une clé aléatoire de 256 bits s'annulent si l'on XOR ses deux moitiés sont très très réduites :

>>> pow(1/256.0, 16)
2.938735877055719e-39

Cette probabilité est en fait exactement identique à celle de tomber directement sur une clé aléatoire de 128 bits qui vaille 0 :

>>> pow(1/2.0, 128)
2.938735877055719e-39

Désormais, considérons que les octets ne peuvent plus prendre les 256 valeurs possibles, mais seulement 62, d'entre elles (majuscules, minuscules, chiffres) :

>>> pow(1/62.0, 16)
2.0976497180691982e-29

La probabilité qu'une telle clé, aléatoirement générée, s'annule totalement après le XOR est 7 milliards de fois plus importante que pour une clé binaire aléatoire.

Si l'on considère que les octets ne peuvent prendre que 16 valeurs différentes (les caractères du code hexadécimal), la probabilité d'annulation est maintenant de :

>>> pow(1/16.0, 16)
5.421010862427522e-20

Soit 18 milliards de milliards de fois plus importante. Bien entendu, cela demeure un événement rarissime (vous pouvez gagnez mille milliards de fois au loto avant que cela n'arrive). Mais il n'y a pas à prendre en considération seulement la probabilité que la clé s'annule totalement. Il suffit qu'une portion suffisamment importante soit concernée pour que les dommages soient palpables.

Rappeler UN HEX n'est pas forcément une mauvaise idée

Reprenons la documentation de MySQL :

For a key length of 128 bits, the most secure way to pass a key to the key_str argument is to create a truly random 128-bit value and pass it as a binary value. >For example: INSERT INTO t VALUES (1,AES_ENCRYPT('text',UNHEX('F3229A0B371ED2D9441B830D21A390C3')));
A passphrase can be used to generate an AES key by hashing the passphrase. For example: INSERT INTO t VALUES (1,AES_ENCRYPT('text', SHA2('My secret passphrase',512))); Do not pass a password or passphrase directly to crypt_str, hash it first. Previous versions of this documentation suggested the former approach, but it is no longer recommended as the examples shown here are more secure.

Pour résumer :

mysql> AES_ENCRYPT("test3", UNHEX('F3229A0B371ED2D9441B830D21A390C3'))

C'est 2128 possibilités, soit 781 milliards de fois l'âge de l'Univers pour essayer toutes les combinaisons.

mysql> AES_ENCRYPT("test3", SHA2('My secret passphrase',512))

C'est 280 possibilités, soit « seulement » 38 millions d'années.

Ce n'est pas exactement ce qu'on pourrait appeler « demain ». Cependant, je serais curieux de connaître les « examples » qui suggèrent à MySQL qu'amputer 99.99999999999964% des clés possibles et augmenter de 1844674407370955161600 % le risque qu'une clé s'annule est « more secure ».
Le premier exemple demeure astronomiquement plus sécurisé que le second.

On corrige les copies

Nous avons donc vu que la fonction AES_ENCRYPT est un guet-apens pour l'utilisateur non averti. De base, son implémentation couplée à sa documentation :

  • utilise un mode de chiffrement sensible à la cryptanalyse (ECB)
  • dilue l'entropie des clés
  • autorise (et favorise) des tailles inutilement longues qui menacent également l'entropie

Positionner le mode ECB par défaut est une très mauvaise idée.

Utiliser des chaînes de caractères en guise de clé est une aberration. Ce serait tolérable si la fonction se chargeait ensuite d'une conversion sécurisée (via une fonction sûre de dérivation de clé comme PBKDF2 qui soit interprétée en binaire.

Professer de recourir à cette solution dans sa documentation est quasiment NSA-friendly.

Toute blague mise à part, cette coquille dans la documentation a été reportée à MySQL :

[9 Mar 10:29] David Soria

Description:
AES_ENCRYPT function allows binary key as well as string key.

[...]

Suggested fix:
There is no way where using a string key could be an acceptable solution
if the key, finally, has to be in binary format (like AES key need to).

You should avoid string key submission or force AES_ENCRYPT to hash
every string submitted (and use the hash value as binary, not string).
But this will break compatibility with previous versions.

At least, change your document recommendation from:
INSERT INTO t VALUES (1,AES_ENCRYPT('text', SHA2('My secret
passphrase',512)));
to
INSERT INTO t VALUES (1,AES_ENCRYPT('text', UNHEX(SHA2('My secret
passphrase',512))));

[9 Mar 16:23] Sinisa Milivojevic

Documentation definitely requires changes as described.

Verified.

[10 Mar 3:36] Paul Dubois

Thank you for your bug report. This issue has been addressed in the
documentation. The updated documentation will appear on our website
shortly.

Et CodeIgniter sera rapidement averti que 128 divisé par 8 vaut 16.

La morale est que le développeur se doit de maîtriser un minimum de concepts cryptographiques avant de recourir à des fonctions aussi avancées qu'AES, qui n'ont aucune pitié pour les étrangers qui foulent leur terre.

Voici le code initial corrigé des vulnérabilités :

mysql> SET block_encryption_mode = 'aes-256-cbc';
mysql> SET @key_str = SHA2(@encryption_key 256);
mysql> SET @init_vector = RANDOM_BYTES(16);
mysql> AES_ENCRYPT(@crypt_str, UNHEX(@key_str), @init_vector);

Tout en n'oubliant pas de stocker init_vector car il sera nécessaire pour déchiffrer.

David SORIA
-
2022-01-05

Nos autres articles