BigInteger: refactor random number generation code somewhat

This commit is contained in:
terrafrost 2013-08-06 23:17:49 -05:00
parent 7b5542cc8a
commit 442922ff0a

View File

@ -3028,6 +3028,37 @@ class Math_BigInteger {
{ {
} }
/**
* Generates a random BigInteger
*
* Byte length is equal to $length. Uses crypt_random if it's loaded and mt_rand if it's not.
*
* @param Integer $length
* @return Math_BigInteger
* @access private
*/
function _random_number_helper($size)
{
$crypt_random = function_exists('crypt_random_string') || (!class_exists('Crypt_Random') && function_exists('crypt_random_string'));
if ($crypt_random) {
$random = crypt_random_string($size);
} else {
$random = '';
if ($size & 1) {
$random.= chr(mt_rand(0, 255));
}
$blocks = $size >> 1;
for ($i = 0; $i < $blocks; ++$i) {
// mt_rand(-2147483648, 0x7FFFFFFF) always produces -2147483648 on some systems
$random.= pack('n', mt_rand(0, 0xFFFF));
}
}
return new Math_BigInteger($random, 256);
}
/** /**
* Generate a random number * Generate a random number
* *
@ -3057,46 +3088,45 @@ class Math_BigInteger {
$min = $temp; $min = $temp;
} }
$max = $max->subtract($min); static $one;
$max = ltrim($max->toBytes(), chr(0)); if (!isset($one)) {
$size = strlen($max) - 1; $one = new Math_BigInteger(1);
$crypt_random = function_exists('crypt_random_string') || (!class_exists('Crypt_Random') && function_exists('crypt_random_string'));
if ($crypt_random) {
$random = crypt_random_string($size);
} else {
$random = '';
if ($size & 1) {
$random.= chr(mt_rand(0, 255));
} }
$blocks = $size >> 1; $max = $max->subtract($min->subtract($one));
for ($i = 0; $i < $blocks; ++$i) { $size = strlen(ltrim($max->toBytes(), chr(0)));
// mt_rand(-2147483648, 0x7FFFFFFF) always produces -2147483648 on some systems
$random.= pack('n', mt_rand(0, 0xFFFF));
}
}
$fragment = new Math_BigInteger($random, 256); /*
$leading = $fragment->compare(new Math_BigInteger(substr($max, 1), 256)) > 0 ? doing $random % $max doesn't work because some numbers will be more likely to occur than others.
ord($max[0]) - 1 : ord($max[0]); eg. if $max is 140 and $random's max is 255 then that'd mean both $random = 5 and $random = 145
would produce 5 whereas the only value of random that could produce 139 would be 139. ie.
not all numbers would be equally likely. some would be more likely than others.
if (!$crypt_random) { creating a whole new random number until you find one that is within the range doesn't work
$msb = chr(mt_rand(0, $leading)); because, for sufficiently small ranges, the likelihood that you'd get a number within that range
} else { would be pretty small. eg. with $random's max being 255 and if your $max being 1 the probability
$cutoff = floor(0xFF / $leading) * $leading; would be pretty high that $random would be greater than $max.
while (true) {
$msb = ord(crypt_random_string(1));
if ($msb <= $cutoff) {
$msb%= $leading;
break;
}
}
$msb = chr($msb);
}
$random = new Math_BigInteger($msb . $random, 256); phpseclib works around this using the technique described here:
http://crypto.stackexchange.com/questions/5708/creating-a-small-number-from-a-cryptographically-secure-random-string
*/
$random_max = new Math_BigInteger(chr(1) . str_repeat("\0", $size), 256);
$random = $this->_random_number_helper($size);
list($max_multiple) = $random_max->divide($max);
$max_multiple = $max_multiple->multiply($max);
while ($random->compare($max_multiple) >= 0) {
$random = $random->subtract($max_multiple);
$random_max = $random_max->subtract($max_multiple);
$random = $random->bitwise_leftShift(8);
$random = $random->add($this->_random_number_helper(1));
$random_max = $random_max->bitwise_leftShift(8);
list($max_multiple) = $random_max->divide($max);
$max_multiple = $max_multiple->multiply($max);
}
list(, $random) = $random->divide($max);
return $this->_normalize($random->add($min)); return $this->_normalize($random->add($min));
} }