なぜ System.Security.Cryptography.RandomNumberGenerator (からの暗号的に安全な乱数ジェネレーターを常に使用するのではなく、 System.Random からの「標準」乱数ジェネレーターを使用するのか。 RandomNumberGeneratorが抽象的であるため、そのサブクラスですか?
Nate LawsonがGoogle Tech Talkプレゼンテーションで「 Crypto Strikes Back 」の13分11分に、Pythonの「標準」乱数ジェネレーターを使用しない、Java C#を使用し、代わりに暗号的に安全なバージョンを使用します。
乱数ジェネレーターの2つのバージョンの違いを知っています( question 101337 を参照)。
しかし、常に安全な乱数ジェネレーターを使用するとは限らない理由は何ですか?なぜSystem.Randomを使用するのですか?おそらくパフォーマンス?
スピードと意図。乱数を生成していて、セキュリティの必要がない場合、なぜ低速暗号化機能を使用するのですか?あなたはセキュリティを必要としないので、なぜ他の人にその番号が安全でない何かのために使われるかもしれないと思わせるのですか?
速度とより便利なインターフェイス(NextDouble()
など)以外に、固定のシード値を使用して繰り返し可能なランダムシーケンスを作成することもできます。これは、特にテスト中に非常に役立ちます。
Random gen1 = new Random(); // auto seeded by the clock
Random gen2 = new Random(0); // Next(10) always yields 7,8,7,5,2,....
まず、リンクしたプレゼンテーションは、セキュリティ上の目的で乱数についてのみ話します。したがって、Random
がセキュリティ以外の目的で悪いと主張することはありません。
しかし、私はそうだと主張しています。 Random
の.net 4実装には、いくつかの点で欠陥があります。乱数の品質を気にしない場合にのみ使用することをお勧めします。より良いサードパーティの実装を使用することをお勧めします。
欠陥1:シード
デフォルトのコンストラクターは現在の時刻をシードします。したがって、短い時間枠(約10ミリ秒)以内にデフォルトコンストラクターで作成されたRandom
のすべてのインスタンスは、同じシーケンスを返します。これは文書化されており、「設計」です。これは、各スレッドの実行の開始時にRandom
のインスタンスを単純に作成することはできないため、コードをマルチスレッド化する場合、特に面倒です。
回避策は、デフォルトのコンストラクタを使用するときは特に注意し、必要に応じて手動でシードすることです。
ここでの別の問題は、シードスペースがかなり小さい(31ビット)ことです。したがって、完全にランダムなシードを使用してRandom
の50kインスタンスを生成すると、おそらく乱数の1つのシーケンスを2回取得します( birthday paradox のため)。そのため、手動シードを正しく行うことも簡単ではありません。
欠陥2:Next(int maxValue)
によって返される乱数の分布は偏っている
Next(int maxValue)
が明らかに均一ではないパラメーターがあります。たとえば、r.Next(1431655765) % 2
を計算すると、サンプルの約2/3で_0
_が得られます。 (回答の最後のサンプルコード。)
欠陥3:NextBytes()
メソッドは非効率的です。
NextBytes()
のバイトあたりのコストは、Next()
で完全な整数サンプルを生成するコストとほぼ同じです。これから、私は彼らが実際にバイトごとに1つのサンプルを作成するのではないかと疑っています。
各サンプルから3バイトを使用したより良い実装では、NextBytes()
をほぼ3倍高速化します。
この欠陥のおかげで、Random.NextBytes()
は私のマシン(Win7、Core i3 2600MHz)では_System.Security.Cryptography.RNGCryptoServiceProvider.GetBytes
_よりも約25%だけ高速です。
誰かがソース/逆コンパイルされたバイトコードを検査した場合、ブラックボックス分析で見つけたよりもさらに多くの欠陥を見つけると確信しています。
コードサンプル
r.Next(0x55555555) % 2
は強くバイアスされています:
_Random r = new Random();
const int mod = 2;
int[] hist = new int[mod];
for(int i = 0; i < 10000000; i++)
{
int num = r.Next(0x55555555);
int num2 = num % 2;
hist[num2]++;
}
for(int i=0;i<mod;i++)
Console.WriteLine(hist[i]);
_
性能:
_byte[] bytes=new byte[8*1024];
var cr=new System.Security.Cryptography.RNGCryptoServiceProvider();
Random r=new Random();
// Random.NextBytes
for(int i=0;i<100000;i++)
{
r.NextBytes(bytes);
}
//One sample per byte
for(int i=0;i<100000;i++)
{
for(int j=0;j<bytes.Length;j++)
bytes[j]=(byte)r.Next();
}
//One sample per 3 bytes
for(int i=0;i<100000;i++)
{
for(int j=0;j+2<bytes.Length;j+=3)
{
int num=r.Next();
bytes[j+2]=(byte)(num>>16);
bytes[j+1]=(byte)(num>>8);
bytes[j]=(byte)num;
}
//Yes I know I'm not handling the last few bytes, but that won't have a noticeable impact on performance
}
//Crypto
for(int i=0;i<100000;i++)
{
cr.GetBytes(bytes);
}
_
System.Randomは、暗号的に安全な乱数を生成しないため、はるかに高性能です。
ランダムデータで4バイトのバッファを1,000,000回満たす私のマシンでの簡単なテストでは、Randomでは49ミリ秒かかりますが、RNGCryptoServiceProviderでは2845ミリ秒かかります。充填するバッファのサイズを大きくすると、RNGCryptoServiceProviderのオーバーヘッドの関連性が低くなるため、差が狭くなることに注意してください。
最も明白な理由はすでに述べたので、ここではもっとわかりにくいものです。暗号化PRNGは通常、「実際の」エントロピーで継続的に再シードする必要があります。したがって、CPRNGを頻繁に使用すると、システムのエントロピープールを使い果たす可能性があります(CPRNGの実装によって異なります)。そのエントロピープール(したがって、DoS攻撃の攻撃ベクトルになります)。
いずれにせよ、あなたのアプリケーションは、実際には極めて重要なdependであるCPRNGの暗号特性に依存する、まったく無関係な他のアプリケーションの攻撃ベクトルになりました。
これは実際の現実の問題です。BTWは、アプリケーションが/dev/random
を誤って使用するLinuxを実行しているヘッドレスサーバー(マウスやキーボード入力などのエントロピーソースがないため、かなり小さなエントロピープールを持つ)で観察されていますあらゆる種類の乱数に対するカーネルCPRNG。正しい動作は、/dev/urandom
から小さなシード値を読み取り、それを使用してown PRNGをシードすることです。
オンラインカードゲームまたは宝くじをプログラミングしている場合は、シーケンスが推測不可能に近いことを確認する必要があります。ただし、たとえばユーザーにその日の引用を表示する場合、パフォーマンスよりもセキュリティの方が重要です。
C#のSystem.Randomクラスは誤ってコーディングされているため、使用しないでください。
https://connect.Microsoft.com/VisualStudio/feedback/details/634761/system-random-serious-bug#tabs
これについては長い間議論されてきましたが、最終的には、パフォーマンスの問題はRNGを選択する際の2番目の考慮事項です。そこには膨大な数のRNGがあり、ほとんどのシステムRNGを構成する缶詰のレーマーLCGは最高ではなく、必ずしも最速でもありません。古い低速のシステムでは、これは優れた妥協策でした。このような妥協は、最近ではほとんど関係ありません。主にA)ものがすでに構築されており、この場合「車輪を再発明」する本当の理由がないため、そしてB)膨大な量の人々がそれを使用するために、 「十分」。
最終的に、RNGの選択は、リスク/報酬比になります。ビデオゲームなどの一部のアプリケーションでは、リスクは一切ありません。レーマーRNGは十分であり、小さく、簡潔で、高速で、十分に理解されており、「箱の中」にあります。
たとえば、アプリケーションがオンラインポーカーゲームまたは宝くじであり、実際の賞金が関係し、方程式のある時点で実際のお金が出てくる場合、「箱の中」のレーマーはもはや適切ではありません。 32ビットバージョンでは、循環を開始する前に有効な状態は2 ^ 32しかありませんせいぜい。最近、それはブルートフォース攻撃への開かれた扉です。このような場合、開発者は非常に長い期間いくつかの種のRNGのようなものに行き、おそらく暗号的に強力なプロバイダーからシードしたいと思うでしょう。これにより、速度とセキュリティのバランスが取れます。そのような場合、人はMersenne Twister、またはMultiple Recursive Generatorのようなものを探しています。
アプリケーションがネットワークを介して大量の財務情報をやり取りするようなものである場合、現在では大きなリスクがあり、可能な報酬を大きく上回っています。装甲車はまだあります。時には重武装の男性が適切な唯一のセキュリティであるため、私を信頼してください。このような場合、暗号強度の高いRNGを使用するのは理にかなっています。セキュリティのレベルに関係なく、必要なレベルではないからです。だから、あなたはあなたが見つけることができる限り多くを取るでしょう、そして、コストは時間とお金のどちらかで、非常に非常に遠隔の2位の問題です。そして、すべてのランダムシーケンスが非常に強力なコンピューターで生成するのに3秒かかることを意味する場合、3秒待つことになります。
すべての人が暗号で保護された乱数を必要とするわけではなく、より高速で単純なprngの恩恵を受ける可能性があります。おそらくもっと重要なのは、System.Random番号のシーケンスを制御できることです。
再作成したい乱数を使用したシミュレーションでは、同じシードを使用してシミュレーションを再実行します。特定の障害のあるシナリオも再生成したい場合、バグを追跡するのに便利です。プログラムをクラッシュさせたのとまったく同じ乱数シーケンスでプログラムを実行します。
セキュリティが必要ない場合、つまり暗号的に強力な値ではなく、比較的不確定な値が必要な場合、Randomにははるかに使いやすいインターフェイスがあります。
異なるニーズは異なるRNGを必要とします。暗号化では、乱数をできるだけランダムにする必要があります。モンテカルロシミュレーションでは、空間を均等に埋めて、既知の状態からRNGを開始できるようにする必要があります。
Random
は乱数ジェネレーターではなく、決定論的な擬似乱数シーケンスジェネレーターであり、歴史的な理由から名前が付けられています。
System.Random
を使用する理由は、これらのプロパティ、つまり決定論的シーケンスが必要な場合です。これは、同じシードで初期化されたときに同じ結果シーケンスが生成されることが保証されています。
インターフェイスを犠牲にすることなく「ランダム性」を改善したい場合は、いくつかのメソッドをオーバーライドしてSystem.Random
から継承できます。
決定論的なシーケンスが必要な理由
真のランダム性ではなく決定論的なシーケンスを使用する理由の1つは、反復可能であるためです。
たとえば、数値シミュレーションを実行している場合、シーケンスを(真の)乱数で初期化し、使用された番号を記録できます。
次に、exact sameシミュレーションを繰り返したい場合、例えばデバッグの目的で、代わりにrecorded値でシーケンスを初期化することでこれを行うことができます。
なぜこの特定の、あまり良くないシーケンスが必要なのでしょうか
私が考えることができる唯一の理由は、このクラスを使用する既存のコードとの後方互換性のためです。
つまり、コードの残りを変更せずにシーケンスを改善したい場合は、先に進みます。