web-dev-qa-db-fra.com

Génération de code promo

Je souhaite générer des codes promo, par ex. AYB4ZZ2. Cependant, je voudrais également pouvoir marquer les coupons utilisés et limiter leur nombre global, disons N. L'approche naïve serait quelque chose comme "générer N des codes alphanumériques uniques, les mettre dans la base de données et effectuer une recherche db sur chaque opération de coupon."

Cependant, autant que je sache, nous pouvons également essayer de trouver une fonction MakeCoupon(n), qui convertit le nombre donné en une chaîne de type coupon avec une longueur prédéfinie .

Pour autant que je comprends, MakeCoupon devrait remplir les conditions suivantes:

  • Soyez bijectif. Son inverse MakeNumber(coupon) devrait être effectivement calculable.

  • La sortie de MakeCoupon(n) doit être alphanumérique et doit avoir une petite longueur constante - pour pouvoir être appelée lisible par l'homme. Par exemple. SHA1 Digest ne répondrait pas à cette exigence.

  • Unicité pratique. Les résultats de MakeCoupon(n) pour chaque n <= N Naturel devraient être totalement uniques ou uniques dans les mêmes termes que, par exemple, MD5 Est unique (avec la même probabilité de collision extrêmement faible) .

  • (celui-ci est difficile à définir) Il ne devrait pas être évident comment énumérer tous les coupons restants à partir d'un seul code de coupon - disons MakeCoupon(n) et MakeCoupon(n + 1) devrait différer visuellement.

    Par exemple. MakeCoupon(n), qui affiche simplement n complétée par des zéros ne répondrait pas à cette exigence, car 000001 et 000002 ne diffèrent pas réellement "visuellement".

Q:

Existe-t-il une fonction ou un générateur de fonctions qui remplit les conditions suivantes? Mes tentatives de recherche ne m'ont conduit qu'à [CPAN] CouponCode, mais il ne remplit pas l'exigence que la fonction correspondante soit bijective.

48
Yippie-Ki-Yay

Fondamentalement, vous pouvez diviser votre opération en plusieurs parties:

  1. "Chiffrez" en quelque sorte votre numéro initial n, de sorte que deux nombres consécutifs donnent des résultats (très) différents
  2. Construisez votre code "lisible par l'homme" à partir du résultat de l'étape 1

Pour l'étape 1, je suggère d'utiliser un chiffrement par bloc simple (par exemple, un chiffrement Feistel avec une fonction ronde de votre choix). Voir aussi cette question .

Les chiffrements Feistel fonctionnent en plusieurs tours. À chaque tour, une fonction round est appliquée à la moitié de l'entrée, le résultat est xored avec l'autre moitié et les deux moitiés sont échangées. La bonne chose à propos des chiffres de Feistel est que la fonction ronde ne doit pas être bidirectionnelle (l'entrée de la fonction ronde est conservée non modifiée après chaque tour, donc le résultat de la fonction ronde peut être reconstruit pendant le décryptage). Par conséquent, vous pouvez choisir les opérations folles que vous aimez :). Les chiffres Feistel sont également symétriques, ce qui répond à votre première exigence.

Un petit exemple en C #

const int BITCOUNT = 30;
const int BITMASK = (1 << BITCOUNT/2) - 1;

static uint roundFunction(uint number) {
  return (((number ^ 47894) + 25) << 1) & BITMASK;
}

static uint crypt(uint number) {
  uint left = number >> (BITCOUNT/2);
  uint right = number & BITMASK;
  for (int round = 0; round < 10; ++round) {
    left = left ^ roundFunction(right);
    uint temp = left; left = right; right = temp;
  }
  return left | (right << (BITCOUNT/2));
}

(Notez qu'après le dernier tour, il n'y a pas d'échange, dans le code, l'échange est simplement annulé dans la construction du résultat)

En plus de répondre à vos exigences 3 et 4 (la fonction est total, donc pour différentes entrées, vous obtenez différentes sorties et l'entrée est "totalement brouillée" selon votre définition informelle), c'est aussi sa propre inverse (remplissant ainsi implicitement l'exigence 1), c'est-à-dire crypt(crypt(x))==x pour chaque x dans le domaine d'entrée (0..2^30-1 dans cette implémentation). Il est également bon marché en termes d'exigences de performance.

Pour l'étape 2, encodez simplement le résultat dans la base de votre choix. Par exemple, pour encoder un nombre de 30 bits, vous pouvez utiliser 6 "chiffres" d'un alphabet de 32 caractères (vous pouvez donc encoder 6 * 5 = 30 bits).

Un exemple pour cette étape en C #:

const string ALPHABET= "AG8FOLE2WVTCPY5ZH3NIUDBXSMQK7946";
static string couponCode(uint number) {
  StringBuilder b = new StringBuilder();
  for (int i=0; i<6; ++i) {
    b.Append(ALPHABET[(int)number&((1 << 5)-1)]);
    number = number >> 5;
  }
  return b.ToString();
}
static uint codeFromCoupon(string coupon) {
  uint n = 0;
  for (int i = 0; i < 6; ++i)
    n = n | (((uint)ALPHABET.IndexOf(coupon[i])) << (5 * i));
  return n;
}

Pour les entrées 0 à 9, cela donne les codes de réduction suivants

0 => 5VZNKB
1 => HL766Z
2 => TMGSEY
3 => P28L4W
4 => EM5EWD
5 => WIACCZ
6 => 8DEPDA
7 => OQE33A
8 => 4SEQ5A
9 => AVAXS5

Notez que cette approche a deux "secrets" internes différents: premièrement, la fonction de tour avec le nombre de tours utilisés et deuxièmement, l'alphabet que vous utilisez pour encoder le résultat encodé. Mais notez également que l'implémentation présentée n'est en aucun cas sécurisée au sens cryptographique!

Notez également que la fonction affichée est une fonction bijective totale, dans le sens où chaque code à 6 caractères possible (avec des caractères hors de votre alphabet) produira un nombre unique. Pour empêcher quiconque d'entrer juste un code aléatoire, vous devez définir une sorte de restriction sur le numéro d'entrée. Par exemple. émettez uniquement des coupons pour les 10 000 premiers numéros. Ensuite, la probabilité qu'un code coupon aléatoire soit valide serait 10000/2 ^ 30 = 0,00001 (il faudrait environ 50000 tentatives pour trouver un code coupon correct). Si vous avez besoin de plus de "sécurité", vous pouvez simplement augmenter la taille du bit/la longueur du code coupon (voir ci-dessous).

EDIT: Modifier la longueur du code coupon

La modification de la longueur du code coupon résultant nécessite un peu de calcul: la première étape (de chiffrement) ne fonctionne que sur une chaîne de bits avec un nombre de bits pair (cela est requis pour que le chiffrement Feistel fonctionne).

Dans la deuxième étape, le nombre de bits qui peuvent être encodés en utilisant un alphabet donné dépend de la "taille" de l'alphabet choisi et de la longueur du code coupon. Cette "entropie", donnée en bits, n'est, en général, pas un nombre entier, encore moins un nombre entier pair. Par exemple:

Un code à 5 chiffres utilisant un alphabet de 30 caractères donne 30 ^ 5 codes possibles, ce qui signifie ld (30 ^ 5) = 24,53 bits/code coupon.

Pour un code à quatre chiffres, il existe une solution simple: étant donné un alphabet à 32 caractères, vous pouvez encoder * ld (32 ^ 4) = 5 * 4 = 20 * Bits. Vous pouvez donc simplement définir BITCOUNT sur 20 et modifier la boucle for dans la deuxième partie du code pour qu'elle s'exécute jusqu'à 4 (Au lieu de 6)

La génération d'un code à cinq chiffres est un peu plus délicate et "affaiblit" l'algorithme: vous pouvez définir le BITCOUNT sur 24 et générer simplement un code à 5 chiffres à partir d'un alphabet de 30 caractères (supprimez deux caractères du ALPHABET chaîne et laissez la boucle for s'exécuter jusqu'à 5).

Mais cela ne générera pas tous les codes à 5 chiffres possibles: avec 24 bits, vous ne pouvez obtenir que 16777216 valeurs possibles de l'étape de cryptage, les codes à 5 chiffres pourraient coder 24300000 nombres possibles, donc certains codes possibles ne seront jamais générés. Plus précisément, la dernière position du code ne contiendra jamais certains caractères de l'alphabet. Cela peut être considéré comme un inconvénient, car il réduit de manière évidente l'ensemble des codes valides.

Lors du décodage d'un code de coupon, vous devrez d'abord exécuter la fonction codeFromCoupon puis vérifier si le bit 25 du résultat est défini. Cela marquerait un code invalide que vous pouvez immédiatement rejeter. Notez que, dans la pratique, cela pourrait même être un avantage, car cela permet une vérification rapide (par exemple du côté client) de la validité d'un code sans révéler toutes les parties internes de l'algorithme.

Si le bit 25 n'est pas défini, vous appellerez la fonction crypt et obtiendrez le numéro d'origine.

72
MartinStettner

Bien que je puisse être amarré pour cette réponse, je sens que je dois répondre - j'espère vraiment que vous entendrez ce que je dis car cela vient de beaucoup d'expérience douloureuse.

Bien que cette tâche soit très difficile sur le plan académique, et les ingénieurs logiciels ont tendance à contester leurs problèmes d'intelect contre la résolution de problèmes , je dois vous donner quelques indications à ce sujet si vous le permettez. Il n'y a pas de magasin de détail dans le monde, qui a de toute façon un succès, qui ne garde pas une très bonne trace de chaque entité qui est généré; de chaque pièce d'inventaire à chaque coupon ou carte-cadeau qu'ils envoient. Ce n'est tout simplement pas un bon intendant si vous l'êtes, car ce n'est pas si les gens vont vous tromper, c'est quand, et donc si vous avez tous les éléments possibles dans votre arsenal, vous serez prêt.

Maintenant, parlons du processus par lequel le coupon est utilisé dans votre scénario.

Lorsque le client échangera le coupon, il y aura une sorte de système de point de vente devant, non? Et cela peut même être une entreprise en ligne où ils peuvent alors simplement entrer leur code de coupon par rapport à un registre où le caissier scanne un code-barres à droite (je suppose que c'est ce que nous avons affaire avec ici) ? Et maintenant, en tant que vendeur, vous dites que si vous avez un code promo valide, je vais vous donner une sorte de réduction et parce que notre objectif était de générer des codes promo réversibles, nous n'avons pas besoin d'une base de données pour vérifier ce code, nous pouvons simplement l'inverser correctement! Je veux dire, c'est juste des maths non? Eh bien, oui et non.

Oui, tu as raison, c'est juste des maths. En fait, c'est aussi le problème parce que cracking SSL . Mais, je vais supposer que nous réalisons tous que les mathématiques utilisées dans SSL sont juste un peu plus complexes que tout ce qui est utilisé ici et la clé est sensiblement plus grand.

Il ne vous appartient pas, et il n'est pas sage pour vous d'essayer de trouver une sorte de schéma que vous êtes juste sûr que personne ne se soucie assez de casser, surtout en ce qui concerne l'argent . Vous rendez votre vie très difficile en essayant de résoudre un problème que vous ne devriez vraiment pas essayer de résoudre parce que vous devez vous protéger de ceux qui utilisent les codes de réduction.

Par conséquent, ce problème est inutilement compliqué et pourrait être résolu comme ceci.

// insert a record into the database for the coupon
// thus generating an auto-incrementing key
var id = [some code to insert into database and get back the key]

// base64 encode the resulting key value
var couponCode = Convert.ToBase64String(id);

// truncate the coupon code if you like

// update the database with the coupon code
  1. Créez une table de coupons dotée d'une clé à incrémentation automatique.
  2. Insérez dans ce tableau et récupérez la clé d'incrémentation automatique.
  3. Base64 code cet identifiant dans un code de coupon.
  4. Tronquez cette chaîne si vous le souhaitez.
  5. Enregistrez cette chaîne dans la base de données avec le coupon juste inséré.
12
Mike Perrenoud

Ce que vous voulez s'appelle cryptage préservant le format .

Sans perte de généralité, en encodant en base 36, nous pouvons supposer que nous parlons d'entiers dans 0..M-1 plutôt que des chaînes de symboles. M devrait probablement être une puissance de 2.

Après avoir choisi une clé secrète et spécifié M, FPE vous donne une permutation pseudo-aléatoire de 0..M-1encrypt avec son inverse decrypt.

string GenerateCoupon(int n) {
    Debug.Assert(0 <= n && n < N);
    return Base36.Encode(encrypt(n));
}

boolean IsCoupon(string code) {
    return decrypt(Base36.Decode(code)) < N;
}

Si votre FPE est sécurisé, ce schéma est sécurisé: aucun attaquant ne peut générer d'autres codes promo avec une probabilité supérieure à O(N/M) étant donné la connaissance arbitraire de nombreux coupons, même s'il parvient à deviner le numéro associé à chaque coupon qu'il connaît.

Il s'agit encore d'un domaine relativement nouveau, il existe donc peu d'implémentations de tels schémas de chiffrement. Cette question crypto.SE mentionne uniquement Botan , une bibliothèque C++ avec des liaisons Perl/Python, mais pas C #.

Avertissement: en plus du fait qu'il n'existe pas encore de normes bien acceptées pour FPE, vous devez considérer la possibilité d'un bogue dans la mise en œuvre. S'il y a beaucoup d'argent sur la ligne, vous devez comparer ce risque avec l'avantage relativement faible d'éviter une base de données.

5
Generic Human

Vous pouvez utiliser un système numérique de base 36. Supposons que vous vouliez 6 caractères dans la sortie du coupon.

pseudo-code pour MakeCoupon

MakeCoupon (n) {

Avoir un tableau d'octets de taille fixe, disons 6. Initialisez toutes les valeurs à 0. convertissez le nombre en base - 36 et stockez les "chiffres" dans un tableau (en utilisant la division entière et les opérations de mod) Maintenant, pour chaque "chiffre", trouvez le code ascii correspondant en supposant que les chiffres commencent à 0..9, A..Z Avec cette sortie de convension, six chiffres sous forme de chaîne.

}

Maintenant, le calcul du nombre en arrière est l'inverse de cette opération.

Cela fonctionnerait avec de très grands nombres (35 ^ 6) avec 6 caractères autorisés.

3
PermanentGuest
  • Choisissez une fonction cryptographique c. Il y a quelques exigences sur c, mais pour l'instant prenons SHA1.

  • choisissez une clé secrète k.

Votre fonction de génération de code promo pourrait être, pour le numéro n:

  • concaténer n et k comme "n"+"k" (c'est ce qu'on appelle le salage dans la gestion des mots de passe)
  • calculer c ("n" + "k")
  • le résultat de SHA1 est 160 bits, les encoder (par exemple avec base64) sous la forme d'une chaîne ASCII
  • si le résultat est trop long (comme vous l'avez dit c'est le cas pour SHA1), tronquez-le pour ne garder que les 10 premières lettres et nommez cette chaîne s
  • votre code promo est printf "%09d%s" n s, c'est-à-dire la concaténation de n et du hachage tronqué s.

Oui, il est trivial de deviner n le numéro du code promo (mais voir ci-dessous). Mais il est difficile de générer un autre code valide.

Vos exigences sont satisfaites:

  1. Pour calculer la fonction inverse, il suffit de lire les 9 premiers chiffres du code
  2. La longueur est toujours de 19 (9 chiffres de n, plus 10 lettres de hachage)
  3. Il est unique, car les 9 premiers chiffres sont uniques. Les 10 derniers caractères le sont aussi, avec une forte probabilité.
  4. Il n'est pas évident de savoir comment générer le hachage, même si l'on devine que vous avez utilisé SHA1.


Certains commentaires:

  • Si vous craignez que la lecture de n soit trop évidente, vous pouvez l'obscurcir légèrement, comme le codage base64, et en alternant dans le code les caractères de n et s.
  • Je suppose que vous n'aurez pas besoin de plus d'un milliard de codes, d'où l'impression de n sur 9 chiffres, mais vous pouvez bien sûr ajuster les paramètres 9 et 10 à la longueur de code de coupon souhaitée.
  • SHA1 n'est qu'une option, vous pouvez utiliser une autre fonction cryptographique comme le cryptage à clé privée, mais vous devez vérifier que cette fonction reste solide lorsqu'elle est tronquée et lorsque le texte clair est fourni.
  • Ce n'est pas optimal en longueur de code, mais a l'avantage de la simplicité et des bibliothèques largement disponibles.
2
jrouquie