web-dev-qa-db-ja.com

クーポンコード生成

クーポンコードを生成したいAYB4ZZ2。ただし、使用済みクーポンにマークを付けて、グローバル番号を制限できるようにしたいと考えています。たとえば、Nです。単純なアプローチは、 "N一意の英数字コードを生成し、データベースに入れて、すべてのクーポン操作でdb検索を実行します。"

しかし、私が知る限り、関数を見つけることもできますMakeCoupon(n)、これは与えられた数をクーポンに変換します-定義済みの長さの文字列。

私が理解している限り、MakeCouponは以下の要件を満たす必要があります:

  • 全単である。逆のMakeNumber(coupon)は効果的に計算できるはずです。

  • MakeCoupon(n)の出力は英数字で、小さくconstantの長さである必要があります-呼び出せるように人間が読める。例えば。 SHA1ダイジェストはこの要件を満たしません。

  • 実用的な独自性。すべての自然なn <= NMakeCoupon(n)の結果は、完全に一意であるか、同じ用語で一意である必要があります。たとえば、MD5は一意です(同じ非常に小さい衝突確率)。

  • (これは定義するのが難しい)単一のクーポンコードから残りのすべてのクーポンを列挙する方法は明らかではないはずです-MakeCoupon(n)MakeCoupon(n + 1)は視覚的に異なります。

    例えば。 000001000002が実際には「視覚的に」異なるわけではないため、ゼロで埋められたnを単に出力するMakeCoupon(n),はこの要件に適合しません。

Q:

次の要件を満たす関数または関数ジェネレーターはありますか?私の検索の試みは、私を [CPAN] CouponCode、 に導くだけです。ただし、対応する関数が全単射であるという要件は満たされません。

48
Yippie-Ki-Yay

基本的に、操作を複数の部分に分割できます。

  1. 何らかの方法で初期番号nを「暗号化」して、2つの連続した番号が(非常に)異なる結果をもたらす
  2. ステップ1の結果から「人間が読み取れる」コードを作成します

ステップ1では、単純なブロック暗号を使用することをお勧めします(たとえば、選択したラウンド関数で Feistel暗号 )。 この質問 も参照してください。

Feistel暗号は、いくつかのラウンドで機能します。各ラウンド中に、入力の半分にラウンド関数が適用され、結果はxoredと残りの半分になり、2つの半分が交換されます。 Feistel暗号の良い点は、ラウンド関数が双方向である必要がないことです(ラウンド関数への入力は各ラウンドの後に変更されずに保持されるため、ラウンド関数の結果は復号化中に再構築できます)。したがって、好きなクレイジーな操作を選択できます:)。また、Feistel暗号は対称的であり、最初の要件を満たします。

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));
}
_

(最後のラウンドの後、スワッピングはありません。コードでは、結果の構築でスワッピングが元に戻されることに注意してください)

要件3と4(関数はtotalであるため、入力が異なると異なる出力が得られ、入力は非公式の定義に従って「完全にスクランブル」されます)また、それ自体が逆です(したがって、要件1を暗黙的に満たしています)。つまり、入力ドメイン(この実装では_0..2^30-1_)の各xに対してcrypt(crypt(x))==xです。また、パフォーマンス要件の面でも安価です。

ステップ2では、選択したベースに結果をエンコードします。たとえば、30ビットの数値をエンコードするには、32文字のアルファベットの6つの「数字」を使用できます(したがって、6 * 5 = 30ビットをエンコードできます)。

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;
}
_

入力0〜9の場合、次のクーポンコードが生成されます。

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

このアプローチには2つの異なる内部「秘密」があることに注意してください。1つ目はラウンド関数と使用されるラウンド数、2つ目は暗号化された結果のエンコードに使用するアルファベットです。ただし、示されている実装は、暗号化の意味では決して安全ではないことに注意してください!

また、示されている関数は、全全単射関数であり、すべての可能な6文字コード(アルファベット以外の文字を含む)が一意の数値を生成するという意味です。誰かがランダムなコードを入力できないようにするには、入力番号に何らかの種類の制限を定義する必要があります。例えば。最初の10.000番号のクーポンのみを発行します。次に、ランダムなクーポンコードが有効になる確率は10000/2 ^ 30 = 0.00001になります(正しいクーポンコードを見つけるには約50000回の試行が必要です)。より多くの「セキュリティ」が必要な場合は、ビットサイズ/クーポンコードの長さを増やすことができます(以下を参照)。

編集:クーポンコードの長さを変更する

結果のクーポンコードの長さを変更するには、いくつかの計算が必要です。最初の(暗号化)ステップは、偶数ビットカウントのビット文字列でのみ機能します(Feistel暗号が機能するために必要です)。

2番目のステップでは、特定のアルファベットを使用してエンコードできるビット数は、選択したアルファベットの「サイズ」とクーポンコードの長さに依存します。ビットで与えられるこの「エントロピー」は、一般に、整数ではなく、偶数の整数よりはるかに少ないです。例えば:

30文字のアルファベットを使用する5桁のコードは、30 = 5の可能なコードになります。これは、ld(30 ^ 5)= 24.53ビット/クーポンコードを意味します。

4桁のコードの場合、簡単な解決策があります。32文字のアルファベットを指定すると、* ld(32 ^ 4)= 5 * 4 = 20 *ビットをエンコードできます。したがって、BITCOUNTを20に設定し、コードの2番目の部分のforループを変更して、_4_(_6_の代わり)まで実行することができます。

5桁のコードを生成することは少し複雑で、アルゴリズムを「弱める」:BITCOUNTを24に設定し、30文字のアルファベットから5桁のコードを生成できます(2文字をALPHABET文字列を使用し、forループを_5_まで実行します。

しかし、これはすべての可能な5桁のコードを生成するわけではありません:24ビットでは、暗号化ステージから16,777,216の値のみを取得でき、5桁のコードは24,300,000の数値をエンコードできるため、いくつかの可能なコードは生成されません。具体的には、コードの最後の位置にアルファベットの一部の文字が含まれることはありません。明らかな方法で有効なコードのセットを絞り込むため、これは欠点と見なすことができます。

クーポンコードをデコードする場合、最初にcodeFromCoupon関数を実行してから、結果のビット25が設定されているかどうかを確認する必要があります。これにより、すぐに拒否できる無効なコードがマークされます。実際には、これは、アルゴリズムのすべての内部構造を明かすことなく、コードの妥当性の迅速なチェック(クライアント側など)を可能にするため、利点になることさえあることに注意してください。

ビット25が設定されていない場合は、crypt関数を呼び出して元の番号を取得します。

72
MartinStettner

私はこの答えのためにドッキングするかもしれませんが、私は応答する必要があるように感じます-私は本当に多くの痛みを伴う経験から来るので、あなたが私が言っていることを聞くことを本当に望みます。

このタスクは非常に学問的に困難であり、ソフトウェアエンジニアは彼らの知性と問題の解決に挑戦するになりがちですが、可能であれば、これに関する何らかの指示を提供する必要があります。世界には小売店はありません。とにかくどんな種類の成功も持っていて、生成されるそれぞれのentityを非常によく追跡していません。各在庫から、クーポンやギフトカードまで、彼らはそれらのドアを送り出します。人があなたをだまそうとしているわけではないので、それがちょうど良いスチュワードではありません。あなたの武器庫にすべての可能なアイテムがあれば準備ができています。

次に、シナリオでクーポンを使用するプロセスについて説明しましょう。

顧客がクーポンを引き換えるときに、ある種のPOSシステムが前面に表示されますか?そして、それは彼らがクーポンコードを入力するだけのオンラインビジネスかもしれないし、レジがバーコードを正しくスキャンするレジスターかもしれない(私たちはここで扱っているものだと仮定している) =?そして今、ベンダーとして、あなたはあなたが有効なクーポンコードを持っているなら、私はあなたにある種の割引を与えるつもりだと言っていますand =私たちの目標は、リバーシブルなクーポンコードを生成することだったので、そのコードを検証するためのデータベースは必要なく、ちょうど元に戻すことができます!数学だけですよね? まあ、はい、いいえ。

はい、あなたは正しいです、それはただの数学です。実際、 SSLのクラッキング であるため、これも問題です。しかし、SSLで使用される計算は、ここで使用されるものよりも少し複雑であることに気付くと思いますandキーは実質的に大きいです。

それはあなたのことではありません。また、特にmoneyに関しては、誰もが破るのに十分気にしないと確信しているようなスキームを試してみることは賢明でもありません。クーポンコードを使用する人から身を守る必要があるため、本当に解決しようとしてはならない問題を解決しようとして、あなたの人生を非常に難しくしています。

したがって、この問題は不必要に複雑であり、このように解決できます。

// 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. 自動インクリメントキーを持つクーポンテーブルを作成します。
  2. そのテーブルに挿入し、自動インクリメントキーを取得します。
  3. Base64はそのIDをクーポンコードにエンコードします。
  4. 必要に応じて、その文字列を切り捨てます。
  5. その文字列を、クーポンを挿入しただけでデータベースに保存します。
12
Mike Perrenoud

必要なものは Format-preserving encryption と呼ばれます。

一般性を失うことなく、base 36でエンコードすることにより、シンボルの文字列ではなく、0..M-1の整数について話していると想定できます。 Mはおそらく2のべき乗でなければなりません。

秘密キーを選択してMを指定すると、FPEは0..M-1encryptの擬似ランダム順列とその逆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;
}

FPEがセキュリティで保護されている場合、このスキームはセキュリティで保護されています。攻撃者は、推測することに成功したとしても、任意の多くのクーポンの知識があれば、O(N/M)彼が知っている各クーポンに関連付けられた番号。

これはまだ比較的新しい分野であるため、このような暗号化スキームの実装はほとんどありません。 このcrypto.SEの質問 のみに言及しています Botan 、Perl/Pythonバインディングを持つC++ライブラリで、C#ではありません。

注意事項:FPEの標準はまだ十分に受け入れられていないという事実に加えて、実装にバグがある可能性を考慮する必要があります。回線に多額の資金がある場合、データベースを回避することの比較的小さな利点とそのリスクを比較検討する必要があります。

5
Generic Human

Base-36番号システムを使用できます。 coupen出力に6文字が必要だと仮定します。

makeCouponの擬似コード

MakeCoupon(n){

すべての値を0に初期化する0..9、A..Zから始まる数字を想定した対応するASCIIコード。このコンベンションでは、6桁を文字列として出力します。

}

これで、数値の計算はこの操作の逆になります。

これは、6文字が許可されている非常に大きな数字(35 ^ 6)で機能します。

3
PermanentGuest
  • 暗号化関数cを選択します。 cにはいくつかの要件がありますが、ここではSHA1を取り上げます。

  • 秘密鍵kを選択します。

番号nのクーポンコード生成関数は次のようになります。

  • nとkを"n"+"k"として連結します(これはパスワード管理でソルティングとして知られています)
  • c( "n" + "k")を計算します
  • sHA1の結果は160ビットで、ASCII文字列として)(たとえばbase64で)エンコードします
  • 結果が長すぎる場合(SHA1の場合と同じように)、最初の10文字のみを保持するように切り捨て、この文字列にsという名前を付けます
  • クーポンコードはprintf "%09d%s" n s、つまり、ゼロが埋め込まれたnと切り捨てられたハッシュsの連結です。

はい、nクーポンコードの番号を推測するのは簡単です(ただし、以下を参照してください)。しかし、別の有効なコードを生成することは困難です。

要件が満たされています。

  1. 逆関数を計算するには、コードの最初の9桁を読むだけです
  2. 長さは常に19(nの9桁に加えてハッシュの10文字)です。
  3. 最初の9桁が一意であるため、一意です。最後の10文字も高確率であります。
  4. SHA1を使用したと推測された場合でも、ハッシュの生成方法は明らかではありません。


コメント:

  • nの読み取りがあまりにも明白であることが心配な場合は、base64エンコードのように軽く難読化して、コード内でnsの文字を交互に変えることができます。
  • 10億個を超えるコード、つまり9桁でnを印刷する必要はないと想定していますが、もちろん、パラメーター9と10を希望のクーポンコード長に調整できます。
  • SHA1は単なるオプションです。秘密キー暗号化などの別の暗号化機能を使用できますが、切り捨てられたときやクリアテキストが提供されたときに、この機能が強力であることを確認する必要があります。
  • これはコード長が最適ではありませんが、シンプルで広く利用可能なライブラリという利点があります。
2
jrouquie