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

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

 std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(0, 5); dis(gen); 

通常这会取代某种“不圣洁的憎恶”,比如:

 srand(time(NULL)); rand()%6; 

我们可以通过争论time(NULL)提供低熵, time(NULL)是可预测的,最终结果是不一致的来批评旧的方法。

但所有这一切都是新的方式:它只是有一个更光泽的单板。

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

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

  • std::random_device可以是,有时是作为一个简单的PRNG与一个固定的种子实现。 因此,它可能会在每次运行中产生相同的序列。 ( 链接 )这比time(NULL)更差time(NULL)

更糟糕的是,复制和粘贴上述代码片段非常容易,尽pipe它们包含的问题。 有些解决scheme需要获取可能不适合每个人的大型 图书馆 。

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

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

  • 必须完全种下mt19937 / mt19937_64。
  • 不能单纯依靠std::random_devicetime(NULL)作为熵源。
  • 不应该依靠Boost或其他库文库。
  • 应适合less数几行,以便它看起来很好复制粘贴到一个答案。

思考

  • 我目前的想法是std::random_device输出可以通过time(NULL) ,从地址空间随机化得到的值和一个硬编码的常量(可以在发布期间设置)来混合(也许通过XOR)在熵上尽力而为。

  • std::random_device::entropy() 并不能很好的说明std::random_device可能做什么或者不可以做什么。

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

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

视窗


你可以依靠CryptGenRandom ,一个CSPRNG。 例如,您可以使用以下代码:

 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; } 

类Unix


在许多类Unix系统上,尽可能使用/ dev / urandom (尽pipe这在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; } 

其他


如果没有CSPRNG可用,则可以select依赖std::random_device 。 不过,如果可能的话,我会避免这种情况,因为各种编译器(最着名的是MinGW)都将它作为PRNG来实现 (事实上​​,每次都会产生相同的序列,以提醒人们它不是随机的)。

播种


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

 std::uint_least32_t seed; sysrandom(&seed, sizeof(seed)); std::mt19937 gen(seed); 

比较提升


在快速浏览源代码之后,我们可以看到类似于boost :: random_device(一个真正的CSPRNG)。 Windows上的Boost使用MS_DEF_PROV ,这是PROV_RSA_FULL的提供程序types。 唯一缺less的将是validation密码上下文,这可以通过CRYPT_VERIFYCONTEXT来完成。 在* Nix上,Boost使用/dev/urandom 。 IE,这个解决scheme是便携式的,经过很好的testing,并且易于使用。

Linux专业化


如果您愿意为了安全而牺牲简洁性, getrandom是Linux 3.17及更高版本和最近Solaris上的绝佳select。 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 

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 

其他想法


如果你需要密码安全的随机字节,你应该用POSIX的无缓冲打开/读/关来replacefstream。 这是因为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; } 

谢谢


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

我还要感谢Peter Cordes提到的getrandom ,而OpenBSD缺乏/dev/urandom

从某种意义上讲,这不可能做到便携。 也就是说,人们可以设想一个运行C ++的有效的完全确定性的平台(比如说,一个模拟器,确定性地对机器时钟进行步进,以及确定性的I / O),在这个平台中没有任何随机性来build立PRNG。

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

 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); } 

如果有一个正确的方法来填充或从标准库中的UniformRandomBitGenerator创buildSeedSequence使用std::random_device适当种植将会简单得多。

我正在执行的实现利用mt19937 PRNG的state_size属性来决定在初始化时要提供多less个种子:

 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}); } 

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

关于std :: random_device的说明 。

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

26.5.6类random_device [ rand.device ]

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

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

Windows上的MinGW编译器着名的不提供std::random_device 非确定性值,尽pipe它们很容易从操作系统中获得。 所以我认为这是一个错误,不太可能在实现和平台上发生。

使用时间播种没有任何问题,假设你不需要它是安全的(而且你没有说这是必要的)。 洞察力是你可以使用哈希来修复非随机性。 我发现在所有情况下都可以正常工作,特别是对于重蒙特卡罗模拟。

这种方法的一个很好的特点是它推广到其他非真随机种子的初始化。 例如,如果您希望每个线程都有自己的RNG(用于threadsafety),则只需根据散列的线程ID进行初始化即可。

以下是从我的代码库中提取的SSCCE (为简单起见,一些OO支持结构被省略):

 #include <cstdint> //`uint32_t` #include <functional> //`std::hash` #include <random> //`std::mt19937` #include <iostream> //`std::cout` static std::mt19937 rng; static void seed(uint32_t seed) { rng.seed(static_cast<std::mt19937::result_type>(seed)); } static void seed() { uint32_t t = static_cast<uint32_t>( time(nullptr) ); std::hash<uint32_t> hasher; size_t hashed=hasher(t); seed( static_cast<uint32_t>(hashed) ); } int main(int /*argc*/, char* /*argv*/[]) { seed(); std::uniform_int_distribution<> dis(0, 5); std::cout << dis(rng); } 

这是我自己的问题:

 #include <random> #include <chrono> #include <cstdint> #include <algorithm> #include <functional> #include <iostream> uint32_t LilEntropy(){ //Gather many potential forms of entropy and XOR them const uint32_t my_seed = 1273498732; //Change during distribution static uint32_t i = 0; static std::random_device rd; const auto hrclock = std::chrono::high_resolution_clock::now().time_since_epoch().count(); const auto sclock = std::chrono::system_clock::now().time_since_epoch().count(); auto *heap = malloc(1); const auto mash = my_seed + rd() + hrclock + sclock + (i++) + reinterpret_cast<intptr_t>(heap) + reinterpret_cast<intptr_t>(&hrclock) + reinterpret_cast<intptr_t>(&i) + reinterpret_cast<intptr_t>(&malloc) + reinterpret_cast<intptr_t>(&LilEntropy); free(heap); return mash; } //Fully seed the mt19937 engine using as much entropy as we can get our //hands on void SeedGenerator(std::mt19937 &mt){ std::uint_least32_t seed_data[std::mt19937::state_size]; std::generate_n(seed_data, std::mt19937::state_size, std::ref(LilEntropy)); std::seed_seq q(std::begin(seed_data), std::end(seed_data)); mt.seed(q); } int main(){ std::mt19937 mt; SeedGenerator(mt); for(int i=0;i<100;i++) std::cout<<mt()<<std::endl; } 

这里的想法是使用异或来结合许多潜在的熵源(快速时间,慢时间, std::random-device ,静态variables位置,堆位置,函数位置,库位置,程序特定值),使最佳努力尝试初始化mt19937。 只要至less一次来源是“好”,结果至less会是“好”。

这个答案并不是最好的,可能包含一个或多个逻辑错误。 所以我正在考虑这是一个正在进行的工作。 如果你有反馈,请评论。

给定的平台可能有熵的来源,例如/dev/random 。 从std::chrono::high_resolution_clock::now()开始的时代以来,纳秒可能是标准库中最好的种子。

我以前曾经使用类似于(uint64_t)( time(NULL)*CLOCKS_PER_SEC + clock() )来获得更多位的熵,对于不是安全性至关重要的应用程序。