Skip to content

Instantly share code, notes, and snippets.

@tom--
Last active February 11, 2023 06:14
  • Star 62 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save tom--/35f8a029d77467559cf2 to your computer and use it in GitHub Desktop.
PHP random bytes, integers and UUIDs

Random bytes, ints, UUIDs in PHP

Simple and safe random getters to copy-paste

string randomBytes( int $length )

int randomInt ( int $min , int $max )

string randomUuid ( void )

For simple implementation of the deterministic UUID versions, see this Gist but I don't recommend using my_rand() for random UUIDs.

Only uses quality randomness sources

You can trust these sources and trust that they are properly seeded

  • LibreSSL
  • CryptGenRandom
  • /dev/urandom
  • avoids OpenSSL's RNG.

Simple

The scripts are simple enough to

  • understand
  • own, instead of adding as dependencies
  • copy-and-paste into your code (hence a Gist, not a repo)
  • debug

In case of failure

The scripts will fail in some PHP runtime environments, for example:

  • Windows: if PHP has no mcrypt or OpenSSL extension, or if PHP is too old
  • Linux/BSD: if PHP has no mcrypt extension, cannot read from /dev/urandom (e.g. because open_basedir) and has no OpenSSL extension compiled against LibreSSL

If you experience an 'Unable to generate a random key' exception, try the testPlatform.php script. It prints info with which you can figure out all the branches in randomBytes().

<?php
/**
* @author Tom Worster <fsb@thefsb.org>
* @copyright Copyright (c) 2008 Yii Software LLC
* @license 3-Clause BSD. See XTRAS.md in this Gist.
*/
/**
* Generates specified number of random bytes. Output is binary string, not ASCII.
*
* @param integer $length the number of bytes to generate
*
* @return string the generated random bytes
* @throws \Exception
*/
function randomBytes($length)
{
if (function_exists('random_bytes')) {
return random_bytes($length);
}
if (!is_int($length) || $length < 1) {
throw new \Exception('Invalid first parameter ($length)');
}
// The recent LibreSSL RNGs are faster and likely better than /dev/urandom.
// Parse OPENSSL_VERSION_TEXT because OPENSSL_VERSION_NUMBER is no use for LibreSSL.
// https://bugs.php.net/bug.php?id=71143
static $libreSSL;
if ($libreSSL === null) {
$libreSSL = defined('OPENSSL_VERSION_TEXT')
&& preg_match('{^LibreSSL (\d\d?)\.(\d\d?)\.(\d\d?)$}', OPENSSL_VERSION_TEXT, $matches)
&& (10000 * $matches[1]) + (100 * $matches[2]) + $matches[3] >= 20105;
}
// Since 5.4.0, openssl_random_pseudo_bytes() reads from CryptGenRandom on Windows instead
// of using OpenSSL library. Don't use OpenSSL on other platforms.
if ($libreSSL === true
|| (DIRECTORY_SEPARATOR !== '/'
&& PHP_VERSION_ID >= 50400
&& substr_compare(PHP_OS, 'win', 0, 3, true) === 0
&& function_exists('openssl_random_pseudo_bytes'))
) {
$key = openssl_random_pseudo_bytes($length, $cryptoStrong);
if ($cryptoStrong === false) {
throw new Exception(
'openssl_random_pseudo_bytes() set $crypto_strong false. Your PHP setup is insecure.'
);
}
if ($key !== false && mb_strlen($key, '8bit') === $length) {
return $key;
}
}
// mcrypt_create_iv() does not use libmcrypt. Since PHP 5.3.7 it directly reads
// CrypGenRandom on Windows. Elsewhere it directly reads /dev/urandom.
if (PHP_VERSION_ID >= 50307 && function_exists('mcrypt_create_iv')) {
$key = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
if (mb_strlen($key, '8bit') === $length) {
return $key;
}
}
// If not on Windows, try a random device.
if (DIRECTORY_SEPARATOR === '/') {
// urandom is a symlink to random on FreeBSD.
$device = PHP_OS === 'FreeBSD' ? '/dev/random' : '/dev/urandom';
// Check random device for speacial character device protection mode. Use lstat()
// instead of stat() in case an attacker arranges a symlink to a fake device.
$lstat = @lstat($device);
if ($lstat !== false && ($lstat['mode'] & 0170000) === 020000) {
$key = @file_get_contents($device, false, null, 0, $length);
if ($key !== false && mb_strlen($key, '8bit') === $length) {
return $key;
}
}
}
throw new \Exception('Unable to generate a random key');
}
/**
* Generates a random UUID using the secure RNG.
*
* Returns Version 4 UUID format: xxxxxxxx-xxxx-4xxx-Yxxx-xxxxxxxxxxxx where x is
* any random hex digit and Y is a random choice from 8, 9, a, or b.
*
* @return string the UUID
*/
function randomUuid()
{
$bytes = randomBytes(16);
$bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
$bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);
$id = str_split(bin2hex($bytes), 4);
return "{$id[0]}{$id[1]}-{$id[2]}-{$id[3]}-{$id[4]}-{$id[5]}{$id[6]}{$id[7]}";
}
/**
* Returns a random integer in the range $min through $max inclusive.
*
* @param int $min Minimum value of the returned integer.
* @param int $max Maximum value of the returned integer.
*
* @return int The generated random integer.
* @throws \Exception
*/
function randomInt($min, $max)
{
if (function_exists('random_int')) {
return random_int($min, $max);
}
if (!is_int($min)) {
throw new \Exception('First parameter ($min) must be an integer');
}
if (!is_int($max)) {
throw new \Exception('Second parameter ($max) must be an integer');
}
if ($min > $max) {
throw new \Exception('First parameter ($min) must be no greater than second parameter ($max)');
}
if ($min === $max) {
return $min;
}
// $range is a PHP float if the expression exceeds PHP_INT_MAX.
$range = $max - $min + 1;
if (is_float($range)) {
$mask = null;
} else {
// Make a bit mask of (the next highest power of 2 >= $range) minus one.
$mask = 1;
$shift = $range;
while ($shift > 1) {
$shift >>= 1;
$mask = ($mask << 1) | 1;
}
}
$tries = 0;
do {
$bytes = randomBytes(PHP_INT_SIZE);
// Convert byte string to a signed int by shifting each byte in.
$value = 0;
for ($pos = 0; $pos < PHP_INT_SIZE; $pos += 1) {
$value = ($value << 8) | ord($bytes[$pos]);
}
if ($mask === null) {
// Use all bits in $bytes and check $value against $min and $max instead of $range.
if ($value >= $min && $value <= $max) {
return $value;
}
} else {
// Use only enough bits from $bytes to cover the $range.
$value &= $mask;
if ($value < $range) {
return $value + $min;
}
}
$tries += 1;
} while ($tries < 123);
// Worst case: this is as likely as 123 heads in as many coin tosses.
throw new \Exception('Unable to generate random int after 123 tries');
}
<?php
/**
* @copyright Copyright (c) 2016 Tom Worster <fsb@thefsb.org>
* @license ISC. See XTRAS.md in this Gist.
*/
require(dirname(__FILE__) . '/randomStuff.php');
array_shift($argv);
$testNumber = (int) array_shift($argv);
$a = array_shift($argv);
$b = array_shift($argv);
$a = is_numeric($a) ? (int) $a : $a;
$b = is_numeric($b) ? (int) $b : $b;
// String width in hex digits
$width = 2 * PHP_INT_SIZE;
if ($testNumber === 5) {
if ($a < 1 || $a > PHP_INT_SIZE - 1) {
fwrite(STDERR, "Num bytes must be between 1 and " . (PHP_INT_SIZE - 1) . "\n");
exit(1);
}
$range = (1 << (8 * $a)) - 1;
$format = '%0' . (2 * $a) . 'x';
} else {
$format = '%0' . (2 * PHP_INT_SIZE) . 'x';
}
function int2hex($int)
{
$hex = dechex($int);
return strlen($hex) % 2 === 0 ? $hex : '0' . $hex;
}
if (!function_exists('hex2bin')) {
function hex2bin($hex) {
return pack("H*" , $hex);
}
}
while (true) {
switch ($testNumber) {
case 1:
echo randomInt($a, $b) . "\n";
break;
case 2:
echo randomUuid() . "\n";
break;
case 3:
echo randomBytes($a);
break;
case 4:
echo hex2bin(sprintf($format, randomInt(~PHP_INT_MAX, PHP_INT_MAX)));
break;
case 5:
$min = randomInt(~PHP_INT_MAX, PHP_INT_MAX - $range);
echo hex2bin(sprintf($format, randomInt($min, $min + $range) - $min));
break;
case 6:
preg_match(
'%^([\da-f]{8})-([\da-f]{4})-[\da-f]{2}([\da-f]{2})-[\da-f]{2}([\da-f]{2})-([\da-f]{12})%i',
randomUuid(),
$matches
);
array_shift($matches);
echo hex2bin(implode('', $matches));
break;
default:
fwrite(STDERR, "No such test: $testNumber\n");
exit(1);
}
}
<?php
/*
The OpenSSL RAND collision bug.
"Since the UNIX fork() system call duplicates the entire process state, a random number generator
which does not take this issue into account will produce the same sequence of random numbers in
both the parent and the child (or in multiple children), leading to cryptographic disaster (i. e.
people being able to read your communications).
"OpenSSL's default random number generator mixes in the PID, which provides a certain degree of
fork safety. However, once the PIDs wrap, new children will start to produce the same random
sequence as previous children which had the same PID."
Try to recreate this through forking.
There are two versions as illustrated. In the diagrams, the parent continues horizontally and
the child forks off downwards. # means the process quits.
Version 1
=========
Begin ------ #
\ m ← myPid
\--- Write rands, #
\ Waste OS PIDs to wrap them around
\--- #
\--- # While childPid < m
\--- # fork and discard parent
\--- #
\
If myPid = m: write rands, #
otherwise: go back to Waste PIDs step
Version 2
=========
Begin ----- #
\--- m ← childPid, Waste PIDs ------ #
\ \--- # While childPid < m
Write rands, # \--- # fork and discard parent
\--- #
\
If myPid = m: write rands, #
otherwise: go back to Waste PIDs step
Version 3
=========
Begin ----- #
\----- m ← childPid, Waste PIDs ------------------------- If childPid = m: #
\ \ \ \ \ otherwise: go back to Waste PIDs step
Write rands, # # # # \
While childPid < m If myPid == m: write rands, #
fork and discard child
*/
if (!defined('OPENSSL_VERSION_TEXT') || !OPENSSL_VERSION_TEXT) {
die("No OpenSSL\n");
}
if (!function_exists('pcntl_fork')) {
die("No PCNTL extension\n");
}
// Fork once and discard parent.
$childPid = pcntl_fork();
if ($childPid === -1) {
die("Cannot fork\n");
}
if ($childPid !== 0) {
exit;
}
// Allow command shell time to write a prompt.
sleep(1);
echo "\n" . date('r') . ' ' . phpversion() . ' ' . OPENSSL_VERSION_TEXT . "\n";
$initialPid = getmypid();
// Fork to get the "identical" processes and compare their OpenSSL RAND output.
$childPid = pcntl_fork();
if ($childPid === -1) {
die("Cannot fork\n");
}
/*
* Version 1
* =========
* The parent of the last fork writes random bytes for comparison and exits.
* Its child then forks some more children to get the same PID as this parent.
*/
//if ($childPid !== 0) {
// writeBytes('Initial parent');
// exit;
//}
//$matchPid = $initialPid;
/*
* Version 2 and 3
* ===============
* The child of the last fork writes random bytes for comparison and exits.
* Its parent then forks some more children to get the same PID as this child.
*/
if ($childPid === 0) {
writeBytes('Initial child');
exit;
}
$matchPid = $childPid;
while (true) {
do {
// Waste PIDs in the OS (without forking) to make them wrap wround.
$lastPid = trim(exec('echo $$'));
} while ($lastPid > $matchPid || $lastPid < $matchPid - 10);
// Fork and discard parents (V1, V2) or children (V3) until a child has PID equal to $matchPid.
while ($lastPid < $matchPid) {
$childPid = pcntl_fork();
if ($childPid === -1) {
die("Cannot fork\n");
}
if ($childPid === 0) {
if (getmypid() === $matchPid) {
writeBytes('Matching PID child');
}
exit;
}
$lastPid = $childPid;
echo date('H:i:s ') . "Forked new child $lastPid\n";
if ($lastPid === $matchPid) {
exit;
}
}
}
function writeBytes($note, $nBytes = 128)
{
echo date('H:i:s ') . getmypid() . " $note got:\n";
$bytes = openssl_random_pseudo_bytes($nBytes);
for ($i = 0; $i < $nBytes; $i += 1) {
printf('%02x', ord($bytes[$i]));
if (($i % 32) === 31) {
echo "\n";
} elseif (($i % 4) === 3) {
echo " ";
}
}
}
<?php
/**
* @copyright Copyright (c) 2016 Tom Worster <fsb@thefsb.org>
* @license ISC. See XTRAS.md in this Gist.
*/
$tests = [
"function_exists('random_bytes')",
"defined('OPENSSL_VERSION_TEXT') ? OPENSSL_VERSION_TEXT : null",
"PHP_VERSION_ID",
"PHP_OS",
"function_exists('openssl_random_pseudo_bytes') ? bin2hex(openssl_random_pseudo_bytes(8)) : null",
"function_exists('mcrypt_create_iv') ? bin2hex(mcrypt_create_iv(8, MCRYPT_DEV_URANDOM)) : null",
"DIRECTORY_SEPARATOR",
"ini_get('open_basedir')",
];
if (DIRECTORY_SEPARATOR === '/') {
$tests[] = "sprintf('%o', lstat(PHP_OS === 'FreeBSD' ? '/dev/random' : '/dev/urandom')['mode'] & 0170000)";
$tests[] = "bin2hex(file_get_contents(PHP_OS === 'FreeBSD' ? '/dev/random' : '/dev/urandom', false, null, 0, 8))";
}
foreach ($tests as $i => $test) {
$result = eval('return ' . $test . ';');
printf("%2d %s\n %s\n\n", $i + 1, $test, var_export($result, true));
}

Testing

Try the test.php script like this

// Look at random integers in 0 thru 999
php test.php 1 0 999 | less

// Look at random UUIDs
php test.php 2 | less

To statistically test for randomness of the function outputs, install dieharder. There are packages called dieharder on OS X Homebrew, Ubuntu and Debian. Others can compile the source.

Dieharder takes a long time but very thorough. With LibreSSL it's OK, otherwise allow plenty of time. The UUID test and all the randomInt() tests are horribly slow.

// Test randomBytes() with 8192-byte length by sending the output to dieharder
$ php test.php 3 8192 | dieharder -g 200 -a

// Test `randomInt()` with max possible range by converting to 4 or 8-byte strings
$ php test.php 4 | dieharder -g 200 -a

// Test `randomInt()` with range 0 to 255 and random offset, convert to 1-byte strings before output
$ php test.php 5 1 | dieharder -g 200 -a

// Test `randomInt()` with range 0 to 256^3-1 and random offset, convert to 3-byte strings before output
$ php test.php 5 3 | dieharder -g 200 -a

// Convert the random substrings of UUIDs to bytes
$ php test.php 6 | dieharder -g 200 -a

Performance

On OS X 10.10.5 and PHP 5.6.16 with LibreSSL 2.3.1

$ php test.php 3 8192 | pv -s 10g -S > /dev/null
  10GiB 0:00:25 [ 405MiB/s]

Without the complex preg_match() stuff it goes up to 435 MiB/s. OpenBSD should be fast too. Probably also NetBSD since 7.0.

On the same OS X machine I see 18.5 MiB/s with PHP 7.0 or with PHP 5.6 using mcrypt or /dev/urandom – that's the rate of OS X's Yarrow RNG. On Linux I get 14.5 MiB/s.

Licenses

New stuff in this Gist

All files except randomStuff.php are original in this Gist and have the following copyright and license.

Copyright (c) 2016, Tom Worster fsb@thefsb.org

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

Modified Yii 2.0 code in this Gist

I extracted the code in randomStuff.php from Yii 2.0, hence that file has its following copyright and license. See also Yii License.

The Yii framework is free software. It is released under the terms of the following BSD License.

Copyright © 2008 by Yii Software LLC (http://www.yiisoft.com) All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  • Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  • Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  • Neither the name of Yii Software LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

@githubjeka
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment