web-dev-qa-db-fra.com

Génération d'un PIN à partir d'octets cryptographiques

Une de nos applications utilise SQL Server pour le back-end, et cette application nécessite un chiffre à 4 chiffres PIN pour les employés à créer qui fonctionne avec une clé (plus ou moins). Le système lui-même par défaut tous les codes PIN à 0000, car il s'agit de la valeur magique pour "ne nécessite pas de code PIN" si l'appareil que vous utilisez n'a pas de clavier.

Nous avons rencontré un problème avec les employés qui ne mettaient pas à jour leur PIN (et nous en avons eu assez d'attendre que le fournisseur corrige le problème). Nous générons donc nos propres codes PIN avec SQL Server à l'aide d'un travail planifié pour mettre à jour les codes PIN de 0000.

Les codes PIN ne ont pas besoin pour être sécurisés (à quel point un code PIN à 4 chiffres est-il sécurisé), ils doivent simplement ne pas être triviaux. Cependant, il n'y a aucune raison de ne pas penser au problème avec la sécurité à l'esprit, et j'aime prendre le temps de comprendre ce que je fais au moins semi avec compétence:

La solution actuelle (pas par moi!) Était:

replace(str(round(Rand(checksum(newid()))*10000,0),4), ' ','0')

Il y a quelques problèmes avec ça. Bien sûr, ni Rand() ni newid() ne sont des fonctions cryptographiques, mais le problème qui nécessite en fait le changement est que cela générera occasionnellement une broche de **** En raison de bizarreries avec la fonction str().

Désormais, SQL Server possède la fonction crypt_gen_random(), qui renvoie un nombre fixe d'octets de nombres sécurisés générés par cryptographie. Cela me fait penser que quelque chose comme ça devrait fonctionner:

format(cast(crypt_gen_random(2) as int) % 10000,'0000')

Le problème que je vois est que crypt_gen_random(2) est fondamentalement, "Choisissez un nombre compris entre 0 et 65535." Dans cet espace, cependant, il y a un biais pour modulo 10000. Il y a 7 instances de chaque résultat entre 0 et 5535, mais seulement 6 instances de chaque résultat entre 5536 et 9999. C'est une différence de 16%.

Maintenant, je peux penser à une solution facile possible, mais je ne sais pas s'il y a une méthode préférée. Je ne suis pas programmeur en sécurité. Peut simplement augmenter le nombre d'octets générés?

format(cast(crypt_gen_random(7) as bigint) % 10000,'0000')

[Remarque: j'utilise bigint au lieu de int car il prend en charge des nombres allant jusqu'à 8 octets. Je ne génère que 7 octets au lieu de 8 pour éviter les nombres négatifs.]

Si je génère 7 octets au lieu de 2, je génère un nombre compris entre 0 et 72057594037927935 (16 ^ 14). Le problème est toujours là - il y a des valeurs 7205759403793 entre 0 et 7935, mais seulement 7205759403792 entre 7936 et 9999 - mais c'est tellement de dilution que cela n'a pas vraiment d'importance. Nous sommes à 10 ^ -12.

Comme je l'ai dit, cependant, je ne suis pas programmeur en sécurité. Je suis DBA, analyste et administrateur, et tout ce que je sais sur la programmation de sécurité, c'est que je ne suis pas qualifié pour savoir quand je fais une erreur de sécurité.

Existe-t-il une meilleure façon de le faire?


Ma solution pour T-SQL finit essentiellement par ressembler à ceci:

declare @candidate_pin int = cast(crypt_gen_random(2) as int)
while @candidate_pin not between 1 and 9999
    set @candidate_pin = cast(crypt_gen_random(2) as int)

select format(@candidate_pin, '0000') as [pin]

J'espérais éviter d'utiliser une logique procédurale au lieu d'une déclaration déclarative plus simple, mais je ne vais pas essayer de la forcer à devenir un CTE récursif ou une autre solution déclarative.

Je ne suis pas particulièrement préoccupé par la performance ici; si nous avons besoin de 100 nouveaux codes PIN dans un trimestre, quelque chose d'étrange se passe.

13
Bacon Bits

Oui, vous avez besoin d'au moins 14 bits pour générer un nombre compris entre 0 et 9999.

Afin d'éviter les biais, vous pouvez générer 14 bits de données aléatoires en utilisant une bonne source de caractère aléatoire. Si le résultat est <= 9999, vous pouvez l'utiliser comme code PIN. Si le résultat est plus grand, jetez-le et réessayez.

Combien cela gaspille-t-il?

Dans le meilleur des cas, vos bits s'aligneront parfaitement avec votre espace de réponse et vous ne perdrez rien. Vous pouviez générer une valeur aléatoire entre 0 et 255 et tout ce que vous aviez à faire était de générer 8 bits aléatoires. Tout le résultat serait valide, en supposant que votre générateur aléatoire est bon.

Dans le pire des cas, vous auriez besoin d'un nombre aléatoire compris entre 0 et 256, ce qui signifie que vous avez besoin de 9 bits, et donc avoir 512 valeurs possibles. Dans environ 50% de ces cas, vous devrez jeter le résultat.

Dans notre exemple, notre borne inférieure avec 13 bits ne nous donnerait que 8196 valeurs possibles, et notre prochaine cible possible est 16384 valeurs possibles. Étant donné que vous voulez 10000 valeurs possibles, votre espace possible est plus d'une fois et demie plus grand que votre espace cible.

Mais n'ayez crainte, vous travaillez avec des quantités de données relativement faibles. Si vous interrogez 7 octets aléatoires, vous obtenez 56 bits aléatoires, ce qui signifie que 4 "essaie" d'obtenir une "bonne" valeur. Même si nous supposions que vous aviez 50% de chances (le pire des cas) d'obtenir une bonne valeur, en 4 essais, vous avez 93,75% de chances de générer un bon code PIN. Si vous n'avez pas de chance, vous devrez interroger 7 autres octets, ce qui augmenterait vos chances à 99,6% pour choisir un bon code PIN.

Cool! Puis-je avoir une démo?

function GeneratePIN()
{
    // The chance to fail after 1000 rounds is 7.5 * 10^-1203
    for(int round = 0; round < 1000; round++)
    {
        // We generate 7 random bytes, which would give us 4 "tries"
        ulong randomness = SecureRandom.GenerateBytes(7);

        // We can iterate this 4 times before our 7 bytes are exhausted
        for (int try = 0; try < 4; try++)
        {
            ushort possiblePIN = randomness & 0x3FFF; //0x3fff is 0011 1111 1111 1111 in binary, so 14 bits set to 1

            // If we succeed, we return the PIN. Else we try again
            if (possiblePIN < 10000)
            {
                return possiblePIN;
            }
            else
            {
                // =>> is the right shift, assign operator
                // This will shift randomness 14 bits to the right
                randomness =>> 14;
            }
        }

        // If we reached this part, we were unlucky and need to try again.
    }

    // If we reach this part, we flipped a coin 1000 times and always got heads.
    // Maybe buy a lottery ticket
    throw new Exception("Buy a lottery ticket!");
}
14
MechMK1

Tout d'abord, je suis totalement d'accord avec MechMK1 sur l'algorithme approprié pour générer un nombre aléatoire uniformément réparti sécurisé cryptographiquement entre 0 et 10000. Cependant:

il n'y a aucune raison de ne pas penser au problème avec la sécurité à l'esprit

Permet. Nombre aléatoire entre 0 et 10000 contient 13,29 bits d'entropie. Un nombre aléatoire entre 0 et 2 ^ 14% 10000 contient 13,22 bits d'entropie, nous avons perdu 0,067. Cela revient à réduire le nombre de broches de 10000 à 9546. Poursuivre les calculs:

  • CSPRNG (0, 2 ^ 14)% 10000 équivalent à CSPRNG (0, 9546), 0,067 bits d'entropie perdus
  • CSPRNG (0, 2 ^ 16)% 10000 équivalent à CSPRNG (0, 9971), 0,004 bits d'entropie perdus
  • CSPRNG (0, 2 ^ 24)% 10000 équivalent à CSPRNG (0, 9999.99964) [maintenant la différence est inférieure à 1 clé, donc de nature purement statistique], 0,00000005 bits d'entropie perdus
  • CSPRNG (0, 2 ^ 32)% 10000 équivalent à CSPRNG (0, 9999.999999995), 0.0000000000008 bits d'entropie perdus [ordre de grandeur ici, je crains que l'arrondi flottant n'ait pu jouer un rôle plus important que les calculs]

En d'autres termes, utilisez format(cast(crypt_gen_random(3) as int) % 10000,'0000') et récupérez les 51 nanobits de sécurité perdus en utilisant le temps d'implémentation enregistré pour vérifier que le mot de passe de l'administrateur n'est pas en quelque sorte laissé vide.

Tout cela pour dire que:

  • Les clés 13,29 bits offrent une sécurité risible en premier lieu. Je dirais simplement de les laisser à 0000 et de consacrer ce temps et cette capacité mentale économisés à effectuer un tiiiiny audit de sécurité interne? Je parie que vous trouverez des mots de passe moins sécurisés dans des endroits plus importants.
  • KISS fonctionne vraiment
9
Andrew Morozko