如何简洁,便携,彻底地播种mt19937 PRNG?

Ric*_*ard 106 c++ random c++11

我似乎看到很多答案,有人建议使用它<random>来生成随机数,通常伴随着这样的代码:

std::random_device rd;  
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(0, 5);
dis(gen);
Run Code Online (Sandbox Code Playgroud)

通常这会取代某种"邪恶的憎恶",例如:

srand(time(NULL));
rand()%6;
Run Code Online (Sandbox Code Playgroud)

我们可能会批评旧的方式,认为time(NULL)提供低熵,time(NULL)可预测,最终结果是不均匀的.

但所有这一切都适用于新的方式:它只有一个更光亮的贴面.

  • rd()返回一个unsigned int.这至少有16位,可能是32位.这还不足以为MT的19937位状态提供种子.

  • 使用std::mt19937 gen(rd());gen()(以32位播种并查看第一个输出)不能提供良好的输出分布.7和13永远不会是第一个输出.两粒种子产生0.十二粒种子产生1226181350.(链接)

  • std::random_device可以(有时是)实现为具有固定种子的简单PRNG.因此,它可能在每次运行时产生相同的序列.(链接)这甚至比time(NULL).

更糟糕的是,尽管存在它们包含的问题,但复制和粘贴上述代码片段非常容易.对此的一些解决方案需要获得可能不适合每个人的大型 .

鉴于此,我的问题是如何在C++中简洁,便携,彻底地播种mt19937 PRNG?

鉴于上述问题,一个很好的答案:

  • 必须完全播种mt19937/mt19937_64.
  • 不能单独依赖std::random_devicetime(NULL)作为熵的来源.
  • 不应该依赖Boost或其他图书馆.
  • 应该适合少量的线条,这样看起来很好,可以复制粘贴到答案中.

思考

  • 我目前的想法是,输出来自std::random_device(可能通过XOR)time(NULL),从地址空间随机化得到的值,以及硬编码常量(可以在分配期间设置)以获得熵的最佳努力.

  • std::random_device::entropy() 没有很好地说明std::random_device可能做什么或不做什么.

Ale*_*agh 56

我认为最大的缺陷std::random_device是,如果没有可用的CSPRNG,它将被允许确定性回退.仅这一点是不使用PRNG种子的一个很好的理由std::random_device,因为产生的字节可能是确定性的.遗憾的是,它不提供API来查明何时发生这种情况,或者请求失败而不是低质量的随机数.

也就是说,没有完全可移植的解决方案:但是,有一种体面的,最小的方法.您可以使用CSPRNG(定义sysrandom如下)的最小包装来为PRNG播种.

视窗


您可以信赖CryptGenRandomCSPRNG.例如,您可以使用以下代码:

bool acquire_context(HCRYPTPROV *ctx)
{
    if (!CryptAcquireContext(ctx, nullptr, nullptr, PROV_RSA_FULL, 0)) {
        return CryptAcquireContext(ctx, nullptr, nullptr, PROV_RSA_FULL, CRYPT_NEWKEYSET);
    }
    return true;
}


size_t sysrandom(void* dst, size_t dstlen)
{
    HCRYPTPROV ctx;
    if (!acquire_context(&ctx)) {
        throw std::runtime_error("Unable to initialize Win32 crypt library.");
    }

    BYTE* buffer = reinterpret_cast<BYTE*>(dst);
    if(!CryptGenRandom(ctx, dstlen, buffer)) {
        throw std::runtime_error("Unable to generate random bytes.");
    }

    if (!CryptReleaseContext(ctx, 0)) {
        throw std::runtime_error("Unable to release Win32 crypt library.");
    }

    return dstlen;
}
Run Code Online (Sandbox Code Playgroud)

类Unix


在许多类Unix系统上,应尽可能使用/ dev/urandom(尽管不能保证在POSIX兼容系统上存在).

size_t sysrandom(void* dst, size_t dstlen)
{
    char* buffer = reinterpret_cast<char*>(dst);
    std::ifstream stream("/dev/urandom", std::ios_base::binary | std::ios_base::in);
    stream.read(buffer, dstlen);

    return dstlen;
}
Run Code Online (Sandbox Code Playgroud)

其他


如果没有可用的CSPRNG,您可以选择依赖std::random_device.但是,如果可能的话,我会避免这种情况,因为各种编译器(最值得注意的是,MinGW)将它作为PRNG实现(实际上,每次产生相同的序列以提醒人们它不是随机的).

播种


现在我们有了最小的开销,我们可以生成所需的随机熵位来为我们的PRNG播种.该示例使用(显然不足)32位来为PRNG播种,您应该增加此值(这取决于您的CSPRNG).

std::uint_least32_t seed;    
sysrandom(&seed, sizeof(seed));
std::mt19937 gen(seed);
Run Code Online (Sandbox Code Playgroud)

提升比较


在快速查看源代码之后,我们可以看到boost :: random_device(一个真正的CSPRNG)的相似之处.Boost MS_DEF_PROV在Windows上使用,它是提供者类型PROV_RSA_FULL.唯一缺少的就是验证加密上下文,这可以完成CRYPT_VERIFYCONTEXT.在*Nix上,Boost使用/dev/urandom.IE,这个解决方案是便携式的,经过良好测试,易于使用.

Linux专业化


如果您愿意牺牲简洁性来保证安全性,getrandom那么在Linux 3.17及更高版本以及最近的Solaris上是一个很好的选择.getrandom行为完全相同/dev/urandom,除非它在启动后内核尚未初始化其CSPRNG时阻塞.以下代码段检测Linux getrandom是否可用,如果没有,则回退到/dev/urandom.

#if defined(__linux__) || defined(linux) || defined(__linux)
#   // Check the kernel version. `getrandom` is only Linux 3.17 and above.
#   include <linux/version.h>
#   if LINUX_VERSION_CODE >= KERNEL_VERSION(3,17,0)
#       define HAVE_GETRANDOM
#   endif
#endif

// also requires glibc 2.25 for the libc wrapper
#if defined(HAVE_GETRANDOM)
#   include <sys/syscall.h>
#   include <linux/random.h>

size_t sysrandom(void* dst, size_t dstlen)
{
    int bytes = syscall(SYS_getrandom, dst, dstlen, 0);
    if (bytes != dstlen) {
        throw std::runtime_error("Unable to read N bytes from CSPRNG.");
    }

    return dstlen;
}

#elif defined(_WIN32)

// Windows sysrandom here.

#else

// POSIX sysrandom here.

#endif
Run Code Online (Sandbox Code Playgroud)

OpenBSD系统


最后一个警告:现代OpenBSD没有/dev/urandom.你应该使用getentropy.

#if defined(__OpenBSD__)
#   define HAVE_GETENTROPY
#endif

#if defined(HAVE_GETENTROPY)
#   include <unistd.h>

size_t sysrandom(void* dst, size_t dstlen)
{
    int bytes = getentropy(dst, dstlen);
    if (bytes != dstlen) {
        throw std::runtime_error("Unable to read N bytes from CSPRNG.");
    }

    return dstlen;
}

#endif
Run Code Online (Sandbox Code Playgroud)

其他想法


如果您需要加密安全的随机字节,您应该用POSIX的无缓冲打开/读取/关闭替换fstream.这是因为两者basic_filebufFILE包含一个内部缓冲器,这将通过标准的分配器被分配(因此不从存储器擦拭).

这可以通过更改sysrandom为:

size_t sysrandom(void* dst, size_t dstlen)
{
    int fd = open("/dev/urandom", O_RDONLY);
    if (fd == -1) {
        throw std::runtime_error("Unable to open /dev/urandom.");
    }
    if (read(fd, dst, dstlen) != dstlen) {
        close(fd);
        throw std::runtime_error("Unable to read N bytes from CSPRNG.");
    }

    close(fd);
    return dstlen;
}
Run Code Online (Sandbox Code Playgroud)

谢谢


特别感谢Ben Voigt指出FILE使用缓冲读取,因此不应使用.

我还要感谢Peter Cordes的提及getrandom,以及OpenBSD的缺乏/dev/urandom.

  • 这就是我过去所做的,但是,或者至少是一个问题,WTF不能让这些平台的图书馆作者为我们这样做吗?我希望文件访问和线程(例如)可以通过库实现进行抽象,那么为什么不进行随机数生成呢? (10认同)
  • 我认为`/ dev/random`是播种RNG的更好选择,但显然[`/ dev/urandom`仍被视为计算安全](https://www.2uo.de/myths-about-urandom/ )即使`/ dev/random`会因为低可用熵而阻塞,所以`urandom`是除了一次性填充之外的所有东西的推荐选择.另见https://unix.stackexchange.com/questions/324209/when-to-use-dev-random-vs-dev-urandom.但是,在启动后很早就要注意来自"urandom"的可预测种子. (3认同)
  • OP此处:如果此答案证明播种好一点,那就太好了。我希望尽可能多的答案能够生成可复制复制的代码,而不是我在问题中发布的简单示例,而不需要编码器方面的太多技术解释,而是可以做的更好。 (2认同)
  • Linux的[`getrandom(2)`](http://man7.org/linux/man-pages/man2/getrandom.2.html)系统调用就像打开和读取`/ dev/urandom`,除了它会阻塞如果内核的随机源尚未初始化.我认为这样可以避免早期启动的低质量随机性问题,而不会像`/ dev/random`那样在其他情况下阻塞. (2认同)
  • 另外,对于任何 x86 目标,您可以检查 `CPUID` 并运行 [`RDSEED` (`int _rdseed64_step( unsigned __int64 *)`](http://felixcloutier.com/x86/RDSEED.html) 如果 CPUID 表示可用. ([`RDRAND`](http://felixcloutier.com/x86/RDRAND.html) 显然没有那么高的随机性质量保证,因此存在符合 NIST SP800-90B 的 `RDSEED`和 NIST SP800-90C。)但请注意,您仍然需要后备,因为 API 允许它报告错误(例如,如果它在给定的 CPU 中发生物理损坏)。我不知道真正的硬件是否曾经这样做过。 (2认同)

ein*_*ica 22

从某种意义上说,这不可能是便携式的.也就是说,人们可以设想一个运行C++的有效的完全确定性平台(例如,一个模拟器,它确定性地处理机器时钟,并且具有"确定的"I/O),其中没有随机性来源PRNG.

  • 我很欣赏这种反应,但也感觉好像一个程序应该做出合理的尽力尝试. (7认同)
  • @Richard同意,但问题是C++标准编写者必须(或至少尝试他们最好的)适应这些奇怪的情况.这就是为什么你得到这些类似的标准定义,你可能得到不错的结果,但编译器仍然可以符合标准,即使它回馈了功能上毫无价值的东西. - 因此,您的限制("简短且不能依赖其他库")排除任何响应,因为您实际上需要逐个平台/逐个编译器的特殊套管.(例如Boost做得很好.) (3认同)
  • @Richard它解释的是,你得到了你在标准*中得到的东西,因为*没有可行的方法可以做得更好.如果你想做得更好(这是一个崇高的目标),你将不得不接受一些或多或少的憎恶:) (2认同)

rat*_*eak 12

您可以使用a std::seed_seq并使用Alexander Huszagh的获取熵的方法将其填充到生成器的至少所需状态大小:

size_t sysrandom(void* dst, size_t dstlen); //from Alexander Huszagh answer above

void foo(){

    std::uint_fast32_t[std::mt19937::state_size] state;
    sysrandom(state, sizeof(state));
    std::seed_seq s(std::begin(state), std::end(state));

    std::mt19937 g;
    g.seed(s);
}
Run Code Online (Sandbox Code Playgroud)

如果有填充或创建一个适当的方式SeedSequenceUniformRandomBitGenerator在使用标准库std::random_device播种正确就会简单得多.


Gal*_*lik 5

我正在处理的实现利用PRNG的state_size属性mt19937来决定在初始化时提供多少种子:

using Generator = std::mt19937;

inline
auto const& random_data()
{
    thread_local static std::array<typename Generator::result_type, Generator::state_size> data;
    thread_local static std::random_device rd;

    std::generate(std::begin(data), std::end(data), std::ref(rd));

    return data;
}

inline
Generator& random_generator()
{
    auto const& data = random_data();

    thread_local static std::seed_seq seeds(std::begin(data), std::end(data));
    thread_local static Generator gen{seeds};

    return gen;
}

template<typename Number>
Number random_number(Number from, Number to)
{
    using Distribution = typename std::conditional
    <
        std::is_integral<Number>::value,
        std::uniform_int_distribution<Number>,
        std::uniform_real_distribution<Number>
    >::type;

    thread_local static Distribution dist;

    return dist(random_generator(), typename Distribution::param_type{from, to});
}
Run Code Online (Sandbox Code Playgroud)

我认为还有改进的余地,因为std::random_device::result_type可能std::mt19937::result_type在大小和范围上有所不同,所以应该真正考虑到这一点。

关于std::random_device 的说明

根据C++11(/14/17)标准:

26.5.6类 random_device [ rand.device ]

2如果实现限制阻止生成非确定性随机数,则实现可以使用随机数引擎。

这意味着如果通过某些限制阻止生成确定性值,则实现可能仅生成确定性值。

众所周知,MinGW编译器Windows不提供来自其的非确定性std::random_device,尽管它们很容易从操作系统中获得。所以我认为这是一个错误,不太可能跨实现和平台普遍发生。

  • @AlexanderHuszagh 我不太确定。我的目的是让我的“便携式解决方案”依赖于*设备*,因为如果设备支持非确定性生成器,那么`std::random_device` 也应该如此。我相信这就是标准的精神。所以我搜索了,只能找到在这方面被破坏的`MinGW`。似乎没有人用我发现的任何其他东西报告这个问题。因此,在我的库中,我只是将 `MinGW` 标记为不受支持。如果有更广泛的问题,那么我会重新考虑。我只是现在没有看到这方面的证据。 (5认同)
  • 我真的很失望 MinGW 正在以一种不提供平台随机性功能的形式提供它,从而破坏了所有人的 `std::random_device`。低质量的实现违背了现有 API 的目的。如果他们在让它工作之前根本不实施它,那么 IMO 会更好。(或者更好的是,如果 API 提供了一种在高质量随机性不可用的情况下请求失败的方法,那么 MinGW 可以避免造成安全风险,同时仍然为游戏或其他任何东西提供不同的种子。) (5认同)
  • @Richard 是否有任何真正的系统实际上没有实现合理的 `std::random_device`?我知道标准允许使用 `PRNG` 作为回退,但我觉得这只是为了掩盖自己,因为很难要求每个使用 `C++` 的设备都具有非确定性随机源。如果他们不这样做,那么你能做些什么呢? (2认同)
  • 事实上,MinGW 故意选择了相同的起始序列,以使“std::random_device”是一个 PRNG 而不是 CSPRNG 的事实变得显而易见。/sf/ask/1321645811/ (2认同)