diff --git a/lib/moodlelib.php b/lib/moodlelib.php index a91cab451f4..15902d002c1 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -7558,14 +7558,16 @@ function count_letters($string) { * @param int $length The length of the string to be created. * @return string */ -function random_string ($length=15) { +function random_string($length=15) { + $randombytes = random_bytes_emulate($length); $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; $pool .= 'abcdefghijklmnopqrstuvwxyz'; $pool .= '0123456789'; $poollen = strlen($pool); $string = ''; for ($i = 0; $i < $length; $i++) { - $string .= substr($pool, (mt_rand()%($poollen)), 1); + $rand = ord($randombytes[$i]); + $string .= substr($pool, ($rand%($poollen)), 1); } return $string; } @@ -7586,13 +7588,56 @@ function complex_random_string($length=null) { if ($length===null) { $length = floor(rand(24, 32)); } + $randombytes = random_bytes_emulate($length); $string = ''; for ($i = 0; $i < $length; $i++) { - $string .= $pool[(mt_rand()%$poollen)]; + $rand = ord($randombytes[$i]); + $string .= $pool[($rand%$poollen)]; } return $string; } +/** + * Try to generates cryptographically secure pseudo-random bytes. + * + * Note this is achieved by fallbacking between: + * - PHP 7 random_bytes(). + * - OpenSSL openssl_random_pseudo_bytes(). + * - In house random generator getting its entropy from various, hard to guess, pseudo-random sources. + * + * @param int $length requested length in bytes + * @return string binary data + */ +function random_bytes_emulate($length) { + global $CFG; + if ($length <= 0) { + debugging('Invalid random bytes length', DEBUG_DEVELOPER); + return ''; + } + if (function_exists('random_bytes')) { + // Use PHP 7 goodness. + $hash = @random_bytes($length); + if ($hash !== false) { + return $hash; + } + } + if (function_exists('openssl_random_pseudo_bytes')) { + // For PHP 5.3 and later with openssl extension. + $hash = openssl_random_pseudo_bytes($length); + if ($hash !== false) { + return $hash; + } + } + + // Bad luck, there is no reliable random generator, let's just hash some unique stuff that is hard to guess. + $hash = sha1(serialize($CFG) . serialize($_SERVER) . microtime(true) . uniqid('', true), true); + // NOTE: the last param in sha1() is true, this means we are getting 20 bytes, not 40 chars as usual. + if ($length <= 20) { + return substr($hash, 0, $length); + } + return $hash . random_bytes_emulate($length - 20); +} + /** * Given some text (which may contain HTML) and an ideal length, * this function truncates the text neatly on a word boundary if possible diff --git a/lib/tests/moodlelib_test.php b/lib/tests/moodlelib_test.php index 8ee6dd2d803..78b2a2d16bd 100644 --- a/lib/tests/moodlelib_test.php +++ b/lib/tests/moodlelib_test.php @@ -2906,4 +2906,100 @@ class core_moodlelib_testcase extends advanced_testcase { $_SERVER['HTTP_X_FORWARDED_FOR'] = $xforwardedfor; } + + /* + * Test emulation of random_bytes() function. + */ + public function test_random_bytes_emulate() { + $result = random_bytes_emulate(10); + $this->assertSame(10, strlen($result)); + $this->assertnotSame($result, random_bytes_emulate(10)); + + $result = random_bytes_emulate(21); + $this->assertSame(21, strlen($result)); + $this->assertnotSame($result, random_bytes_emulate(21)); + + $result = random_bytes_emulate(666); + $this->assertSame(666, strlen($result)); + + $this->assertDebuggingNotCalled(); + + $result = random_bytes_emulate(0); + $this->assertSame('', $result); + $this->assertDebuggingCalled(); + + $result = random_bytes_emulate(-1); + $this->assertSame('', $result); + $this->assertDebuggingCalled(); + } + + /** + * Test function for creation of random strings. + */ + public function test_random_string() { + $pool = 'a-zA-Z0-9'; + + $result = random_string(10); + $this->assertSame(10, strlen($result)); + $this->assertRegExp('/^[' . $pool . ']+$/', $result); + $this->assertNotSame($result, random_string(10)); + + $result = random_string(21); + $this->assertSame(21, strlen($result)); + $this->assertRegExp('/^[' . $pool . ']+$/', $result); + $this->assertNotSame($result, random_string(21)); + + $result = random_string(666); + $this->assertSame(666, strlen($result)); + $this->assertRegExp('/^[' . $pool . ']+$/', $result); + + $result = random_string(); + $this->assertSame(15, strlen($result)); + $this->assertRegExp('/^[' . $pool . ']+$/', $result); + + $this->assertDebuggingNotCalled(); + + $result = random_string(0); + $this->assertSame('', $result); + $this->assertDebuggingCalled(); + + $result = random_string(-1); + $this->assertSame('', $result); + $this->assertDebuggingCalled(); + } + + /** + * Test function for creation of complex random strings. + */ + public function test_complex_random_string() { + $pool = preg_quote('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789`~!@#%^&*()_+-=[];,./<>?:{} ', '/'); + + $result = complex_random_string(10); + $this->assertSame(10, strlen($result)); + $this->assertRegExp('/^[' . $pool . ']+$/', $result); + $this->assertNotSame($result, complex_random_string(10)); + + $result = complex_random_string(21); + $this->assertSame(21, strlen($result)); + $this->assertRegExp('/^[' . $pool . ']+$/', $result); + $this->assertNotSame($result, complex_random_string(21)); + + $result = complex_random_string(666); + $this->assertSame(666, strlen($result)); + $this->assertRegExp('/^[' . $pool . ']+$/', $result); + + $result = complex_random_string(); + $this->assertEquals(28, strlen($result), '', 4); // Expected length is 24 - 32. + $this->assertRegExp('/^[' . $pool . ']+$/', $result); + + $this->assertDebuggingNotCalled(); + + $result = complex_random_string(0); + $this->assertSame('', $result); + $this->assertDebuggingCalled(); + + $result = complex_random_string(-1); + $this->assertSame('', $result); + $this->assertDebuggingCalled(); + } }