diff --git a/library/Zend/Captcha/Word.php b/library/Zend/Captcha/Word.php index 2df6554981..80d8aecb57 100644 --- a/library/Zend/Captcha/Word.php +++ b/library/Zend/Captcha/Word.php @@ -22,6 +22,9 @@ /** @see Zend_Captcha_Base */ require_once 'Zend/Captcha/Base.php'; +/** @see Zend_Crypt_Math */ +require_once 'Zend/Crypt/Math.php'; + /** * Word-based captcha adapter * @@ -39,10 +42,10 @@ abstract class Zend_Captcha_Word extends Zend_Captcha_Base /**#@+ * @var array Character sets */ - static $V = array("a", "e", "i", "o", "u", "y"); - static $VN = array("a", "e", "i", "o", "u", "y","2","3","4","5","6","7","8","9"); - static $C = array("b","c","d","f","g","h","j","k","m","n","p","q","r","s","t","u","v","w","x","z"); - static $CN = array("b","c","d","f","g","h","j","k","m","n","p","q","r","s","t","u","v","w","x","z","2","3","4","5","6","7","8","9"); + static public $V = array("a", "e", "i", "o", "u", "y"); + static public $VN = array("a", "e", "i", "o", "u", "y","2","3","4","5","6","7","8","9"); + static public $C = array("b","c","d","f","g","h","j","k","m","n","p","q","r","s","t","u","v","w","x","z"); + static public $CN = array("b","c","d","f","g","h","j","k","m","n","p","q","r","s","t","u","v","w","x","z","2","3","4","5","6","7","8","9"); /**#@-*/ /** @@ -175,7 +178,7 @@ public function setWordlen($wordlen) * * @return string */ - public function getId () + public function getId() { if (null === $this->_id) { $this->_setId($this->_generateRandomId()); @@ -189,7 +192,7 @@ public function getId () * @param string $id * @return Zend_Captcha_Word */ - protected function _setId ($id) + protected function _setId($id) { $this->_id = $id; return $this; @@ -250,7 +253,7 @@ public function setUseNumbers($_useNumbers) $this->_useNumbers = $_useNumbers; return $this; } - + /** * Get session object * @@ -280,7 +283,7 @@ public function getSession() public function setSession(Zend_Session_Namespace $session) { $this->_session = $session; - if($session) { + if ($session) { $this->_keepSession = true; } return $this; @@ -326,10 +329,12 @@ protected function _generateWord() $vowels = $this->_useNumbers ? self::$VN : self::$V; $consonants = $this->_useNumbers ? self::$CN : self::$C; + $totIndexCon = count($consonants) - 1; + $totIndexVow = count($vowels) - 1; for ($i=0; $i < $wordLen; $i = $i + 2) { // generate word with mix of vowels and consonants - $consonant = $consonants[array_rand($consonants)]; - $vowel = $vowels[array_rand($vowels)]; + $consonant = $consonants[Zend_Crypt_Math::randInteger(0, $totIndexCon, true)]; + $vowel = $vowels[Zend_Crypt_Math::randInteger(0, $totIndexVow, true)]; $word .= $consonant . $vowel; } @@ -347,7 +352,7 @@ protected function _generateWord() */ public function generate() { - if(!$this->_keepSession) { + if (!$this->_keepSession) { $this->_session = null; } $id = $this->_generateRandomId(); @@ -359,7 +364,7 @@ public function generate() protected function _generateRandomId() { - return md5(mt_rand(0, 1000) . microtime(true)); + return md5(Zend_Crypt_Math::randBytes(32)); } /** diff --git a/library/Zend/Crypt/Math.php b/library/Zend/Crypt/Math.php index e6b8b5796c..b1c65e6067 100644 --- a/library/Zend/Crypt/Math.php +++ b/library/Zend/Crypt/Math.php @@ -57,13 +57,100 @@ public function rand($minimum, $maximum) } $rand = ''; $i2 = strlen($maximum) - 1; - for ($i = 1;$i < $i2;$i++) { - $rand .= mt_rand(0,9); + for ($i = 1; $i < $i2; $i++) { + $rand .= mt_rand(0, 9); } - $rand .= mt_rand(0,9); + $rand .= mt_rand(0, 9); return $rand; } + /** + * Return a random strings of $length bytes + * + * @param integer $length + * @param boolean $strong + * @return string + */ + public static function randBytes($length, $strong = false) + { + $length = (int) $length; + if ($length <= 0) { + return false; + } + if (function_exists('openssl_random_pseudo_bytes')) { + $bytes = openssl_random_pseudo_bytes($length, $usable); + if ($strong === $usable) { + return $bytes; + } + } + if (function_exists('mcrypt_create_iv')) { + $bytes = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM); + if ($bytes !== false && strlen($bytes) === $length) { + return $bytes; + } + } + if (file_exists('/dev/urandom') && is_readable('/dev/urandom')) { + $frandom = fopen('/dev/urandom', 'r'); + if ($frandom !== false) { + return fread($frandom, $length); + } + } + if (true === $strong) { + require_once 'Zend/Crypt/Exception.php'; + throw new Zend_Crypt_Exception( + 'This PHP environment doesn\'t support secure random number generation. ' . + 'Please consider installing the OpenSSL and/or Mcrypt extensions' + ); + } + $rand = ''; + for ($i = 0; $i < $length; $i++) { + $rand .= chr(mt_rand(0, 255)); + } + return $rand; + } + + /** + * Return a random integer between $min and $max + * + * @param integer $min + * @param integer $max + * @param boolean $strong + * @return integer + */ + public static function randInteger($min, $max, $strong = false) + { + if ($min > $max) { + require_once 'Zend/Crypt/Exception.php'; + throw new Zend_Crypt_Exception( + 'The min parameter must be lower than max parameter' + ); + } + $range = $max - $min; + if ($range == 0) { + return $max; + } elseif ($range > PHP_INT_MAX || is_float($range)) { + require_once 'Zend/Crypt/Exception.php'; + throw new Zend_Crypt_Exception( + 'The supplied range is too great to generate' + ); + } + // calculate number of bits required to store range on this machine + $r = $range; + $bits = 0; + while ($r) { + $bits++; + $r >>= 1; + } + $bits = (int) max($bits, 1); + $bytes = (int) max(ceil($bits / 8), 1); + $filter = (int) ((1 << $bits) - 1); + do { + $rnd = hexdec(bin2hex(self::randBytes($bytes, $strong))); + $rnd &= $filter; + } while ($rnd > $range); + return ($min + $rnd); + } + /** * Get the big endian two's complement of a given big integer in * binary notation @@ -71,7 +158,8 @@ public function rand($minimum, $maximum) * @param string $long * @return string */ - public function btwoc($long) { + public function btwoc($long) + { if (ord($long[0]) > 127) { return "\x00" . $long; } @@ -84,7 +172,8 @@ public function btwoc($long) { * @param string $binary * @return string */ - public function fromBinary($binary) { + public function fromBinary($binary) + { return $this->_math->binaryToInteger($binary); } @@ -98,5 +187,4 @@ public function toBinary($integer) { return $this->_math->integerToBinary($integer); } - } diff --git a/tests/Zend/Crypt/MathTest.php b/tests/Zend/Crypt/MathTest.php index 4a8b1639d1..53c2ee4991 100644 --- a/tests/Zend/Crypt/MathTest.php +++ b/tests/Zend/Crypt/MathTest.php @@ -21,7 +21,7 @@ */ require_once 'Zend/Crypt/Math.php'; - +require_once 'Zend/Crypt/Exception.php'; /** * @category Zend @@ -36,8 +36,7 @@ class Zend_Crypt_MathTest extends PHPUnit_Framework_TestCase public function testRand() { - if (!extension_loaded('bcmath')) - { + if (!extension_loaded('bcmath')) { $this->markTestSkipped('Extension bcmath not loaded'); } @@ -59,4 +58,74 @@ public function testRand() $this->assertTrue(bccomp($result, $lower) !== '-1'); } + public function testRandBytes() + { + for ($length = 1; $length < 4096; $length++) { + $rand = Zend_Crypt_Math::randBytes($length); + $this->assertTrue(false !== $rand); + $this->assertEquals($length, strlen($rand)); + } + } + + public function testRandInteger() + { + for ($i = 0; $i < 1024; $i++) { + $min = rand(1, PHP_INT_MAX/2); + $max = $min + rand(1, PHP_INT_MAX/2 - 1); + $rand = Zend_Crypt_Math::randInteger($min, $max); + $this->assertGreaterThanOrEqual($min, $rand); + $this->assertLessThanOrEqual($max, $rand); + } + } + + public static function provideRandInt() + { + return [ + [2, 1, 10000, 100, 0.9, 1.1, false], + [2, 1, 10000, 100, 0.8, 1.2, true] + ]; + } + + /** + * A Monte Carlo test that generates $cycles numbers from 0 to $tot + * and test if the numbers are above or below the line y=x with a + * frequency range of [$min, $max] + * + * @dataProvider provideRandInt + */ + public function testMontecarloRandInteger($num, $valid, $cycles, $tot, $min, $max, $strong) + { + try { + $test = Zend_Crypt_Math::randBytes(1, $strong); + } catch (Zend_Crypt_Exception $e) { + $this->markTestSkipped($e->getMessage()); + } + + $i = 0; + $count = 0; + do { + $up = 0; + $down = 0; + for ($i = 0; $i < $cycles; $i++) { + $x = Zend_Crypt_Math::randInteger(0, $tot, $strong); + $y = Zend_Crypt_Math::randInteger(0, $tot, $strong); + if ($x > $y) { + $up++; + } elseif ($x < $y) { + $down++; + } + } + $this->assertGreaterThan(0, $up); + $this->assertGreaterThan(0, $down); + $ratio = $up / $down; + if ($ratio > $min && $ratio < $max) { + $count++; + } + $i++; + } while ($i < $num && $count < $valid); + + if ($count < $valid) { + $this->fail('The random number generator failed the Monte Carlo test'); + } + } }