【问题标题】:Why use the C# class System.Random at all instead of System.Security.Cryptography.RandomNumberGenerator?为什么要使用 C# 类 System.Random 而不是 System.Security.Cryptography.RandomNumberGenerator?
【发布时间】:2010-11-18 10:37:13
【问题描述】:

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

Nate Lawson 在 13:11 分钟的 Google Tech Talk 演讲“Crypto Strikes Back”中告诉我们不要使用 Python、Java 和 C# 的“标准”随机数生成器,而是使用加密安全版本。

我知道两个版本的随机数生成器之间的区别(请参阅question 101337)。

但是不总是使用安全随机数生成器的理由是什么?为什么要使用 System.Random 呢?也许是性能?

【问题讨论】:

  • 您更愿意输入哪个?
  • 太多人认真地用它作为他们所做事情的理由(通常不会大声说出来)。读代码比写代码多,谁在乎琐碎的长度差异?
  • 但是,如果你不做加密,为什么还要使用加密 RNG?
  • @Macha,这就是别名的用途 -> using R = System.Security.Cryptography.RandomNumberGenerator; R.Create();

标签: c# .net cryptography random


【解决方案1】:

速度和意图。如果您正在生成一个随机数并且不需要安全性,为什么要使用慢速加密功能?您不需要安全性,那么为什么要让其他人认为该号码可能用于安全的事情,而事实并非如此呢?

【讨论】:

  • 我非常喜欢intent参数。
  • 应该注意的是,Random.GetNext 远不擅长在频谱上“传播”随机数,尤其是在线程环境中。我在编写程序来测试 Rand7 和 Rand5 问题的不同解决方案时遇到了这个问题。在刚才对 0 到 10 之间的 100000 个随机数的快速线程测试中,生成的数字中有 82470 个为 0。我在之前的测试中看到了类似的差异。 Crytpography random 的数字分布非常均匀。我想教训是总是测试你的随机数据,看看它是否“足够随机”满足你的需求。
  • @Kristoffer 我认为你误用了Random。让我猜猜:您为每个数字创建了一个 Random 类的新实例,因为它是由粗略计时器播种的,因此将在大约 1-16 毫秒的时间间隔内以相同的值播种。
  • @CodesInChaos:除此之外,Random 存在竞争条件,当多个线程使用同一个对象时,它会返回全 0。
  • @KristofferL:见上述评论,另见this answer
【解决方案2】:

除了速度和更有用的界面(NextDouble() 等)之外,还可以通过使用固定的种子值来制作可重复的随机序列。这在测试期间非常有用。

Random gen1 = new Random();     // auto seeded by the clock
Random gen2 = new Random(0);    // Next(10) always yields 7,8,7,5,2,....

【讨论】:

  • 还有 BitConverter.ToInt32(Byte[] value, int startIndex) 可能更容易理解。 ;)
  • Ian Bell 和 David Braben 在电脑游戏 Elite 中使用随机生成器创建了大量的行星列表及其属性(大小等),但内存非常有限。这也依赖于生成器创建确定性模式(从种子) - Crypto 显然没有提供(通过设计)。这里有一些关于他们如何做到这一点的更多信息:wiki.alioth.net/index.php/Random_number_generator 和“无限游戏”一书Universe: Mathematical Techniques” ISBN:1584500581 对此类技术进行了更一般的讨论。
  • @phoog “因此,您的应用程序代码不应假定相同的种子会在不同版本的 .NET Framework 中产生相同的伪随机序列。” - 我不知道,对我来说似乎很清楚。但是,尽管有此警告,但如果他们无法在不破坏现有程序的情况下在实践中更改它,我不会感到惊讶。
  • @phoog:你说的是一件事,然后却恰恰相反。你直接自相矛盾。
【解决方案3】:

首先,您链接的演示文稿仅讨论了出于安全目的的随机数。所以它并没有声称Random 对非安全目的不利。

但我确实声称它是。 Random 的 .net 4 实现在几个方面存在缺陷。我建议仅在您不关心随机数的质量时才使用它。我建议使用更好的第三方实现。

缺陷 1:播种

带有当前时间的默认构造函数种子。因此,在短时间内(大约 10 毫秒)使用默认构造函数创建的所有 Random 实例都返回相同的序列。这是记录和“设计”的。如果您想对代码进行多线程处理,这尤其令人讨厌,因为您不能简单地在每个线程执行开始时创建 Random 的实例。

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

这里的另一个问题是种子空间相当小(31 位)。因此,如果您使用完全随机的种子生成 50k 个Random 实例,您可能会两次获得一个随机数序列(由于birthday paradox)。所以人工播种也不容易做好。

缺陷二:Next(int maxValue)返回的随机数分布有偏差

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

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

NextBytes() 的每字节成本大约与使用Next() 生成完整整数样本的成本一样大。由此我怀疑他们确实每个字节创建一个样本。

在每个样本中使用 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);
}

【讨论】:

  • 有趣,可以确认你的发现:在我的机器上 Next(1431655765) 也给出了 2/3 的任何种子。 1431655765有什么魔力?你是怎么得出这个数字的?
  • @citykid 将数字视为十六进制或位。它的神奇之处在于Random 用于将 31 位整数转换为具有指定上限的数字的可疑方式。我忘记了细节,但它类似于randomValue * max / 2^{31}
  • 1431655765_10 = 1010101010101010101010101010101_2
  • 嗯。那么你推荐使用 C# 的 Random 的什么实现呢?
  • 天哪,Next() 的分布的不均匀性,由您在这里展示,是一个非常壮观的错误 - 在您第一次写下您的发现 6 年后,今天仍然存在。 (我说“错误”而不仅仅是“缺陷”,因为the docs 声称“从有限的数字集中以相等的概率选择伪随机数”。事实并非如此,你的代码证明了这一点。)
【解决方案4】:

System.Random 的性能要高得多,因为它不会生成加密安全的随机数。

在我的机器上进行一个简单的测试,用随机数据填充 4 字节的缓冲区 1,000,000 次 Random 需要 49 毫秒,而 RNGCryptoServiceProvider 需要 2845 毫秒。请注意,如果您增加要填充的缓冲区的大小,则差异会缩小,因为 RNGCryptoServiceProvider 的开销不太相关。

【讨论】:

  • 感谢您通过实际测试进行演示。
  • 您可能认为这很苛刻,但是-1 用于发布性能基准测试的结果而不包括基准测试的代码。即使 RandomRNGCryptoServiceProvider 的性能特征在过去 8 年中没有改变(据我所知,它们可能已经改变了),我已经看到在 Stack Overflow 上使用了足够多的完全破坏的基准,而不是相信代码未公开的基准测试的结果。
  • 只是添加到这个旧评论线程: System.Random 并不是真正的高性能。有一些 PRNG 实现 性能优于 System.Random。例如,我的 XoshiroPRNG.Net 实现是两者;它可以在 nuget.org/packages/XoshiroPRNG.Net 上找到——点击 repo 的链接查看性能基准代码和结果。
【解决方案5】:

已经提到了最明显的原因,所以这里有一个更模糊的原因:加密 PRNG 通常需要不断地重新植入“真实”熵。因此,如果您过于频繁地使用 CPRNG,您可能会耗尽系统的熵池,这(取决于 CPRNG 的实现)会削弱它(从而允许攻击者预测它)或者它会在尝试填满时阻塞它的熵池(因此成为 DoS 攻击的攻击向量)。

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

这是一个实际存在的问题,顺便说一句,已在运行 Linux 的无头服务器(由于缺少鼠标和键盘输入等熵源而自然具有相当小的熵池)上观察到,其中应用程序错误地使用了 @987654322 @kernel CPRNG 用于各种随机数,而正确的行为是从 /dev/urandom 读取一个小的种子值并使用它来播种他们的自己的 PRNG。

【讨论】:

  • 我阅读了关于熵和熵耗竭的维基百科文章和其他一些互联网资源,但我不太了解。当随机数生成器输入系统时间、空闲字节数等时,如何耗尽熵池?其他人如何将其用作攻击向量来预测随机数?你能提供一个简单的例子吗?也许这个讨论必须离线。 en.wikipedia.org/wiki/Entropy_%28computing%29
  • 系统时间不是熵源,因为它是可预测的。我不确定空闲字节的数量,但我也怀疑它是一个高质量的熵源。通过向服务器发送更多请求,攻击者可以导致空闲字节数减少,使其具有部分确定性。您的应用程序成为攻击媒介,因为通过耗尽熵池,它会强制另一个对安全至关重要的应用程序使用随机性较小的随机数 - 或者等到熵源得到补充。
  • 我知道,如果一个人有一个伪随机生成器,例如32 位种子,蛮力攻击通常相当容易;即使是 64 位种子也可能受到生日攻击。但是,一旦种子变得比这大得多,我就看不到风险了。如果有一个随机生成器,每个输出字节通过块加密算法传递一个 128 位状态,然后输出底部 8 位,那么即使有连续输出字节的演出,攻击者如何推断状态,没有弱点加密算法本身?
【解决方案6】:

如果您正在编写在线纸牌游戏或彩票,那么您需要确保序列几乎无法猜测。但是,如果您要向用户展示当天的报价,则性能比安全更重要。

【讨论】:

    【解决方案7】:

    这个问题已经讨论了很长时间,但归根结底,性能问题是选择 RNG 时的次要考虑因素。那里有大量的 RNG,大多数系统 RNG 所包含的罐装 Lehmer LCG 并不是最好的,甚至不一定是最快的。在旧的、缓慢的系统上,这是一个很好的折衷方案。如今,这种妥协很少真正相关。这个东西一直存在到今天的系统中,主要是因为 A) 这个东西已经建成了,在这种情况下没有真正的理由去“重新发明轮子”,而且 B) 对于绝大多数人将使用它的目的,它是'足够好'。

    最终,RNG 的选择取决于风险/回报率。在某些应用中,例如视频游戏,没有任何风险。 Lehmer RNG 绰绰有余,而且小巧、简洁、快速、易于理解且“在盒子中”。

    例如,如果应用程序是在线扑克游戏或彩票,其中涉及实际奖品并且真钱在等式中的某个点发挥作用,那么“盒子里”的 Lehmer 就不再足够了。在 32 位版本中,它只有 2^32 个可能的有效状态才开始循环充其量。如今,这为暴力攻击打开了大门。在这种情况下,开发人员会希望使用某些物种的Very Long Period RNG,并可能从密码学强大的提供者那里播种。这在速度和安全性之间提供了很好的折衷。在这种情况下,人们会寻找类似 Mersenne Twister 或某种多重递归生成器 之类的东西。

    如果应用程序类似于通过网络传递大量财务信息,那么现在存在巨大的风险,并且远远超过任何可能的回报。仍然有装甲车,因为有时全副武装的人是唯一足够的安全保障,相信我,如果一个拥有坦克、战斗机和直升机的特种部队旅在经济上是可行的,那将是首选方法。在这种情况下,使用加密强的 RNG 是有意义的,因为无论您获得何种级别的安全性,它都达不到您想要的程度。所以你会尽可能多地拿走,而成本是一个非常非常遥远的次要问题,无论是时间还是金钱。如果这意味着在一台非常强大的计算机上生成每个随机序列需要 3 秒,那么您将等待 3 秒,因为在事情的计划中,这是一个微不足道的成本。

    【讨论】:

    • 我认为你的量级是错误的;发送财务数据需要非常快速;如果您的交易算法可以比竞争对手快 0.1 毫秒得出结果,那么您在买入/卖出/止损/报价命令队列中的表现会更好。 3秒是永恒。这就是为什么交易者会投资疯狂的好电脑。见上一个答案; Crypt.RNG 每个新号码只需要 0,0028 毫秒; 0.0000028 秒,因此您在处理所需的时间以及速度的重要性方面落后了 9 个数量级。
    【解决方案8】:

    请注意,C# 中的 System.Random 类编码不正确,因此应避免使用。

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

    【讨论】:

    • ...而且它看起来永远不可能修复。
    • 似乎链接已关闭,因为 Microsoft 停用了“连接”。
    【解决方案9】:

    不是每个人都需要加密安全的随机数,他们可能会从更快的纯 prng 中受益更多。也许更重要的是您可以控制 System.Random 数字的序列。

    在使用您可能想要重新创建的随机数的模拟中,您使用相同的种子重新运行模拟。当您还想重新生成给定的错误场景时,它可以很方便地跟踪错误 - 使用与程序崩溃完全相同的随机数序列运行您的程序。

    【讨论】:

      【解决方案10】:

      Random 不是随机数生成器,它是确定性伪随机序列生成器,因历史原因而得名。

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

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

      为什么需要确定性序列

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

      例如,如果您正在运行数值模拟,您可以使用(真)随机数初始化序列,并记录使用的数字

      然后,如果您希望重复 完全相同 模拟,例如出于调试目的,您可以改为使用 recorded 值初始化序列。

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

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

      简而言之,如果您想在不更改其余代码的情况下改进序列,请继续。

      【讨论】:

        【解决方案11】:

        如果我不需要安全性,即我只想要一个相对不确定的值而不是一个加密强度高的值,那么 Random 具有更易于使用的界面。

        【讨论】:

          【解决方案12】:

          不同的需求需要不同的 RNG。对于加密,您希望您的随机数尽可能随机。对于 Monte Carlo 模拟,您希望它们均匀地填充空间并能够从已知状态启动 RNG。

          【讨论】:

          • 如果只有 System.Random 也能做到……哦,好吧。
          【解决方案13】:

          我编写了一个游戏(iPhone 上的水晶滑块:Here),它会在地图上放置“随机”系列的宝石(图像),您可以按照自己的意愿旋转地图并选择它们离开了。 - 类似于宝石迷阵。我使用的是 Random(),它以自手机启动后 100ns 滴答数为种子,是一个非常随机的种子。

          我发现它令人惊奇它会生成几乎相同的游戏 - 在 90 种左右的宝石中,有 2 种颜色,我会得到两个完全相同的游戏,除了 1 到 3宝石!如果你掷 90 枚硬币,除了 1-3 次以外,得到相同的图案,那是极不可能的!我有几个屏幕截图显示它们相同。我对 System.Random() 的糟糕程度感到震惊!我假设,我一定在我的代码中写了一些可怕的错误并且使用错误。不过我错了,是生成器。

          作为一个实验——也是最终的解决方案,我回到了我从 1985 年左右开始使用的随机数生成器——这要好得多。它更快,在重复之前有 1.3 * 10^154 (2^521) 的周期。最初的算法是用 16 位数字播种的,但我将其更改为 32 位数字,并改进了初始播种。

          原文在这里:

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

          多年来,我已经完成了所有我能想到的随机数测试,而且都超过了所有这些。我不希望它作为加密货币具有任何价值,但它返回的数字与“return *p++;”一样快直到它用完 521 位,然后它在这些位上运行一个快速过程以创建新的随机位。

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

          差异要好得多 - 天哪,我很惊讶 - 我应该无法从 90 颗左右的宝石屏幕上分辨出来,但在此之后我紧急发布了我的游戏。

          而且我再也不会使用 System.Random() 来做任何事情了。我很震惊他们的版本被现在 30 岁的东西所震撼!

          -Traderhut 游戏

          【讨论】:

          • 我的第一个猜测是你重新创建Random 太频繁了。它应该只在该实例上多次调用Next 时创建。 Random 很糟糕,但还不错。您能否发布一个示例程序以及一对出现此问题的种子?
          • 代码会在每个关卡的开头创建一个Random()(但这是一个大问题,关卡1比后面的多)代码大致如下:
          • Rnd = new Random ((uint)GameSeed); NextGameSeed = Rnd.Next (2000000000);每个关卡都使用了一个新的随机种子,该随机种子是用一个新种子创建的——种子是为每个关卡保存的,因此我可以重新创建地图,并确认匹配的随机种子序列。这让我可以确认游戏是一系列有效的已解决地图并重新创建游戏。
          • 最初,Random 是基于 System.DateTime.Now.Ticks(或 0)创建的,然后使用与上述 Rnd.Next() 相同的调用选择 GameSeed。如果我不能这样做,那么随机数生成器的播种存在严重问题。
          • 这不是原始问题的答案!
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-01-04
          • 2013-12-07
          • 2015-05-03
          • 2011-10-09
          • 2013-09-13
          • 2010-11-18
          相关资源
          最近更新 更多