最近この質問を投稿しました ユーザーがオンラインで利用できるギフトカードのようなバウチャーのコードについて。私は、大きなキースペース、低い推測可能性、および人間の読みやすさの間の最良のトレードオフを見つけたかったのです。実装に取り掛かった今、私はまったく別の問題を抱えていることに気付きました。それは、よりアルゴリズム的な課題です。
簡単にするためにAからZまでの10文字など、いくつかのコード形式を採用し、バウチャーの生成を開始するとします。これを行うための正しいアルゴリズムは何ですか?!
私の最初のアプローチは、0から308,915,776までのすべての可能なコードに番号を付けてから、その範囲で乱数の生成を開始することです。ただし、これには明らかに大きな問題があります。以前に生成されたすべてのバウチャーコードに対して乱数を確認する必要があり、既存のバウチャーコードと衝突する場合は、コードを破棄して別のコードを試す必要があります。システムがより多くのデータを蓄積すると、速度が低下します。極端な場合、コードが1つしか残っていない場合、システムがそれを正しく推測することはほぼ不可能です。
すべてのコードを事前に生成してシャッフルしてから、順番に使用することができます。しかし、これは私が多くのコードを格納しなければならないことを意味し、実際、私のキースペースは私が説明したものよりも大きいので、非常に大量のデータについて話している。したがって、それもあまり望ましくありません。
したがって、これにより、コードを順番に使用することになります。ただし、推測可能なバウチャーコードは必要ありません。バウチャー「AAAAAAAAAY」を購入したユーザーは、「AAAAAAAAAZ」と入力した場合、別の有効なコードを取得する可能性が高くないはずです。
アルファベットと位置をシャッフルして、代わりに
「ABCDEFGHIJKLMNOPQRSTUVWXYZ」を使用します
「LYFZTGKBNDRAPWEOXQHVJSUMIC」
位置の代わりに
9 8 7 6 5 4 3 2 10位置は
1 8 0 7 5 4 3 9 2 6
このロジックを使用して、コードを指定します
LNWHDTECMA
次のコードは
LNEHDTECMA
これは間違いなく推測しにくいです。しかし、それらはまだ互いに1文字しか離れておらず、これらのバウチャーを2つだけ指定すると、どの位置が増加しているかがわかり、24回以下の推測で次のコードを取得する可能性が90%になります。
私の「エスケープハッチ」は、これらすべてを捨ててGUIDを使用することです。ユーザーが入力する必要のある文字数よりも多くの文字があり、I/1やO/0のような類似の文字が含まれていますが、魔法のように上記の頭痛の種をすべて解消します。それでも、私はこれについて考えるのを楽しんでいます、多分あなたもそうです。私はいくつかの代替案を聞きたいです。あなたはどれだけ持ってる?
ありがとう!
ランダムに生成された2つのコードが衝突する可能性は、基本的にユーザーが有効なコードを推測するのと同じです。ユーザーが推測するのを防ぐことはできません。したがって、必須実際に使用されるコードの数よりもはるかに大きいキースペースがあるため、ランダムな衝突も非常に起こりそうにありません(ただし、誕生日のパラドックスのおかげで、それらを完全に無視するのに十分な可能性は低いでしょうが、少なくともコードを適度に短くしたい場合)、既存のコードと照合し、衝突が発生した場合に再生成することは、完全に実行可能な戦略です。
Nビットのシリアル番号Rを、連結されたペア(R、S)のMビットハッシュHと組み合わせて使用します。ここで、Sは、実行する秘密の「塩」Sです[〜#〜] not [〜#〜]公開しません。次に、ペア(R、H)を任意の可逆的な方法でアルファベット順にエンコードします。 MD5 *やSHAのようなアルゴリズムが好きで、ビット数が多すぎる場合は、標準のハッシュアルゴリズムの最下位Mビットを使用してください。
簡単に確認できます。英数字エンコーディングをデコードして、RとHが表示されるようにします。次に、H '= hash(R + S)を計算し、H = H'であることを確認します。
edit:Rは、増分シリアル番号またはランダムなどにすることができます。各値を1回だけ使用するようにしてください。
*誰かが「MD5が壊れている」と言う前に、MD5の既知の弱点は衝突攻撃であり、not原像攻撃 であることを思い出させてください。また、公開されていない秘密のソルト値を使用することにより、攻撃者がソルト値を推測できない限り、セキュリティメカニズムをテストする機能を拒否します。妄想を感じる場合は、2つのソルト値SprefixとSsuffixを選択し、連結されたトリプル(Sprefix、R、Ssuffix)のハッシュを計算します。
一部の乱数ジェネレーターには興味深い特性があります。正しく使用すると、長期間にわたって重複する数値が生成されません。それらは フルサイクル と呼ばれるものを生成します。そこで説明されているアルゴリズムの1つを使用してシードすると、多くの一意の番号が得られます。
数字を文字にマッピングするスマートな方法を追加すると、コードを取得できます。
「完璧なハッシュ」を使用すると思います- http://en.wikipedia.org/wiki/Perfect_hash_function 4桁の乱数と組み合わせて...
したがって、バウチャーコードを毎回インクリメントしてからハッシュし、4桁の乱数を追加すると、最後にチェックディジットも追加されます(Alix Axelが提案したように)。
これは衝突がなく非常に安全です。たとえば、誰かがハッシュアルゴリズムを作成した場合、最後に4桁のコードを推測する必要があります...
Programming Pearls 乱数のセットを生成するアルゴリズムの例がいくつかあります。この種の問題に興味がある場合は、それを読む必要があります。
この本は、値がm
未満のn
乱数を生成する場合、数値を生成して重複を破棄するという単純なアプローチでは、2m
の場合、m < n / 2
以下の乱数を生成することを示しています。これがC++の場合です。
void gensets(int m, int n)
{
set<int> S;
set<int>::iterator i;
while (S.size() < m) {
int t = bigrand() % n;
S.insert(t);
}
for (i = S.begin(); i != S.end(); ++i)
cout << *i << "\n";
}
明らかに、人々が値を推測することを心配している場合は、m
をn / 2
よりもはるかに小さくする必要があります。
m
未満のn
乱数を生成するセットベースのアルゴリズムもあり、各値は同じ確率で重複せず、m
を超える乱数を生成しないことが保証されています。数字:
void genfloyd(int m, int n)
{
set<int> S;
set<int>::iterator i;
for (int j = n-m; j < n; j++) {
int t = bigrand() % (j+1);
if (S.find(t) == S.end())
S.insert(t); // t not in S
else
S.insert(j); // t in S
}
for (i = S.begin(); i != S.end(); ++i)
cout << *i << "\n";
}
ただし、番号の順序はランダムではないため、これはおそらく適切な選択ではありません。
私も他の質問に答えました:P
最良の方法は、8文字になるまで、一度に1文字の英数字をランダムに生成することです。これがバウチャーになります。
理想的には、重複があるかどうかを安全に想定できるように、十分な長さのシーケンスを選択するのが最善の方法です。 誕生日の問題 のため、おそらく直感に反して、これは思ったよりも頻繁に発生することに注意してください。
たとえば、8文字の場合、1785793904896の組み合わせが可能ですが、1,573,415枚のバウチャーしか生成しない場合、50%の確率で重複する可能性があります。
したがって、それはすべて、生成するコードの数と、快適なコードの最大長によって異なります。多くを生成していて、それを短くしたい場合は、以前に生成したものを保存し、データベースに対して重複がないか確認する必要があります。
コメント全体を読んだところ、保護するために他の多くの人々が非常に巧妙で洗練された手段を使用していることがわかりました。私のアルゴリズムを推測できる可能性は1/2600000です。あなたがしなければならないのは、世代ごとにソルトプレフィックスソルトサフィックスを変更することだけです。
sprefix +random_numbers+ssuffix
これは他のすべての答えの最良の部分の要約です。 :)
次のようなギフトカード番号を生成する必要があります。
乱数は推測できませんが、必ずしも一意ではありません。さまざまなアルゴリズムによって生成される数値は一意ですが、推測可能です(アルゴリズムはリバースエンジニアリングできます)。両方のプロパティを提供する単一のアルゴリズムを知りません。リバースエンジニアリングに逆らう必要があるため、暗号化の領域に分類されます。もちろん、専門家でない人は暗号システムを設計しようとすべきではありません。
幸い、同じアルゴリズムから両方のプロパティを取得する必要はありません。ギフトカードコードは、2つの部分で構成できます。一意の部分( 線形合同法 、おそらく、またはモジュロ算術、または毎回インクリメントする整数を使用して生成されます)と部分です。それは推測できません(単なる乱数)。
効果的に機能するのは、単に作成時間を有利に使用することです。たとえば、年の最後の2桁、2桁の月、2桁の日、2桁の時、2桁の分、2桁の秒を入力してから、秒をたとえばマイクロ秒まで実行します。さらに難読化する必要がある場合は、事前にスクランブルをかけます(たとえば、YYMMddhmmssではなくMYmdshhdMmYs)。次に、ベースを(おそらく、十五進法に)変更して、推測の試みをさらに拒否します。これには2つの大きな利点があります。1-年を含む日付を使用すると、同じ時間が2回経過しないため、重複が破棄されます。 100年後になって初めてリスクがあります。唯一の懸念は、同じマイクロ秒で2つ作成される可能性があることです。そのため、一度に複数の作成を禁止するのは簡単な作業です。ミリ秒の遅延で問題が解決します。
2-推測は非常に困難になります。数字(および文字!)の基数と順序を把握することは困難な作業になるだけでなく、マイクロ秒に達するとシーケンスはほとんど無関係になります。顧客が購入したマイクロ秒数と時計があなたの時計とどのように一致するかを理解するのがどれほど難しいかは言うまでもありません。
異議は「待ってください!つまり17桁(YYMMDDhhmmss.sssss)ですが、後で大きなベースに持ち出すと減少します。10個の数字と26文字を使用してベース36に移動すると、11桁のコードですべての可能性がカバーされます。大文字と小文字が互換性がない場合、データは問題なく10桁の目標に圧縮できます。
Jason Orendoffの回答 に基づいて、ギフトカードコードを生成するアルゴリズムをまとめました。基本的に、2つの40ビット番号があります。1つは一意であることを保証するためのもので、もう1つは推測が難しいことを保証するためのものです。
次に、合計80ビットのシーケンスが Base32 を使用して16文字の文字列に変換されます。
import Java.security.SecureRandom;
import Java.util.Random;
import Java.util.concurrent.atomic.AtomicLong;
import org.Apache.commons.codec.binary.Base32;
public class GiftCardUtil {
private AtomicLong sequence;
private Random random;
public GiftCardUtil() {
// 1325383200000L == 1 Jan 2012
sequence = new AtomicLong(System.currentTimeMillis() - 1325383200000L);
random = new SecureRandom();
}
public String generateCode() {
System.out.println(sequence.get());
byte[] id = new byte[10];
longTo5ByteArray(sequence.incrementAndGet(), id);
byte[] rnd = new byte[5];
random.nextBytes(rnd);
System.arraycopy(rnd, 0, id, 5, 5);
return new Base32().encodeAsString(id);
}
private void longTo5ByteArray(long l, byte[] b) {
b[0] = (byte) (l >>> 32);
b[1] = (byte) (l >>> 24);
b[2] = (byte) (l >>> 16);
b[3] = (byte) (l >>> 8);
b[4] = (byte) (l >>> 0);
}
}
アンドレアスが提案したのが最善の方法だと思います。しかし、私の答えは興味深い関連する議論についてです。
S = {1、...、MAX}の順列を一緒に形成する一連の数値を生成したいとします。これを行う1つの方法は、S上の巡回群の要素を取得することです。たとえば、p
が互いに素である場合、数値R = {x modulo p, x^2 modulo p, x^3 modulo p, ..., x^(p-1) modulo p}
は{1, ..., p-1}
上の巡回群を形成します。 x
はp
と互いに素です。したがって、素数としてMAXを選択する場合は、このシーケンスを使用します。
「クラックするのが難しい」シーケンスが必要です。十分にタフなシーケンスのジェネレーターは、疑似ランダムジェネレーターと呼ばれます(もちろん、おそらく必要ありませんthatタフなクラック)。例として、上記のR
の要素の最後の桁があります。ただし、p
は秘密にされています(正しいですか?)。しかし、Andreasによる回答では、すでに(疑似)乱数のソースが使用されているため、疑似乱数ジェネレーターとは言えません。
疑似乱数ジェネレータに興味がある場合は、Knuthの有名な本の第2巻で詳細に説明されています。
次に、暗号化ハッシュの使用について説明します。MD5からビットを取得するのは非常に簡単です。読みやすくするために、私は次のアイデアを思いつきました。単語のリストを取得し、キーのビットを使用して単語のリストにインデックスを付けます。私のワードリストは約100000ワードなので、ワードあたり約16ビットで、4ワードの場合は64ビットのキースペースになります。結果は通常、非常に読みやすくなっています。
たとえば、前の段落の暗号署名は次のとおりです。
神風特攻隊のフレッシュマンションの吐き気
(私の単語リストはより大きなキースペースに傾いています。より短いフレーズが必要な場合は、単語が少なくなります。)
MD5ライブラリが手元にある場合、この戦略は非常に簡単に実装できます。私は約40行のLuaで実装しています。
ただし、次のとおりです。
この関連するSO質問:(非常に)安全な「ハッシュ」ではなく、小さい(<10桁)を作成するアイデア も参照してください。 =。
この方法をより安全にする簡単な方法の1つは、自動インクリメントされないID値を使用することです。1つのオプションは、UNIXタイムスタンプの最後の6桁または7桁としてIDを使用し、チェックサムを計算することです。