web-dev-qa-db-fra.com

Comment ensemencer de manière succincte, portable et complète le PRNG mt19937?

Il semble y avoir beaucoup de réponses dans lesquelles quelqu'un suggère d'utiliser <random> Pour générer des nombres aléatoires, généralement avec un code comme celui-ci:

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

Habituellement, cela remplace une sorte d '"abomination profane" telle que:

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

Nous pourrions critiquer l'ancienne manière en faisant valoir que time(NULL) fournit une entropie faible, time(NULL) est prévisible et que le résultat final est non uniforme.

Mais tout cela est vrai de la nouvelle façon: il a juste un vernis plus brillant.

  • rd() renvoie un seul unsigned int. Cela a au moins 16 bits et probablement 32. Ce n'est pas assez pour semer l'état 19937 bits de MT.

  • Utiliser std::mt19937 gen(rd());gen() (ensemencer avec 32 bits et regarder la première sortie) ne donne pas une bonne distribution de sortie. 7 et 13 ne peuvent jamais être la première sortie. Deux graines produisent 0. Douze graines produisent 1226181350. ( Link )

  • std::random_device Peut être, et est parfois, implémenté comme un simple PRNG avec une valeur de départ fixe. Il pourrait donc produire la même séquence à chaque exécution. ( Lien ) C'est encore pire que time(NULL).

Pire encore, il est très facile de copier et coller les extraits de code précédents, malgré les problèmes qu’ils contiennent. Certaines solutions à cela nécessitent l’acquisition de largishbibliothèques qui peuvent ne pas convenir à tout le monde.

À la lumière de ceci, ma question est Comment peut-on semer de manière succincte, portable et complète le mt19937 PRNG en C++?

Compte tenu des problèmes ci-dessus, une bonne réponse:

  • Vous devez entièrement ensemencer mt19937/mt19937_64.
  • Ne peut pas compter uniquement sur std::random_device Ou time(NULL) comme source d'entropie.
  • Ne devrait pas compter sur Boost ou d'autres bibliothèques.
  • Devrait tenir dans un petit nombre de lignes de telle sorte qu'il semblerait Nice copier-collé dans une réponse.

Pensées

  • Ma pensée actuelle est que les sorties de std::random_device Peuvent être écrasées (peut-être via XOR) avec time(NULL), les valeurs dérivées de randomisation de l'espace d'adressage , et un code codé en dur constante (qui peut être définie lors de la distribution) pour obtenir le meilleur effort au niveau de l'entropie.

  • std::random_device::entropy()ne le fait pas donne une bonne indication de ce que std::random_device pourrait ou ne pourrait pas faire.

106
Richard

Je dirais que le plus grand défaut avec std::random_device est la suivante: une solution de secours déterministe est permise si aucun CSPRNG n'est disponible. Cela seul est une bonne raison de ne pas ensemencer a PRNG en utilisant std::random_device, puisque les octets produits peuvent être déterministes. Malheureusement, il ne fournit pas d'API pour savoir quand cela se produit, ou pour demander un échec au lieu de nombres aléatoires de faible qualité.

C'est-à-dire qu'il n'y a pas de solution complètement portable : il existe toutefois une approche décente et minimale. Vous pouvez utiliser un wrapper minimal autour d'un CSPRNG (défini comme sysrandom ci-dessous) pour générer le PRNG.

Les fenêtres


Vous pouvez compter sur CryptGenRandom, un CSPRNG. Par exemple, vous pouvez utiliser le code suivant:

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-like


Sur de nombreux systèmes de type Unix, vous devez utiliser / dev/urandom lorsque cela est possible (bien que cela ne soit pas garanti sur les systèmes compatibles 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;
}

Autre


Si aucun CSPRNG n'est disponible, vous pouvez choisir de vous fier à std::random_device. Cependant, je voudrais éviter cela si possible, car divers compilateurs (notamment MinGW) l’implémentent avec un PRNG (en fait, produisant la même séquence tous les le temps d'alerter les humains que ce n'est pas correctement aléatoire).

Ensemencement


Maintenant que nous avons nos pièces avec une surcharge minimale, nous pouvons générer les bits d'entropie aléatoire souhaités pour ensemencer notre PRNG. L'exemple utilise (un nombre manifestement insuffisant) 32 bits pour initialiser le PRNG et vous devez augmenter cette valeur (qui dépend de votre CSPRNG).

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

Comparaison Pour Boost


Nous pouvons voir des parallèles à boost :: random_device (un vrai CSPRNG) après un rapide coup d’œil sur le code source . Boost utilise MS_DEF_PROV sur Windows, qui est le type de fournisseur pour PROV_RSA_FULL. Il ne manquerait que de vérifier le contexte cryptographique, ce qui peut être fait avec CRYPT_VERIFYCONTEXT. Sur * Nix, Boost utilise /dev/urandom. En d'autres termes, cette solution est portable, bien testée et facile à utiliser.

Spécialisation Linux


Si vous êtes prêt à renoncer à la concision pour la sécurité, getrandom est un excellent choix sous Linux 3.17 et versions ultérieures, ainsi que dans les versions récentes de Solaris. getrandom se comporte de manière identique à /dev/urandom, sauf qu'il se bloque si le noyau n'a pas encore initialisé son CSPRNG après le démarrage. L'extrait de code suivant détecte si Linux getrandom est disponible et si non retombe à /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


Il y a un dernier avertissement: OpenBSD moderne n'a pas /dev/urandom. Vous devriez utiliser getentropy à la place.

#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

D'autres pensées


Si vous avez besoin d'octets aléatoires sécurisés sur le plan cryptographique, vous devriez probablement remplacer le fstream par le fichier open/read/close de POSIX sans mémoire tampon. C'est parce que les deux basic_filebuf et FILE contiennent un tampon interne, qui sera alloué via un allocateur standard (et ne sera donc pas effacé de la mémoire).

Cela pourrait facilement être fait en changeant sysrandom en:

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

Merci


Un merci spécial à Ben Voigt pour avoir signalé que FILE utilise des lectures en mémoire tampon et ne doit donc pas être utilisé.

Je voudrais également remercier Peter Cordes d’avoir mentionné getrandom et le manque d’OpenBSD /dev/urandom.

55
Alexander Huszagh

Dans un sens, cela ne peut pas être fait de manière portable. En d’autres termes, on peut concevoir une plate-forme pleinement déterministe valide fonctionnant en C++ (par exemple, un simulateur qui incrémente l’horloge de la machine de manière déterministe et avec une E/S "déterminée") dans laquelle il n’existe aucune source d’aléatoire pour générer un PRNG.

22
einpoklum

Vous pouvez utiliser un std::seed_seq et remplissez-le au moins à la taille requise pour le générateur en utilisant la méthode d’Alexander Huszagh pour obtenir l’entropie:

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

S'il y avait une manière appropriée de remplir ou de créer un SeedSequence à partir d'un niformRandomBitGenerator dans la bibliothèque standard en utilisant std::random_device pour ensemencer correctement serait beaucoup plus simple.

12
ratchet freak

L'implémentation sur laquelle je travaille tire parti de la propriété state_size De mt19937 PRNG pour décider du nombre de graines à fournir à l'initialisation:

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

Je pense qu'il y a place à amélioration car std::random_device::result_type Peut différer de std::mt19937::result_type En taille et en étendue, il faut donc en tenir compte.

Une note à propos de std :: random_device .

Selon la C++11(/14/17) norme (s):

26.5.6 Classe random_device [ Rand.device ]

2 Si les limitations d'implémentation empêchent de générer des nombres aléatoires non déterministes, l'implémentation peut employer un moteur de nombres aléatoires.

Cela signifie que l'implémentation ne peut générer que des valeurs déterministes s'il est impossible de générer des valeurs non déterministes par une certaine limitation.

Le compilateur MinGW sur Windows ne fournit pas non déterministe les valeurs de son std::random_device, Bien qu'elles soient facilement disponibles à partir du système d'exploitation . Je considère donc que ceci est un bogue et qu’il est peu probable que cela se produise entre les implémentations et les plates-formes.

4
Galik

Il n'y a rien de mal à semer en utilisant le temps, en supposant que vous n'en avez pas besoin pour être sécurisé (et vous n'avez pas dit que c'était nécessaire). L'idée est que vous pouvez utiliser le hachage pour corriger le caractère non aléatoire. J'ai trouvé que cela fonctionne correctement dans tous les cas, y compris et en particulier pour les simulations de Monte Carlo lourdes.

Une caractéristique intéressante de cette approche est qu’elle généralise à l’initialisation à partir d’autres ensembles de semences non vraiment aléatoires. Par exemple, si vous voulez que chaque thread ait son propre RNG (pour la sécurité du thread), vous pouvez simplement l'initialiser en fonction de l'ID de thread haché.

Ce qui suit est un SSCCE , distillé à partir de ma base de code (pour simplifier; certains OO support structures élidé):

#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);
}
2
imallett

Voici mon propre coup à la question:

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

L’idée ici est d’utiliser XOR pour combiner plusieurs sources potentielles d’entropie (temps rapide, temps lent, std::random-device, emplacements de variables statiques, emplacements de tas, emplacements de fonctions, emplacements de bibliothèques, valeurs spécifiques à un programme) pour tenter au mieux d’initialiser le fichier mt19937. Tant qu'au moins une fois la source est "bonne", le résultat sera au moins aussi "bon".

Cette réponse n’est pas aussi courte qu’il serait préférable et peut contenir une ou plusieurs erreurs de logique. Donc, je considère que c'est un travail en cours. Veuillez commenter si vous avez des commentaires.

0
Richard