为什么使用C#类System.Random而不是System.Security.Cryptography.RandomNumberGenerator?

为什么会有人使用System.Random中的“标准”随机数生成器,而不是始终使用System.Security.Cryptography.RandomNumberGenerator (或其子类,因为RandomNumberGenerator是抽象的)中的密码安全的随机数生成器?

Nate Lawson在分钟13:11的Google Tech Talk演讲中表示“ encryption反击 ”不会使用来自Python,Java和C#的“标准”随机数生成器,而是使用encryption安全版本。

我知道随机数发生器的两个版本之间的区别(见问题101337 )。

但是,有什么理由不总是使用安全的随机数发生器? 为什么要使用System.Random? 性能可能?

速度和意图。 如果你正在生成一个随机数并且不需要安全性,为什么要使用一个缓慢的encryption函数呢? 你不需要安全,那么为什么让其他人认为这个数字可能被用于某些安全的事情呢?

除了速度和更有用的接口( NextDouble()等)之外,还可以通过使用固定的种子值来制作可重复的随机序列。 这在testing过程中非常有用。

 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实现在几个方面存在缺陷。 我build议只使用它,如果你不关心你的随机数字的质量。 我build议使用更好的第三方实现。

缺陷1:播种

默认构造函数种子与当前时间。 因此,在短时间内(大约10ms)使用默认构造函数创build的Random所有实例都返回相同的序列。 这是logging和“按devise”。 如果你想multithreading你的代码,这是特别讨厌的,因为你不能在每个线程的执行开始时简单地创build一个Random的实例。

解决方法是在使用默认构造函数时要格外小心,并在必要时手动进行种子处理。

另一个问题是种子空间很小(31位)。 所以如果你用随机的随机种子产生50k的Random ,你可能会得到一个随机数序列两次(由于生日悖论 )。 所以手工播种也不容易。

缺陷2:由Next(int maxValue)返回的随机数分布是有偏见的

Next(int maxValue)显然Next(int maxValue)参数。 例如,如果你计算r.Next(1431655765) % 2你将在大约2/3的样本中得到0 。 (答案结尾的示例代码)

缺陷3: NextBytes()方法效率低下。

NextBytes()的每字节成本与使用Next()生成完整整数示例的成本NextBytes() 。 从这个我怀疑他们确实每个字节创build一个样本。

使用每个样本中3个字节的更好的实现方式会使NextBytes()速度提高几乎3倍。

由于这个缺陷Random.NextBytes()只比我的机器上的System.Security.Cryptography.RNGCryptoServiceProvider.GetBytes (Win7,Core i3 2600MHz)快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个字节的缓冲区的简单testing为1,000,000次,其中Random为49 ms,而RNGCryptoServiceProvider为2845 ms。 请注意,如果您增加了正在填充的缓冲区的大小,则差异会缩小,因为RNGCryptoServiceProvider的开销不太相关。

最明显的原因已经被提及,所以这里更加模糊:密码PRNG通常需要不断地被“真实”的熵重新编码。 因此,如果您经常使用CPRNG,则可能会耗尽系统的熵池(这取决于CPRNG的实施)会削弱系统的熵值(从而允许攻击者预测它),或者在尝试填充时阻塞其熵池(从而成为DoS攻击的攻击载体)。

无论哪种方式,您的应用程序现在已经成为其他完全不相关的应用程序的攻击媒介 – 与您不同 – 实际上非常依赖于CPRNG的encryption属性。

这是一个实际的真实世界问题,在无头的服务器(自然而然具有相当小的熵池,因为它们没有像鼠标和键盘input这样的熵源)运行的Linux上被观察到,其中应用程序错误地使用了/dev/random kernel CPRNG用于各种随机数,而正确的行为是从/dev/urandom读取一个小的种子值,并使用它来种子他们自己的 PRNG。

如果你正在编程一个在线纸牌游戏或lotter,那么你会想确保序列几乎不可能猜到。 但是,如果您正在向用户显示,则表示当天的报价比性能更重要。

这已经被讨论了一段时间,但最终,性能问题是selectRNG时的次要考虑因素。 那里有大量的RNGs,大多数系统RNGs组成的Lehmer LCGjar头并不是最好的,也不一定是最快的。 在旧的,缓慢的系统,这是一个很好的妥协。 这种妥协现在很less有真正的相关性。 这件事情一直持续到现在的系统,主要是因为A)事情已经build立,在这种情况下没有真正的理由“重新发明轮子”,B)为什么广大的人将使用它,它是'够好了'。

最终,select一个RNG归结为风险/回报比率。 在某些应用中,例如video游戏,没有任何风险。 Lehmer RNG绰绰有余,体积小,简洁,快速,易于理解,并且“在盒子里”。

例如,如果应用程序是一个在线扑克游戏或抽奖,其中涉及到实际的奖金,并且真正的金钱在等式中的某个点发挥作用,那么“在框中”的Lehmer已经不够了。 在32位版本中,它最多只能有2 ^ 32个可能的有效状态。 这些日子,这是暴力攻击的开放之门。 在这样的情况下,开发者会想要去某些物种的非常长的 RNG,并且可能从一个密码强的提供者那里得到它。 这在速度和安全性之间提供了一个很好的折衷。 在这种情况下,这个人将会寻找类似于Mersenne Twister的东西,或者某种types的多重recursion生成器

如果应用程序是通过networking传递大量财务信息的话,那么现在存在着巨大的风险,而且这个风险大大超过了任何可能的回报。 仍然有装甲车,因为有时候全副武装的人是唯一足够的安全,相信我,如果一个有坦克,战斗机和直升机的特种作战人员在财务上是可行的,那么这将是select的方法。 在这样的情况下,使用密码强的RNG是有道理的,因为无论您的安全级别如何,它都不是您想要的。 所以你会尽可能多的find你所需要的,而且这个成本是一个非常非常遥远的第二位的问题,不论是在时间和金钱上。 如果这意味着每一个随机序列在一台非常强大的计算机上需要3秒钟的时间才能生成,那么就需要等待3秒钟,因为在这个计划中,这是一个微不足道的成本。

请注意,在C#中的System.Random类编码不正确,所以应该避免。

https://connect.microsoft.com/VisualStudio/feedback/details/634761/system-random-serious-bug#tabs

不是每个人都需要密码安全的随机数字,他们可能会从更快速的普通版本中受益更多。 也许更重要的是你可以控制System.Random号码的顺序。

在使用随机数的模拟中,您可能需要重新创build,然后使用相同的种子重新运行模拟。 当你想重新生成一个给定的错误场景时,它可以方便地跟踪错误 – 使用与程序崩溃相同的随机数序列来运行程序。

如果我不需要安全性,也就是说,我只想要一个相对不确定的值,而不是一个密码强的值,那么Random具有更简单的接口。

不同的需求需要不同的RNG。 对于encryption,你希望你的随机数字尽可能的随机。 对于蒙特卡洛模拟,您希望它们均匀地填充空间,并能够从已知状态启动RNG。

Random不是一个随机数发生器,它是一个确定性的伪随机序列发生器,由于历史的原因,它的名称。

使用System.Random的原因是,如果你想要这些属性,即一个确定性的序列,当使用相同的种子进行初始化时,保证产生相同的结果序列。

如果要在不牺牲接口的情况下提高“随机性”,则可以从System.Randominheritance,覆盖几个方法。

你为什么要一个确定性的序列

具有确定性序列而不是真随机性的一个原因是因为它是可重复的。

例如,如果您正在运行数值模拟,则可以用(真实)随机数初始化序列,并logging使用的是什么数字

然后,如果您希望重复完全相同的模拟,例如为了debugging目的,您可以通过用logging值初始化序列来实现。

你为什么要这个特殊的,不是很好的序列

我能想到的唯一原因是为了向后兼容现有的使用这个类的代码。

简而言之,如果你想在不改变代码的其他部分的情况下改进序列,请继续。

我写了一个游戏(在iPhone上的Crystal Sliders: 在这里 ),它会在地图上放置一个“随机”的一系列gem(图像),你将旋转你想要的地图并select它们,然后它们就消失了。 – 类似于gem迷阵 我使用的是Random(),并且自从手机启动以来,它的种子数量为100ns,这是一个非常随机的种子。

我发现它会产生几乎相同的游戏 – 两种颜色的90个左右的gem,除了1到3个gem外,我会得到两个完全相同的游戏! 如果你翻转90个硬币,并获得相同的模式,除了1-3翻转,这是非常不可能的! 我有几个屏幕截图,显示他们一样。 我很震惊System.Random()是多么糟糕! 我认为,我必须在代码中写下一些可怕的错误,并错误地使用它。 我错了,但它是发电机。

作为一个实验 – 也是一个最终的解决scheme,我回到了自1985年以来一直使用的随机数生成器 – 这是最好的。 它更快,在重复之前有一个1.3 * 10 ^ 154(2 ^ 521)的周期。 原来的algorithm播种了一个16位的数字,但我改变了一个32位的数字,并改善了初始播种。

原来的是这里:

ftp://ftp.grnet.gr/pub/lang/algorithms/c/jpl-c/random.c

多年来,我已经抛出了每一个我能想到的随机数字testing,并通过了所有这些testing。 我不认为它作为一个encryption函数是有价值的,但它返回的数字与“return * p ++”一样快。 直到它用完了521位,然后它运行一个快速的过程来创build新的随机数。

我创build了一个C#包装 – 称为JPLRandom()实现了与Random()相同的接口,并更改了代码中调用它的所有位置。

这个差距是最好的 – OMG我感到惊讶 – 我不应该只从一个模式看90个左右的gem的屏幕,但是我在这之后紧急释放了我的游戏。

而且我永远不会再使用System.Random()。 我很震惊,他们的版本被现在30岁的东西吹走了!

贸易游戏