Bonsoir à tous, ou plutôt bon matin, vu l'heure. Bref.
Mon grand chéri du moment, parmi les nombreux langages de programmation que compte le monde, c'est le haskell. Et je m'efforce de d'améliorer ma maîtrise de la bête. Pour ce faire, je me suis attaché à coder dans ce beau langage la fonction de hachage SHA-1. Entendons-nous bien : il existe sur hackage un certain nombre d'implémentations de cette fonction dans divers modules, mais sauf erreur de ma part, il s'agit systématiquement de bindings vers l'implémentation en C. C'est moche, quoi. Moi j'ai fait du code pur, au sens de haskell, bien sûr.
Bref, voilà le résultat : si vous avez quelque connaissance en haskell, je suis preneur de toute remarque. Comme à mon habitude, le code est lourdement commenté. Cela étant, je ne décris pas l'algorithme en lui-même, je laisse cette tâche à la spécification officielle, qui est fort bien expliquée sur Wikipédia.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | import Data.Char import Data.Bits -- La fonction sha1 principale -- =========================== -- -- Se contente de faire appel à sha1num et de lui donner sa représentation héxadécimale sha1 :: [Char] -> [Char] sha1 m = hd $ sha1num m where hd (a, b, c, d, e) = (hexadecimare 8 a) ++ (hexadecimare 8 b) ++ (hexadecimare 8 c) ++ (hexadecimare 8 d) ++ (hexadecimare 8 e) -- La fonction sha1 numérique -- ========================== -- -- Les étapes suivantes sont appliquées dans l'ordre : -- 1. `farcire` rembourre le message qui a alors une longueur multiple de 512 bits. -- 2. `separareN` découpe le message rembourré en tronçons de 512 bits et en fait une liste. -- 3. `separareM` découpe un tronçon en "mots" de 32 bits et en fait une liste. -- 4. `genereW` transforme une liste de 16 mots en une liste de 80 W selon l'algorithme défini dans la spécification. -- 5. À l'issue du `map`, on a donc une liste contenant les listes de 80 W associées à chaque tronçon de 512 bits. -- 6. `statuereH` pose le quintuple contenant les 5 H de départ. -- 7. `sha1nucleus` applique la série de transformations et rotations qui consituent le cœur de SHA-1 aux 80 W d'un -- même tronçon, à partir d'un quintuple de base. -- 8. Le pli gère l'addition modulo (2^32) du résultat de `sha1nucleus` au quintuple précédent et s'assure que le -- résultat d'un tronçon serve de base au tronçon suivant. sha1num :: [Char] -> (Integer, Integer, Integer, Integer, Integer) sha1num m = foldl (\x y -> addere32 x $ sha1nucleus x y) statuereH (map (genereW . separareM) (separareN $ farcire m)) where -- Fonction de bourrage -- `numerisare` transforme une chaîne de caractères en un grand nombre par le biais du code ASCII de chaque -- caractère, multiplié à la puissance adéquate de (2^8)=256. -- `farsura` détermine combien de 0 ajouter au résultat de `numerisare`*2+1 pour atteindre une longueur -- multiple de 512 bits. -- Il ne reste plus alors qu'à faire une simple addition de la longueur en bits pour qu'elle se retrouve, -- en gros-boutiste, à la toute fin du nombre. -- Les nombreux appels à `toInteger` sont nécessaires car GHC préférerait travailler en Int et tronque les résultats. farcire m = ((numerisare m)*2+1) * (2^(farsura $ 8*length m)) + toInteger (8*length m) where farsura l | l `mod` 512 < 448 = 511 - (l `mod` 512) | otherwise = 1023 - (l `mod` 512) numerisare m | m == [] = 0 | otherwise = foldl1 (\x y -> 256*x+y) (map (toInteger . ord) m) -- Sans difficulté, utilise la fonction de division euclidienne et de modulo pour individualiser les tronçons -- de 512 bits et en faire un tableau. separareN x | x == 0 = [] | otherwise = (separareN $ x `div` (2^512)) ++ [x `mod` (2^512)] -- Le fonctionnement général est le même mais en s'assurant que l'on obtienne 16 "mots" par tronçon, d'où -- l'utilisation de `replicate`. separareM x = (replicate (16 - (length $ separareMbis x)) 0) ++ (separareMbis x) where separareMbis x | x == 0 = [] | otherwise = (separareMbis $ x `div` (2^32)) ++ [x `mod` (2^32)] -- Fonction de génération des W à partir des 16 "mots" de départ -- L'utilisation de `iterate` qui génère une liste de chacune des étapes de la création progressive -- de la liste des W est, étonnamment, considérablement plus rapide qu'une fonction récursive. La -- 64e étape est celle à laquelle nous désirons arriver. -- La fonction `sha1nucleus` a besoin de savoir où elle en est dans le pli pour choisir la fonction -- non-linéaire f_t et la constante K_t, c'est pourquoi on renvoie une liste de paires (W_t, t). genereW xs = zip ((iterate (iteranda) xs) !! 64) [0..79] where iteranda xs = xs ++ [rotl 1 ((w3 xs) `xor` (w8 xs) `xor` (w14 xs) `xor` (w16 xs))] where w3 xs = (xs !! (length xs - 3)) w8 xs = (xs !! (length xs - 8)) w14 xs = (xs !! (length xs - 14)) w16 xs = (xs !! (length xs - 16)) -- Un simple quintuple contenant les valeurs de départ définies par la spécification. statuereH = (0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0) -- Le cœur de SHA-1 -- -- La fonction consiste à "fondre" tous les W dans le quintuple de départ, au moyen d'une même série -- d'opérations qui mélangent le contenu dans tous les sens. Ces opérations étant à peu près les mêmes -- à chaque étape, un pli est particulièrement efficace. -- `miscere` est simplement un moyen de raccourcir la définition de `functio` dont la première partie -- est particulièrement complexe. -- `variabilis` correspond à f_t, la fonction qui varie en fonction du tour où l'on est. -- `constans` au contraire est la série de constantes K_t. -- Dans les deux cas, il n'y a que 4 valeurs possibles, les t étant regroupés en 4 groupes : le (t `div` 20) -- sert à raccourcir l'écriture. -- `optio`, `paritas` et `majoritas` sont des fonctions utilisées par d'autres algorithmes de hachage, que -- l'on pourra au besoin extraire et "globaliser". -- Dans `optio`, la partie après le OU logique est normalement ((NON x) ET Z) mais la fonction `complement` -- de Data.Bits donne des résultats surprenants avec les Integer et fausse totalement le calcul. Le -- substitut utilisé ici a exactement la même table de vérité. sha1nucleus initium xs = foldl (functio) initium xs where functio (a, b, c, d, e) (w, t) = (miscere a b c d e w t, a, (rotl 30 b), c, d) where miscere a b c d e w t = ((rotl 5 a) + (variabilis (t `div` 20) b c d) + e + (constans !! (t `div` 20)) + w) `mod` (2^32) variabilis t | t == 0 = optio | t == 1 || t == 3 = paritas | t == 2 = majoritas optio x y z = (x .&. y) .|. ((x `xor` z) .&. z) paritas x y z = x `xor` y `xor` z majoritas x y z = (x .&. y) .|. (x .&. z) .|. (y .&. z) constans = [0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6] -- La fonction qui additionne terme à terme et modulo 2^32 deux quintuples. addere32 (a, b, c, d, e) (f, g, h, i, j) = ((a+f) ` mod` (2^32), (b+g) ` mod` (2^32), (c+h) ` mod` (2^32), (d+i) ` mod` (2^32), (e+j) ` mod` (2^32)) -- Il n'est pas garanti que les fonctions de décalage et de rotation de -- Data.Bits fonctionnent selon nos besoins, en raison de la taille -- variable des Integer. On a préféré les réécrire. rotl s n = ((shl s n) .|. (shr (32-s) n)) `mod` (2^32) rotr s n = ((shl (32-s) n) .|. (shr s n)) `mod` (2^32) shl s n = n * (2^s) shr s n = n `div` (2^s) -- Cette fonction transforme un entier `decim` en sa représentation héxadécimale -- sur `long` caractères. Le "truc" pour obtenir une longueur fixe est le même que -- que celui utilisé dans la fonction `separareM` ci-dessus. hexadecimare :: Int -> Integer -> [Char] hexadecimare long decim = (replicate (long - (length $ hex decim)) '0') ++ (hex decim) where hex d | d == 0 = [] | otherwise = (hex $ d `div` 16) ++ [hexs !! (fromIntegral $ d `mod` 16)] hexs = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'] |
Si le titre est assez général, c'est que j'envisage de passer ensuite à d'autres fonctions de hachage, en particulier SHA-2 et MD5 dont le fonctionnement est très proche de celui-ci, au point que l'on peut peut-être factoriser du code de l'une à l'autre. À voir.
PS : désolé, le code est assez large, il faut faire défiler pour voir la fin. :-/