L'opérateur modulo (%) donne un résultat différent pour différentes versions .NET en C #

89

Je crypte l'entrée de l'utilisateur pour générer une chaîne de mot de passe. Mais une ligne de code donne des résultats différents dans différentes versions du framework. Code partiel avec valeur de touche enfoncée par l'utilisateur:

Touche enfoncée: 1. La variable asciiest 49. Valeur de «e» et «n» après quelques calculs:

e = 103, 
n = 143,

Math.Pow(ascii, e) % n

Résultat du code ci-dessus:

  • Dans .NET 3.5 (C #)

    Math.Pow(ascii, e) % n

    donne 9.0.

  • Dans .NET 4 (C #)

    Math.Pow(ascii, e) % n

    donne 77.0.

Math.Pow() donne le (même) résultat correct dans les deux versions.

Quelle est la cause et y a-t-il une solution?

Rajiv
la source
12
Bien sûr, les deux réponses à la question sont fausses. Le fait que vous ne semblez pas vous soucier de cela est, eh bien, inquiétant.
David Heffernan
34
Vous devez revenir plusieurs étapes en arrière. "Je crypte l'entrée de l'utilisateur pour générer une chaîne de mot de passe" cette partie est déjà douteuse. Que voulez-vous vraiment faire? Voulez-vous stocker un mot de passe sous forme chiffrée ou hachée? Voulez-vous l'utiliser comme entropie pour générer une valeur aléatoire? Quels sont vos objectifs de sécurité?
CodesInChaos
49
Bien que cette question illustre un problème intéressant avec l'arithmétique en virgule flottante, si l'objectif de l'OP est de "crypter l'entrée de l'utilisateur pour générer une chaîne pour le mot de passe", je ne pense pas que rouler votre propre cryptage soit une bonne idée, donc je ne recommanderais pas mise en œuvre de l’une des réponses.
Harrison Paine
18
Belle démonstration pourquoi d'autres langages interdisent l'utilisation de %avec des nombres à virgule flottante.
Ben Voigt
5
Bien que les réponses soient bonnes, aucune d'entre elles ne répond à la question de savoir ce qui a changé entre .NET 3.5 et 4 qui est à l'origine du comportement différent.
msell

Réponses:

160

Math.Powfonctionne sur les nombres à virgule flottante double précision; ainsi, vous ne devez pas vous attendre à ce que les 15 à 17 premiers chiffres du résultat soient précis:

Tous les nombres à virgule flottante ont également un nombre limité de chiffres significatifs, qui détermine également avec quelle précision une valeur à virgule flottante se rapproche d'un nombre réel. Une Doublevaleur a jusqu'à 15 chiffres décimaux de précision, bien qu'un maximum de 17 chiffres soit conservé en interne.

Cependant, l'arithmétique modulo nécessite que tous les chiffres soient précis. Dans votre cas, vous calculez 49103 , dont le résultat se compose de 175 chiffres, ce qui rend l'opération modulo sans signification dans vos deux réponses.

Pour déterminer la valeur correcte, vous devez utiliser l'arithmétique de précision arbitraire, telle que fournie par la BigIntegerclasse (introduite dans .NET 4.0).

int val = (int)(BigInteger.Pow(49, 103) % 143);   // gives 114

Edit : Comme indiqué par Mark Peters dans les commentaires ci-dessous, vous devez utiliser la BigInteger.ModPowméthode, qui est destinée spécifiquement à ce type d'opération:

int val = (int)BigInteger.ModPow(49, 103, 143);   // gives 114
Douglas
la source
20
+1 pour avoir souligné le vrai problème, à savoir que le code de la question est tout simplement faux
David Heffernan
36
Il convient de noter que BigInteger fournit une méthode ModPow () qui effectue (dans mon test rapide à l'instant) environ 5 fois plus rapidement pour cette opération.
Mark Peters
8
+1 Avec la modification. ModPow n'est pas seulement rapide, il est numériquement stable!
Ray
2
@maker Non, la réponse n'a pas de sens , n'est pas invalide .
Cody Gray
3
@ makerofthings7: Je suis d'accord avec vous en principe. Cependant, l'imprécision est inhérente à l'arithmétique à virgule flottante, et il est jugé plus pratique de s'attendre à ce que les développeurs soient conscients des risques que d'imposer des restrictions sur les opérations en général. Si l'on voulait être vraiment "sûr", alors le langage devrait également interdire les comparaisons d'égalité en virgule flottante, pour éviter des résultats inattendus tels que l' 1.0 - 0.9 - 0.1 == 0.0évaluation de false.
Douglas
72

Outre le fait que votre fonction de hachage n'est pas très bonne * , le plus gros problème avec votre code n'est pas qu'elle renvoie un nombre différent en fonction de la version de .NET, mais que dans les deux cas elle renvoie un nombre totalement dénué de sens: la bonne réponse au problème est

49 103 Mod = 143 est 114. ( lien vers Wolfram Alpha )

Vous pouvez utiliser ce code pour calculer cette réponse:

private static int PowMod(int a, int b, int mod) {
    if (b == 0) {
        return 1;
    }
    var tmp = PowMod(a, b/2, mod);
    tmp *= tmp;
    if (b%2 != 0) {
        tmp *= a;
    }
    return tmp%mod;
}

La raison pour laquelle votre calcul produit un résultat différent est que pour produire une réponse, vous utilisez une valeur intermédiaire qui supprime la plupart des chiffres significatifs du nombre 49103 : seuls les 16 premiers de ses 175 chiffres sont corrects!

1230824813134842807283798520430636310264067713738977819859474030746648511411697029659004340261471771152928833391663821316264359104254030819694748088798262075483562075061997649

Les 159 chiffres restants sont tous faux. L'opération de mod, cependant, recherche un résultat qui nécessite que chaque chiffre soit correct, y compris les tout derniers. Par conséquent, même la moindre amélioration de la précision Math.Powpeut avoir été implémentée dans .NET 4 entraînerait une différence drastique de votre calcul, qui produit essentiellement un résultat arbitraire.

* Puisque cette question parle d'élever des nombres entiers à des puissances élevées dans le contexte du hachage de mot de passe, il peut être une très bonne idée de lire ce lien de réponse avant de décider si votre approche actuelle doit être modifiée pour une meilleure.

dasblinkenlight
la source
20
Bonne réponse. Le vrai point est que c'est une fonction de hachage terrible. OP doit repenser la solution et utiliser un algorithme plus approprié.
david.pfx
1
Isaac Newton: Est-il possible que la lune soit attirée par la terre de la même manière que la pomme est attirée par la terre? @ david.pfx: Le vrai point est que c'est une terrible façon de cueillir des pommes. Newton doit repenser la solution et peut-être embaucher un homme avec une échelle.
jwg
2
Le commentaire de @jwg David a obtenu autant de votes positifs pour une raison. La question initiale indiquait clairement que l'algorithme était utilisé pour hacher les mots de passe, et c'est en effet un algorithme terrible à cette fin - il est extrêmement susceptible de rompre entre les versions du framework .NET, comme cela a déjà été démontré. Toute réponse qui ne mentionne pas que l'OP a besoin de remplacer son algorithme plutôt que de «réparer» cela lui rend un mauvais service.
Chris
@Chris Merci pour le commentaire, j'ai modifié pour inclure la suggestion de David. Je ne l'ai pas dit aussi fortement que vous, car le système d'OP peut être un jouet ou un morceau de code jetable qu'il construit pour son propre amusement. Merci!
dasblinkenlight
27

Ce que vous voyez est une erreur d'arrondi en double. Math.Powfonctionne avec double et la différence est comme ci-dessous:

.NET 2.0 et 3.5 => var powerResult = Math.Pow(ascii, e);renvoie:

1.2308248131348429E+174

.NET 4.0 et 4.5 => var powerResult = Math.Pow(ascii, e);renvoie:

1.2308248131348427E+174

Notez le dernier chiffre avant Eet cela cause la différence dans le résultat. Ce n'est pas l'opérateur de module (%) .

Habib
la source
3
sainte vache est-ce la SEULE réponse à la question des OP? J'ai lu toute la méta "blah blah security fausse question, je sais plus que toi n00b" et je me suis toujours demandé "pourquoi l'écart constant entre 3,5 et 4,0? est-ce? "Seulement pour être dit" Votre vrai problème n'est pas de regarder vos pieds "ou" À quoi vous attendez-vous lorsque vous portez des sandales faites maison le soir? !!! "MERCI!
Michael Paulukonis
1
@MichaelPaulukonis: C'est une fausse analogie. L'étude des roches est une poursuite légitime; l'exécution d'arithmétique à précision arbitraire à l'aide de types de données à précision fixe est tout simplement erronée. Je comparerais cela à un recruteur de logiciels demandant pourquoi les chiens sont pires que les chats pour écrire C #. Si vous êtes zoologiste, la question pourrait avoir du mérite; pour tout le monde, c'est inutile.
Douglas
24

La précision en virgule flottante peut varier d'une machine à l'autre, et même sur la même machine .

Cependant, le .NET crée une machine virtuelle pour vos applications ... mais il y a des changements de version en version.

Par conséquent, vous ne devriez pas vous y fier pour produire des résultats cohérents. Pour le chiffrement, utilisez les classes fournies par le Framework plutôt que de déployer les vôtres.

Joe
la source
10

Il y a beaucoup de réponses sur la façon dont le code est mauvais. Cependant, pourquoi le résultat est différent…

Les FPU d'Intel utilisent le format 80 bits en interne pour obtenir plus de précision pour les résultats intermédiaires. Donc, si une valeur est dans le registre du processeur, elle obtient 80 bits, mais lorsqu'elle est écrite dans la pile, elle est stockée à 64 bits .

Je m'attends à ce que la nouvelle version de .NET ait un meilleur optimiseur dans sa compilation Just in Time (JIT), donc elle garde une valeur dans un registre plutôt que de l'écrire dans la pile puis de la relire à partir de la pile.

Il se peut que le JIT puisse maintenant renvoyer une valeur dans un registre plutôt que sur la pile. Ou transmettez la valeur à la fonction MOD dans un registre.

Voir aussi Question sur le débordement de pile Quels sont les applications / avantages d'un type de données de précision étendue 80 bits?

D'autres processeurs, par exemple l'ARM, donneront des résultats différents pour ce code.

Ian Ringrose
la source
6

Il est peut-être préférable de le calculer vous-même en utilisant uniquement l'arithmétique des nombres entiers. Quelque chose comme:

int n = 143;
int e = 103;
int result = 1;
int ascii = (int) 'a';

for (i = 0; i < e; ++i) 
    result = result * ascii % n;

Vous pouvez comparer les performances avec les performances de la solution BigInteger publiées dans les autres réponses.

Ronald
la source
7
Cela nécessiterait 103 multiplications et réductions de module. On peut faire mieux en calculant e2 = e * e% n, e4 = e2 * e2% n, e8 = e4 * e4% n, etc. puis result = e * e2% n * e4% n * e32% n * e64% n. Un total de 11 multiplications et réductions de module. Compte tenu de la taille des nombres impliqués, on pourrait éliminer quelques réductions supplémentaires de module, mais ce serait mineur par rapport à la réduction de 103 opérations à 11.
supercat
2
@supercat De belles mathématiques, mais en pratique, elles ne sont pertinentes que si vous utilisez cela sur un grille-pain.
alextgordon
7
@alextgordon: Ou si l'on envisage d'utiliser des valeurs d'exposant plus grandes. L'extension de la valeur de l'exposant à, par exemple, 65521 nécessiterait environ 28 multiplications et réductions de module si l'on utilise la réduction de résistance, contre 65 520 si ce n'est pas le cas.
supercat du
+1 pour donner une solution accessible où il est clair exactement comment le calcul est effectué.
jwg
2
@Supercat: vous avez absolument raison. Il est facile d'améliorer l'algorithme, ce qui est pertinent s'il est calculé très souvent ou si les exposants sont grands. Mais le message principal est qu'il peut et doit être calculé en utilisant l'arithmétique entière.
Ronald