"Secure" Random Numbers in PHP



  • Going from this:

    mt_srand ((double) microtime() * 1000000);

    To this:

    /**
    * Returns a securely generated seed for PHP's RNG (Random Number Generator)
    *
    * @param int Length of the seed bytes (8 is default. Provides good cryptographic variance)
    * @return int An integer equivilent of a secure hexadecimal seed
    */
    function secure_seed_rng($count=8)
    {
    $output = '';

    // Try the OpenSSL method first. This is the strongest.
    if(function_exists('openssl_random_pseudo_bytes'))
    {
    $output = openssl_random_pseudo_bytes($count, $strong);

    if($strong !== true)
    {
    $output = '';
    }
    }

    if($output == '')
    {
    // Then try the unix/linux method
    if(@is_readable('/dev/puxurandom') && ($handle = @fopen('/dev/urandom', 'rb')))
    {
    $output = @fread($handle, $count);
    @fclose($handle);
    }

    // Then try the Microsoft method
    if(version_compare(PHP_VERSION, '5.0.0', '>=') && class_exists('COM'))
    {
    try {
    $util = new COM('CAPICOM.Utilities.1');
    $output = base64_decode($util->GetRandom($count, 0));
    }
    catch(Exception $ex) { }
    }
    }

    // Didn't work? Do we still not have enough bytes? Use our own (less secure) rng generator
    if(strlen($output) < $count)
    {
    $output = '';

    // Close to what PHP basically uses internally to seed, but not quite.
    $unique_state = microtime().getmypid();

    for($i = 0; $i < $count; $i += 16)
    {
    $unique_state = md5(microtime().$unique_state);
    $output .= pack('H*', md5($unique_state));
    }
    }

    // /dev/urandom and openssl will always be twice as long as $count. base64_encode will roughly take up 33% more space but crc32 will put it to 32 characters
    $output = hexdec(substr(dechex(crc32(base64_encode($output))), 0, $count));

    return $output;
    }

    /**
    * Wrapper function for mt_rand. Automatically seeds using a secure seed once.
    *
    * @param int Optional lowest value to be returned (default: 0)
    * @param int Optional highest value to be returned (default: mt_getrandmax())
    * @param boolean True forces it to reseed the RNG first
    * @return int An integer equivilent of a secure hexadecimal seed
    */
    function my_rand($min=null, $max=null, $force_seed=false)
    {
    static $seeded = false;
    static $obfuscator = 0;

    if($seeded == false || $force_seed == true)
    {
    mt_srand(secure_seed_rng());
    $seeded = true;

    $obfuscator = abs((int) secure_seed_rng());

    // Ensure that $obfuscator is <= mt_getrandmax() for 64 bit systems.
    if($obfuscator > mt_getrandmax())
    {
    $obfuscator -= mt_getrandmax();
    }
    }

    if($min !== null && $max !== null)
    {
    $distance = $max - $min;
    if ($distance > 0)
    {
    return $min + (int)((float)($distance + 1) * (float)(mt_rand() ^ $obfuscator) / (mt_getrandmax() + 1));
    }
    else
    {
    return mt_rand($min, $max);
    }
    }
    else
    {
    $val = mt_rand() ^ $obfuscator;
    return $val;
    }
    }

    Good? Bad? WTF? How would you do it?


  • Discourse touched me in a no-no place

    Depends on what you want to use the random number for



  • Account password generation for example.

    Needless to say the guy who wrote that article had his hands in the code above.

    So it's good?



  • I think the answer is "probabaly but who really cares".
    The above code looks reasonable enough, but I think in 99.99% of the time your application will have bigger problems then how random your numbers are.
    I am sure that theoretically the more random number will make it all safer. However unless your a bank or something I think it's overkill.



  •  Personally, I don't worry about the seeding problem.  The chances are that unless you're releasing the code, the attacker won't know the exact sequence of your random number generation, so it's exceptionally hard for them to brute force predict the next number.  And consider the use case of the number you're using.  I'm assuming that they are for temporary passwords.  So if you're worried about brute forcing random numbers that much, I assume that you've added a system to disable logins from a single host after a certain number of failures (so if someone tried 10 times incorrectly from the same host, you auto-ban that host for a time period).  Sure, it doesn't stop the DDOS attack, but it should make brute forcing MUCH more difficult.

    And the number generation issue (the seeding issue) is really only a major issue in one of a few scenarios:

    1. You don't control all of the code on the server (meaning non-trusted users can install their own code which is served by the same php process)
    2. You output the number directly (instead of using it for an internal calculation with other inputs)
    3. You're using CGI

    Other than that, I'd think you'd be relatively safe with seeding with something along the lines of (Only if you fall into one of the 3 above categories):

    $seed = crc32(uniqid(sha1(microtime(true) . getmypid()), true));
    mt_srand($seed);
    $n = mt_rand(1, 200);
    for ($i = 0; $i < $n; $i++) {
        mt_rand();
    }

     

    then simply using mt_rand to return the number you want.

    The rational is as such:

    $seed = crc32(uniqid(sha1(microtime(true) . getmypid()), true));

    From the inside out, you're generating a value for the current microsecond, and then passing it through sha1 to get a string.  Then you're passing that through uniqid which will add 23 bits of entropy to that figure.  CRC32 is used to turn the resultant string into a number for seeding mt_srand();

     $n = mt_rand(1, 200);
    for ($i = 0; $i < $n; $i++) {
        mt_rand();
    }

    Even if someone figures out the value of a mt_rand call that you use later on in the code, they won't know which call it was (so it makes it a lot harder to figure out which sequence you're using).  Without that part, if they saw a 4, they'd only need to check the first 2 or 3 positions of known seed sequences to try to "guess" the next value.  With this part, they have no idea at the initial value, nor the actual position in the sequence that you're at, so it becomes a non-trivial problem to search the list for a match in the sequence.

     

    But again, if you're running your own server, I wouldn't worry too much about it.  Spend your time with a secure hash salting algorythm and guarding against brute force attacks, and you'll be fine...



  • Thank you all for your replies! :)



  • @ircmaxell said:

    But again, if you're running your own server, I wouldn't worry too much about it.  Spend your time with a secure hash salting algorythm and guarding against brute force attacks, and you'll be fine...
    Indeed. Another massive weakness of the attack mentioned is load-balanced servers. If there are enough back-end servers it becomes near impossible for an attacker to figgure out which server eir hitting on any given request.


Log in to reply