[BUGFIX] Restructure the random byte generator
authorHelmut Hummel <helmut.hummel@typo3.org>
Tue, 29 Mar 2011 22:18:47 +0000 (00:18 +0200)
committerSteffen Gebert <steffen.gebert@typo3.org>
Mon, 24 Oct 2011 19:38:17 +0000 (21:38 +0200)
Restructure the code to use the most performant methods first
if available. Take specialities of Windows OS and special
PHP versions into account.

Read/ generate more bytes than needed in one call, because it
does not cost (much) more to generate more random bytes, but it's
much cheaper for the next calls, because the bytes are already there.

Resolves: #23355
Releases: 4.6, 4.5, 4.4

Change-Id: I6bad300842f3da40c620b3d79b8116345a2749a0
Reviewed-on: http://review.typo3.org/4537
Reviewed-by: Helmut Hummel
Tested-by: Helmut Hummel
Reviewed-by: Steffen Ritter
Tested-by: Steffen Ritter
Reviewed-by: Steffen Gebert
Tested-by: Steffen Gebert
t3lib/class.t3lib_div.php
tests/t3lib/class.t3lib_divTest.php

index fd5fcb1..aa2d477 100644 (file)
@@ -1347,57 +1347,118 @@ final class t3lib_div {
        /**
         * Returns a string of highly randomized bytes (over the full 8-bit range).
         *
-        * @copyright Drupal CMS
-        * @license GNU General Public License version 2
-        * @param integer $count Number of characters (bytes) to return
+        * Note: Returned values are not guaranteed to be crypto-safe,
+        * most likely they are not, depending on the used retrieval method.
+        *
+        * @param integer $bytesToReturn Number of characters (bytes) to return
         * @return string Random Bytes
-        */
-       public static function generateRandomBytes($count) {
-               $output = '';
-                       // /dev/urandom is available on many *nix systems and is considered
-                       // the best commonly available pseudo-random source.
-               if (TYPO3_OS != 'WIN' && ($fh = @fopen('/dev/urandom', 'rb'))) {
-                       $output = fread($fh, $count);
-                       fclose($fh);
-               } elseif (TYPO3_OS == 'WIN') {
-                       if (class_exists('COM')) {
-                               try {
-                                       $com = new COM('CAPICOM.Utilities.1');
-                                       $output = base64_decode($com->GetRandom($count, 0));
-                               } catch (Exception $e) {
-                                       // CAPICOM not installed
+        * @see http://bugs.php.net/bug.php?id=52523
+        * @see http://www.php-security.org/2010/05/09/mops-submission-04-generating-unpredictable-session-ids-and-hashes/index.html
+        */
+       public static function generateRandomBytes($bytesToReturn) {
+                       // Cache 4k of the generated bytestream.
+               static $bytes = '';
+               $bytesToGenerate = max(4096, $bytesToReturn);
+
+                       // if we have not enough random bytes cached, we generate new ones
+               if (!isset($bytes{$bytesToReturn - 1})) {
+                       if (TYPO3_OS === 'WIN') {
+                                       // Openssl seems to be deadly slow on Windows, so try to use mcrypt
+                                       // Windows PHP versions have a bug when using urandom source (see #24410)
+                               $bytes .= self::generateRandomBytesMcrypt($bytesToGenerate, MCRYPT_RAND);
+                       } else {
+                                       // Try to use native PHP functions first, precedence has openssl
+                               $bytes .= self::generateRandomBytesOpenSsl($bytesToGenerate);
+
+                               if (!isset($bytes{$bytesToReturn - 1})) {
+                                       $bytes .= self::generateRandomBytesMcrypt($bytesToGenerate, MCRYPT_DEV_URANDOM);
                                }
-                       }
-                       if ($output === '') {
-                               if (function_exists('mcrypt_create_iv')) {
-                                       $output = mcrypt_create_iv($count, MCRYPT_DEV_URANDOM);
-                               } elseif (function_exists('openssl_random_pseudo_bytes')) {
-                                       $isStrong = NULL;
-                                       $output = openssl_random_pseudo_bytes($count, $isStrong);
-                                               // skip ssl since it wasn't using the strong algo
-                                       if ($isStrong !== TRUE) {
-                                               $output = '';
-                                       }
+
+                                       // If openssl and mcrypt failed, try /dev/urandom
+                               if (!isset($bytes{$bytesToReturn - 1})) {
+                                       $bytes .= self::generateRandomBytesUrandom($bytesToGenerate);
                                }
                        }
-               }
 
-                       // fallback if other random byte generation failed until now
-               if (!isset($output{$count - 1})) {
-                               // We initialize with the somewhat random.
-                       $randomState = $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']
-                                       . base_convert(memory_get_usage() % pow(10, 6), 10, 2)
-                                       . microtime() . uniqid('') . getmypid();
-                       while (!isset($output{$count - 1})) {
-                               $randomState = sha1(microtime() . mt_rand() . $randomState);
-                               $output .= sha1(mt_rand() . $randomState, TRUE);
+                               // Fall back if other random byte generation failed until now
+                       if (!isset($bytes{$bytesToReturn - 1})) {
+                               $bytes .= self::generateRandomBytesFallback($bytesToReturn);
                        }
-                       $output = substr($output, strlen($output) - $count, $count);
                }
+
+                       // get first $bytesToReturn and remove it from the byte cache
+               $output = substr($bytes, 0, $bytesToReturn);
+               $bytes = substr($bytes, $bytesToReturn);
+
                return $output;
        }
 
        /**
+        * Generate random bytes using openssl if available
+        *
+        * @param string $bytesToGenerate
+        * @return string
+        */
+       protected static function generateRandomBytesOpenSsl($bytesToGenerate) {
+               if (!function_exists('openssl_random_pseudo_bytes')) {
+                       return '';
+               }
+               $isStrong = NULL;
+               return (string) openssl_random_pseudo_bytes($bytesToGenerate, $isStrong);
+       }
+
+       /**
+        * Generate random bytes using mcrypt if available
+        *
+        * @param $bytesToGenerate
+        * @param $randomSource
+        * @return string
+        */
+       protected static function generateRandomBytesMcrypt($bytesToGenerate, $randomSource) {
+               if (!function_exists('mcrypt_create_iv')) {
+                       return '';
+               }
+               return (string) @mcrypt_create_iv($bytesToGenerate, $randomSource);
+       }
+
+       /**
+        * Read random bytes from /dev/urandom if it is accessible
+        *
+        * @param $bytesToGenerate
+        * @return string
+        */
+       protected static function generateRandomBytesUrandom($bytesToGenerate) {
+               $bytes = '';
+               $fh = @fopen('/dev/urandom', 'rb');
+               if ($fh) {
+                               // PHP only performs buffered reads, so in reality it will always read
+                               // at least 4096 bytes. Thus, it costs nothing extra to read and store
+                               // that much so as to speed any additional invocations.
+                       $bytes = fread($fh, $bytesToGenerate);
+                       fclose($fh);
+               }
+
+               return $bytes;
+       }
+
+       /**
+        * Generate pseudo random bytes as last resort
+        *
+        * @param $bytesToReturn
+        * @return string
+        */
+       protected static function generateRandomBytesFallback($bytesToReturn) {
+               $bytes = '';
+                       // We initialize with somewhat random.
+               $randomState = $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] . base_convert(memory_get_usage() % pow(10, 6), 10, 2) . microtime() . uniqid('') . getmypid();
+               while (!isset($bytes{$bytesToReturn - 1})) {
+                       $randomState = sha1(microtime() . mt_rand() . $randomState);
+                       $bytes .= sha1(mt_rand() . $randomState, TRUE);
+               }
+               return $bytes;
+       }
+
+       /**
         * Returns a hex representation of a random byte string.
         *
         * @param integer $count Number of hex characters to return
@@ -5730,4 +5791,4 @@ final class t3lib_div {
        }
 }
 
-?>
\ No newline at end of file
+?>
index 3c772b8..101ac44 100644 (file)
@@ -3438,5 +3438,67 @@ class t3lib_divTest extends tx_phpunit_testcase {
                        t3lib_div::hasValidClassPrefix('customPrefix_foo', array('customPrefix_'))
                );
        }
+
+       /**
+        * @test
+        * @dataProvider generateRandomBytesReturnsExpectedAmountOfBytesDataProvider
+        * @param int $numberOfBytes Number of Bytes to generate
+        */
+       public function generateRandomBytesReturnsExpectedAmountOfBytes($numberOfBytes) {
+               $this->assertEquals(
+                       strlen(t3lib_div::generateRandomBytes($numberOfBytes)),
+                       $numberOfBytes
+               );
+       }
+
+       public function generateRandomBytesReturnsExpectedAmountOfBytesDataProvider() {
+               return array(
+                       array(1),
+                       array(2),
+                       array(3),
+                       array(4),
+                       array(7),
+                       array(8),
+                       array(31),
+                       array(32),
+                       array(100),
+                       array(102),
+                       array(4000),
+                       array(4095),
+                       array(4096),
+                       array(4097),
+                       array(8000),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider generateRandomBytesReturnsDifferentBytesDuringDifferentCallsDataProvider
+        * @param int $numberOfBytes  Number of Bytes to generate
+        */
+       public function generateRandomBytesReturnsDifferentBytesDuringDifferentCalls($numberOfBytes) {
+               $results = array();
+               $numberOfTests = 5;
+
+                       // generate a few random numbers
+               for ($i = 0; $i < $numberOfTests; $i++) {
+                       $results[$i] = t3lib_div::generateRandomBytes($numberOfBytes);
+               }
+
+                       // array_unique would filter out duplicates
+               $this->assertEquals(
+                       $results,
+                       array_unique($results)
+               );
+       }
+
+       public function generateRandomBytesReturnsDifferentBytesDuringDifferentCallsDataProvider() {
+               return array(
+                       array(32),
+                       array(128),
+                       array(4096)
+               );
+       }
+
 }
 ?>
\ No newline at end of file