Aller au contenu

Hachage universel

Un article de Wikipédia, l'encyclopédie libre.

En mathématiques et en informatique, le hachage universel, en anglais universal hashing, (dans un algorithme probabiliste ou un bloc de données) est une méthode qui consiste à sélectionner aléatoirement une fonction de hachage dans une famille de fonctions de hachages qui ont certaines propriétés mathématiques. Cela permet de minimiser la probabilité de collision de hachage. Plusieurs familles de fonctions de hachages sont connues (pour hacher des entiers, des chaînes de caractères ou des vecteurs), et leur calcul est souvent très efficace. Le hachage universel a de nombreux usages en informatique, par exemple dans l’implémentation des tables de hachage, les algorithmes probabilistes et le chiffrement de données.

Introduction

[modifier | modifier le code]

Supposons que nous désirions associer des clés provenant de l'univers à des fragments binaires (indexés ). L'algorithme devra gérer des ensembles de données à partir des clés, qui n'est pas connu à l'avance. Habituellement le but du hachage est de minimiser le nombre de collisions (clés de qui ont la même image par la fonction). Une fonction de hachage déterministe ne peut pas offrir la garantie contre la création intentionnelle de collision si la taille de est plus grande que la taille de par l'adversaire pour choisir de telle sorte qu'il soit l'antécédent d'un fragment. Cela donne un ensemble de clés qui donnent toutes le même hash, rendant le hachage virtuellement inutile. De plus une fonction de hachage déterministe ne se prête pas au rehachage qui peut être nécessaire si un trop grand nombre de collisions a été constaté.

La solution à ces problèmes est de choisir la fonction de hachage de manière aléatoire à partir d'une famille de fonctions de hachage. Une famille de fonctions est appelée famille universelle si .

En d'autres mots, deux clés prises au hasard dans l'univers ont une probabilité au plus de de donner une collision quand la fonction est tirée aléatoirement de . C'est la probabilité à laquelle on s'attendrait si la fonction de hachage attribuait un hash de manière aléatoire à chaque clé. Parfois la définition est élargie pour accepter une probabilité de collision en . Ce concept a été introduit par Larry Carter et Mark N. Wegman (en)[1] en 1977, et a trouvé de nombreuses applications en informatique[2]). Si on a une borne supérieure sur la probabilité de collision, on parle de fonctions -presque universelles.

Beaucoup mais pas toutes les fonctions de hachages universelles vérifient la propriété plus forte de différence uniforme :

, si est tiré aléatoirement parmi , la différence est uniformément distribuée dans .

Notez que la définition de l'universalité ne s’intéresse qu'aux cas de collisions () alors que la propriété de différence uniforme est plus forte.

De façon similaire, une famille universelle peut être XOR-universelle si , la valeur de est uniformément distribué dans avec l'opération ou exclusif bit à bit. Ceci n'est possible que dans le cas où est une puissance de deux.

Une autre condition plus forte que l'universalité est l'indépendance par paires : Cette propriété est obtenue si on a la probabilité que donne une paire de hashs soit la même que si cette paire de hash était calculée d'une manière parfaitement aléatoire : . L'indépendance par paire est parfois appelée universalité forte.

Une autre propriété est l'uniformité. On dit qu'une famille est uniforme si tous les hashs possibles sont équiprobables en d'autres termes pour toute valeur de hash . L'universalité n'implique pas l'uniformité mais l'universalité forte si.

À partir d'une famille vérifiant la propriété de distance uniforme on peut créer une famille de fonctions de hachage fortement universelle de fonction de hachage, c'est-à-dire une famille de fonction de hachage indépendantes deux à deux en ajoutant à chaque fonction de la famille mère une constante aléatoire uniformément distribuée à valeurs dans . Si est une puissance de 2, on peut créer cette famille indépendante par une opération XOR avec une constante aléatoire uniformément distribuée. Étant donné que le décalage d'une constante n'a pas forcément d'importance dans certaines applications (par exemple les tables de hachage), la distinction nette entre la propriété de distance uniforme et l'indépendance paire à paire n'est pas toujours effectuée[3].

Pour certaines applications comme les tables de hachage, il est important que les bits les moins significatifs du hash soient eux aussi universels. Quand une famille est fortement universelle, cette propriété est garantie : si est une famille fortement universelle avec alors la famille faite avec les fonctions pour est également fortement universelle pour . Cette propriété n'est pas vraie en général pour les familles universelles. Par exemple la famille faites avec les fonctions identité est clairement universelle mais la famille faite des fonctions ne l'est pas.

UMAC, Poly1305 (en)-AES et quelques autres algorithmes de codes d'authentification de message se basent sur le hachage universel[4],[5]. Dans ces applications, le logiciel sélectionne une nouvelle fonction de hachage pour chaque message, basé sur un identifiant unique généré pour le hachage du message.

Plusieurs implémentations de tables de hachage utilisent le hachage universel. Dans ces applications, typiquement, le logiciel ne prend une nouvelle fonction de hachage que s'il note que trop de clés ont produit une collision; sinon la même fonction de hachage reste utilisée. Certains schémas de résolution de collision, comme le hachage dynamique parfait, changent de fonction de hachage à la première collision observée. Les autres méthodes de résolution de collision comme la méthode du coucou et le 2-choice hashing, autorisent un certain nombre de collisions avant de changer de fonction de hachage.

Garanties mathématiques

[modifier | modifier le code]

Pour n'importe quel ensemble de clés, utiliser une famille universelle garantit les propriétés suivantes :

  1. Pour tout de , le nombre de clés pour le hash est . En implémentant des tables de hachage par un chaînage, ce nombre est proportionnel au temps de calcul pour une opération basée sur une clé (recherche, insertion ou suppression).
  2. Le nombre de paires de clé de qui produisent des collisions ( et ) est majoré par qui est de l'ordre de . Dans le cas du hachage de entrées, il y a une probabilité supérieure à 1/2 de n'avoir aucune collision.
  3. Le nombre de clés qui dans les fragments de au moins clés est majoré par [6]. Ainsi la capacité de chaque fragment est limitée à trois fois la taille moyenne (), le nombre total de clés dans les fragments qui dépassent la moyenne est au plus . Cette propriété ne tient que pour une famille de fonctions de hachage où la probabilité de collision est majorée par . Si une définition plus faible est utilisée et qu'on se contente d'une majoration en , alors ce résultat n'est plus valable[6].

Les garanties ci-dessus sont valables pour n'importe quel ensemble donné, elles tiennent même si le jeu de données à hacher a été sélectionné par un attaquant. Cependant, comme l'attaquant crée son jeu de données avant que l'algorithme n'ait fait sa sélection de fonction de hachage aléatoire, si l'attaquant peut deviner les fonctions qui seront sélectionnées par l'algorithme, la sélection aléatoire n'apporte rien par rapport au hachage déterministe.

La deuxième et la troisième garantie sont typiquement utilisées en conjonction avec un rehachage. Par exemple, un algorithme probabiliste peut être sélectionné pour un taux de collision attendu de . Si trop de collisions sont observées, un autre algorithme de la famille est sélectionné de manière aléatoire et le traitement est répété. L'universalité garantit que le nombre de répétition est une variable aléatoire géométrique.

Constructions

[modifier | modifier le code]

Étant donné que les informations sont représentées en machine par des mots, les hachages peuvent être réduit à trois situations : un mot (un entier, integer en anglais) ; un vecteur de mots de taille fixée ; et un vecteur de taille variable assimilé à une chaîne de mots informatiques/caractères (string en anglais).

Hachage d'entiers

[modifier | modifier le code]

Cette section se réfère au cas du hachage d'entiers qui tiennent dans un mot machine. Dans ce cas les opérations addition, multiplication, et division sont exécutées directement sur le processeur et coûtent très peu de ressources. À partir de ce point on considère l'univers à hacher .

La proposition initiale de Carter et Wegman[1] était de choisir un entier premier et définir

est un couple d'entiers choisis aléatoirement modulo avec . (Cela correspond à un pas d'itération d'un générateur congruentiel linéaire.)

Pour vérifier que est bien une famille universelle, notez que ne tient que si

pour un certain entier compris entre et . Si leur différence, , est non nulle et possède un inverse modulo . Chercher revient à résoudre :

.

Il y a choix possibles pour ( est exclu) et pour pris dans l'intervalle autorisé, il y a valeurs pour le côté droit. La probabilité de collision est alors de

.

Une autre manière de prouver que est une famille universelle passe par la notion de distance statistique. On écrit la différence comme :

.

Étant donné que est non nul et que est uniformément distribué dans , on déduit que modulo est aussi uniformément distribué dans . La distribution de est ainsi presque uniforme, avec une différence de probabilité au maximum de entre les échantillons. Il en résulte que la distance statistique à une famille uniforme est , qui devient négligeable quand .

La famille la plus simple de fonctions de hachage, , est seulement approximativement universelle : pour tout [1]. De plus, cette analyse est assez fine ; Carter et Wegman[1] ont montré que pour tout .

Éviter le calcul modulaire

[modifier | modifier le code]

La méthode la plus populaire dans le hachage d'entier est la méthode « multiplie et décale » décrit par Dietzfelbinger et al. en 1997[7]. En évitant l'arithmétique modulaire, cette méthode est plus simple à implémenter et est significativement plus rapide en pratique (usuellement plus rapide d'un facteur 4)[8]).

Cette méthode suppose que le nombre de fragments binaires soit une puissance de deux, . Soit le nombre de bits dans un mot machine. Les fonctions de hachage sont paramétrées par des entiers positifs impairs (qui tiennent dans un mot de bits). Pour calculer , on multiplie par modulo et ensuite on garde les bits de poids fort du résultat comme hash. En notation mathématique cela s'écrit :

et peut être implémenté dans un langage de la famille du C par le code :

(unsigned) (a*x) >> (w-M)

Ce schéma ne satisfait pas la propriété de différence uniforme et est seulement -presque-universel ; pour tout , .

Pour comprendre le comportement de la fonction de hachage, remarquez que si et ont les mêmes 'M' bits de poids fort alors n'a que des 1 ou que des 0 dans ses 'M' bits de poids fort (cela dépend du maximum entre et ). Supposons maintenant que les bits de poids faible de apparaissent en position . Comme est un entier impair et que les entiers impairs possèdent un inverse dans l'anneau , on déduit que sera uniformément distribué parmi les entiers de bits avec le bit de poids le plus faible situé à la position . La probabilité que ces bits soient tous égaux à 0 ou à 1 est donc au plus de . D'autre part si , alors les M bits de poids fort de contiennent des 0 et des 1 et on est certain que . Finalement, si alors le bit de vaut 1 et si et seulement si les bits valent également 1, ce qui arrive avec la probabilité de .

Cette analyse est difficile, comme on peut le constater avec les exemples et . Pour obtenir une fonction de hachage 'vraiment' universelle, on peut utiliser la méthode multiplie et additionne.

qui peut être implémenté en langages de la famille du C par

(unsigned) (a*x+b) >> (w-M)

est un entier pair positif tiré aléatoirement, , et un entier positif tiré aléatoirement . Avec ce choix de et , pour tout [9]. Ceci diffère légèrement, mais sur un point important, de l'erreur de traduction de l'article en anglais[10].

Hachage de vecteurs

[modifier | modifier le code]

Cette section concerne le hachage de vecteur de mots dont la longueur est fixée à l'avance. Interpréter l'entrée comme un vecteur de mots machine (entiers de bits chacun). Si est une famille universelle qui vérifie la propriété de différence uniforme, la famille suivante, trouvée par Carter et Wegman[1], possède également la propriété de différence uniforme (et donc est universelle) :

, où chaque est choisi de manière aléatoire et indépendante.

Si est une puissance de deux, la somme peut être remplacée par une opération ou exclusif[11].

En pratique si l'arithmétique en double précision est disponible, on utilise la famille de fonction de hachage « multiplie et décale »[12]. On initialise la fonction avec un vecteur d'entiers pairs de bits de long. Ensuite si le nombre de fragments est , avec

.

Il est possible de diviser le nombre de multiplication par 2, ce qui se traduit par un calcul à peu près deux fois plus rapide en pratique[11]. Initialiser la fonction de hachage avec un vecteur de nombres entiers impairs chacun de bits de long. La famille de hachage ainsi produite est universelle[13] :

.

Si les opérations en double précision ne sont pas disponibles, on peut interpréter l'entrée comme un vecteur de demi-mots (des entiers de bits de longs). L'algorithme va alors utiliser multiplications, avec k le nombre de demi mots du vecteur. Ainsi l'algorithme tourne à un "taux" d'une multiplicatio par mot d'entrée.

Ce schéma peut également être utilisé pour hacher des entiers en considérant que leurs bits sont un vecteur d'octets. Dans cette variante, la technique est appelée hachage tabulaire et fournit une alternative pratique aux fonctions de hachage universelle basées sur des multiplications[14].

L'universalité forte à grande vitesse est également possible[15]. Initialiser la fonction de hachage avec un vecteur d'entiers pris au hasard de bits de long. Calculer :

.

Le résultat est fortement universel sur bits. Expérimentalement cette méthode consomme 0.2 cycle CPU par octet sur les dernières puces Intel pour .

Hachage de chaînes

[modifier | modifier le code]

Cette section vise le cas de vecteur de mots de taille variable. Si la taille de la chaîne peut être majorée par un petit nombre, il est préférable d'utiliser la méthode avec des vecteurs de taille définie en complétant la chaîne à hacher par des zéros au besoin. L'espace mémoire requis pour le calcul est la longueur maximale de la chaîne, mais le temps de calcul dépend uniquement de la longueur de . Tant qu'on interdit les zéros dans la chaîne, on peut ignorer le remplissage d'une chaîne par des zéros terminaux sans que la propriété d'universalité en soit affectée[11]. Noter que si on autorise la présence de zéros dans la chaîne alors le problème d'ajout de zéros terminaux se règle en choisissant d'ajouter un caractère non nul, qui balisera la fin de la chaîne à toutes les chaînes en entrée avant le traitement[15].

Maintenant supposons qu'il faille hacher , sans qu'on puisse avoir d'estimation correcte de . Une famille de hachage universelle[12] traite la chaîne comme les coefficients d'un polynôme modulo un grand nombre premier. Si , soit un nombre premier, on définit :

, où est uniformément aléatoire et est tiré aléatoirement d'une famille aléatoire universelle qui indexe l'ensemble des entiers .

En utilisant les propriétés de l'arithmétique modulaire, la formule ci-dessus peut être calculée sans produire de grands nombres par le code suivant[16] :

uint hash(String x, int a, int p)
	uint h = INITIAL_VALUE
	for (uint i=0 ; i < x.length ; ++i)
		h = ((h*a) + x[i]) mod p
	return h

L'algorithme ci-dessus est également connu en tant que fonction de hachage multiplicative[17]. En pratique l'opérateur modulo et le paramètre p peuvent être évités tous les deux simplement en autorisant les entiers à dépasser leur taille maximale (overflow) car cette opération est équivalente à un modulo MAX_INT_VALUE + 1 dans beaucoup de langages de programmation. Ci-dessous, un tableau de valeurs choisies pour initialiser h dans quelques implémentations populaires.

Implementation INITIAL_VALUE a
Bernstein's hash function djb2[18] 5381 33
STLPort 4.6.2 0 5
La fonction de hachage de Kernighan et Ritchie[19] 0 31
java.lang.String.hashCode()[20] 0 31

Soient deux chaînes et la taille de la plus longue des deux ; pour les besoins de l'analyse on supposera que la seconde chaîne est complétée par des zéros jusqu'à atteindre la longueur . Une collision avant d'appliquer implique que est une racine du polynôme dont les coefficients sont . Ce polynôme a au plus racines modulo , donc la probabilité de collision est au plus de . La probabilité de collision à travers l'aléa descend la probabilité totale de collision à . Ainsi si le nombre premier est suffisamment grand comparé à la longueur des chaînes à hacher, la famille est très proche d'une famille universelle (en termes de distance statistique).

Éviter l'arithmétique modulaire

[modifier | modifier le code]

Pour limiter la consommation de ressources par les calculs d'arithmétique modulaire, deux astuces sont utilisées en pratique[11] :

  1. On peut choisir le nombre premier proche d'une puissance de 2, un nombre premier de Mersenne par exemple. Cela permet d'implémenter les opérations de modulo sans utiliser de division (en utilisant des opérations plus rapides comme les additions et les décalages de bits). Par exemple sur des architectures modernes on peut travailler avec , alors que les sont stockés sur 32 bits.
  2. On peut appliquer un hachage vectoriel sur des blocs de données de l'entrée. Par exemple, on hache l'entrée par vecteurs de 16 mots et on applique le hachage de chaîne aux hashs obtenus. Le hachage de chaîne, très lent, étant appliqué à un ensemble de données significativement plus petit le calcul devient presque aussi rapide que le hachage vectoriel.
  3. On peut choisir une puissance de deux comme diviseur ce qui permet de réduire l'opération modulo à un masquage de bits plus rapide que les divisions. La famille de fonctions NH utilise cette méthode.

Notes et références

[modifier | modifier le code]
(en) Cet article est partiellement ou en totalité issu de l’article de Wikipédia en anglais intitulé « Universal_hashing » (voir la liste des auteurs).
  1. a b c d et e (en) L. Carter et M. N. Wegman, « Universal Classes of Hash Functions », Journal of Computer and System Sciences, vol. 18, no 2,‎ , p. 143-154 (DOI 10.1016/0022-0000(79)90044-8).
  2. Voir par exemple (en) Peter Bro Miltersen, « Universal Hashing » [archive] [PDF], .
  3. (en) Rajeev Motwani et Prabhakar Raghavan, Randomized Algorithms, Cambridge University Press, (ISBN 0-521-47465-5), p. 221.
  4. (en) David Wagner (ed.), Advances in Cryptology - CRYPTO 2008 (lire en ligne), p. 145.
  5. (en) Jean-Philippe Aumasson, Willi Meier, Raphael Phan et Luca Henzen, The Hash Function BLAKE, (lire en ligne), p. 10.
  6. a et b (en) Ilya Baran, Erik D. Demaine et Mihai Pătraşcu, « Subquadratic Algorithms for 3SUM », Algorithmica, vol. 50, no 4,‎ , p. 584-596 (DOI 10.1007/s00453-007-9036-3, lire en ligne).
  7. (en) Martin Dietzfelbinger, Torben Hagerup, Jyrki Katajainen et Martti Penttonen, « A Reliable Randomized Algorithm for the Closest-Pair Problem », Journal of Algorithms, vol. 25, no 1,‎ , p. 19-51 (DOI 10.1006/jagm.1997.0873, lire en ligne [Postscript], consulté le ).
  8. (en) Mikkel Thorup (en), « Text-book algorithms at SODA ».
  9. (de) Philipp Woelfel, Über die Komplexität der Multiplikation in eingeschränkten Branchingprogrammodellen, Universität Dortmund, , PDF (lire en ligne).
  10. (en) Philipp Woelfel, « Efficient Strongly Universal and Optimally Universal Hashing », dans Mathematical Foundations of Computer Science 1999, coll. « LNCS » (no 1672), , PDF (DOI 10.1007/3-540-48340-3_24, lire en ligne), p. 262-272.
  11. a b c et d (en) Mikkel Thorup, « String hashing for linear probing », dans Proc. 20th ACM-SIAM Symposium on Discrete Algorithms (SODA), (DOI 10.1137/1.9781611973068.72, lire en ligne), p. 655-664, section 5.3.
  12. a et b (en) Martin Dietzfelbinger, Joseph Gil, Yossi Matias et Nicholas Pippenger, « Polynomial Hash Functions Are Reliable (Extended Abstract) », dans Proc. 19th International Colloquium on Automata, Languages and Programming (ICALP), , p. 235-246.
  13. (en) J. Black, S. Halevi, H. Krawczyk et T. Krovetz, « UMAC: Fast and Secure Message Authentication », dans Advances in Cryptology (CRYPTO '99), (lire en ligne), Equation 1.
  14. (en) Mihai Pătrașcu et Mikkel Thorup, « The power of simple tabulation hashing », dans Proceedings of the 43rd annual ACM Symposium on Theory of Computing (STOC '11), (DOI 10.1145/1993636.1993638, arXiv 1011.5200), p. 1-10.
  15. a et b (en) Owen Kaser et Daniel Lemire, « Strongly universal string hashing is fast », Computer Journal,‎ (DOI 10.1093/comjnl/bxt070, arXiv 1202.4961, lire en ligne).
  16. (en) « Hebrew University Course Slides ».
  17. (en) Peter Kankowsk, « Hash functions: An empirical comparison ».
  18. (en) Ozan Yigit, « String hash functions ».
  19. (en) B. Kernighan et D. Ritchie, The C Programming Language, , 2e éd., 118 p. (ISBN 0-13-110362-8), chap. 6.
  20. (en) « String (Java Platform SE 6) », sur docs.oracle.com.

Articles connexes

[modifier | modifier le code]

Bibliographie

[modifier | modifier le code]

(en) Donald E. Knuth, The Art of Computer Programming, vol. 3 : Sorting and Searching, , 2e éd. [détail de l’édition]

Liens externes

[modifier | modifier le code]

(en) Open Data Structures - Section 5.1.1 - Multiplicative Hashing