From 2bccef8b365f6c27e6ce1444cfbad42722bddcad Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Wed, 26 Oct 2016 12:30:28 -0700
Subject: [PATCH] Issue #2763787 by pwolanin, nerdstein, rlhawk, YesCT,
 slasher13, tuutti, alexpott, therealssj, TravisCarden, dawehner,
 maximpodorov, klausi, catch, talhaparacha, Eric_A: Upgrade random_compat to
 latest version

---
 composer.lock                                 | 10 +--
 core/composer.json                            |  2 +-
 core/lib/Drupal/Component/Utility/Crypt.php   | 41 ++++++++++-
 .../Drupal/Component/Utility/composer.json    |  2 +-
 core/modules/system/system.install            | 38 ++++++++++
 .../Utility/CryptRandomFallbackTest.php       | 71 +++++++++++++++++++
 .../Tests/Component/Utility/CryptTest.php     |  2 +
 7 files changed, 157 insertions(+), 9 deletions(-)
 create mode 100644 core/tests/Drupal/Tests/Component/Utility/CryptRandomFallbackTest.php

diff --git a/composer.lock b/composer.lock
index 832979fc30d1..2ecd5c2e1721 100644
--- a/composer.lock
+++ b/composer.lock
@@ -999,16 +999,16 @@
         },
         {
             "name": "paragonie/random_compat",
-            "version": "1.1.1",
+            "version": "v2.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/paragonie/random_compat.git",
-                "reference": "a208865a5aeffc2dbbef2a5b3409887272d93f32"
+                "reference": "088c04e2f261c33bed6ca5245491cfca69195ccf"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/paragonie/random_compat/zipball/a208865a5aeffc2dbbef2a5b3409887272d93f32",
-                "reference": "a208865a5aeffc2dbbef2a5b3409887272d93f32",
+                "url": "https://api.github.com/repos/paragonie/random_compat/zipball/088c04e2f261c33bed6ca5245491cfca69195ccf",
+                "reference": "088c04e2f261c33bed6ca5245491cfca69195ccf",
                 "shasum": ""
             },
             "require": {
@@ -1043,7 +1043,7 @@
                 "pseudorandom",
                 "random"
             ],
-            "time": "2015-12-01 02:52:15"
+            "time": "2016-04-03 06:00:07"
         },
         {
             "name": "psr/http-message",
diff --git a/core/composer.json b/core/composer.json
index 53ab8fa68aa3..ba488e5d2e97 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -31,7 +31,7 @@
         "symfony/psr-http-message-bridge": "v0.2",
         "zendframework/zend-diactoros": "~1.1",
         "composer/semver": "~1.0",
-        "paragonie/random_compat": "~1.0",
+        "paragonie/random_compat": "^1.0|^2.0",
         "asm89/stack-cors": "~1.0"
     },
     "require-dev": {
diff --git a/core/lib/Drupal/Component/Utility/Crypt.php b/core/lib/Drupal/Component/Utility/Crypt.php
index ace4ebda1a9a..6ebdc4aac831 100644
--- a/core/lib/Drupal/Component/Utility/Crypt.php
+++ b/core/lib/Drupal/Component/Utility/Crypt.php
@@ -19,7 +19,8 @@ class Crypt {
    *
    * In PHP 7 and up, this uses the built-in PHP function random_bytes().
    * In older PHP versions, this uses the random_bytes() function provided by
-   * the random_compat library.
+   * the random_compat library, or the fallback hash-based generator from Drupal
+   * 7.x.
    *
    * @param int $count
    *   The number of characters (bytes) to return in the string.
@@ -28,7 +29,43 @@ class Crypt {
    *   A randomly generated string.
    */
   public static function randomBytes($count) {
-    return random_bytes($count);
+    try {
+      return random_bytes($count);
+    }
+    catch (\Exception $e) {
+      // $random_state does not use drupal_static as it stores random bytes.
+      static $random_state, $bytes;
+      // If the compatibility library fails, this simple hash-based PRNG will
+      // generate a good set of pseudo-random bytes on any system.
+      // Note that it may be important that our $random_state is passed
+      // through hash() prior to being rolled into $output, that the two hash()
+      // invocations are different, and that the extra input into the first one
+      // - the microtime() - is prepended rather than appended. This is to avoid
+      // directly leaking $random_state via the $output stream, which could
+      // allow for trivial prediction of further "random" numbers.
+      if (strlen($bytes) < $count) {
+        // Initialize on the first call. The $_SERVER variable includes user and
+        // system-specific information that varies a little with each page.
+        if (!isset($random_state)) {
+          $random_state = print_r($_SERVER, TRUE);
+          if (function_exists('getmypid')) {
+            // Further initialize with the somewhat random PHP process ID.
+            $random_state .= getmypid();
+          }
+          $bytes = '';
+          // Ensure mt_rand() is reseeded before calling it the first time.
+          mt_srand();
+        }
+
+        do {
+          $random_state = hash('sha256', microtime() . mt_rand() . $random_state);
+          $bytes .= hash('sha256', mt_rand() . $random_state, TRUE);
+        } while (strlen($bytes) < $count);
+      }
+      $output = substr($bytes, 0, $count);
+      $bytes = substr($bytes, $count);
+      return $output;
+    }
   }
 
   /**
diff --git a/core/lib/Drupal/Component/Utility/composer.json b/core/lib/Drupal/Component/Utility/composer.json
index a23634facfd6..13671efef403 100644
--- a/core/lib/Drupal/Component/Utility/composer.json
+++ b/core/lib/Drupal/Component/Utility/composer.json
@@ -6,7 +6,7 @@
   "license": "GPL-2.0+",
   "require": {
     "php": ">=5.5.9",
-    "paragonie/random_compat": "~1.0",
+    "paragonie/random_compat": "^1.0|^2.0",
     "drupal/core-render": "~8.2"
   },
   "autoload": {
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index cbdafbc9f436..e8f45eb445ac 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -257,6 +257,44 @@ function system_requirements($phase) {
     $requirements['php_opcache']['title'] = t('PHP OPcode caching');
   }
 
+  if ($phase != 'update') {
+    // Test whether we have a good source of random bytes.
+    $requirements['php_random_bytes'] = array(
+      'title' => t('Random number generation'),
+    );
+    try {
+      $bytes = random_bytes(10);
+      if (strlen($bytes) != 10) {
+        throw new \Exception(t('Tried to generate 10 random bytes, generated @count', array('@count' => strlen($bytes))));
+      }
+      $requirements['php_random_bytes']['value'] = t('Successful');
+    }
+    catch (\Exception $e) {
+      // If /dev/urandom is not available on a UNIX-like system, check whether
+      // open_basedir restrictions are the cause.
+      $open_basedir_blocks_urandom = FALSE;
+      if (DIRECTORY_SEPARATOR === '/' && !@is_readable('/dev/urandom')) {
+        $open_basedir = ini_get('open_basedir');
+        if ($open_basedir) {
+          $open_basedir_paths = explode(PATH_SEPARATOR, $open_basedir);
+          $open_basedir_blocks_urandom = !array_intersect(array('/dev', '/dev/', '/dev/urandom'), $open_basedir_paths);
+        }
+      }
+      $args = array(
+        ':drupal-php' => 'https://www.drupal.org/docs/7/system-requirements/php#csprng',
+        '%exception_message' => $e->getMessage(),
+      );
+      if ($open_basedir_blocks_urandom) {
+        $requirements['php_random_bytes']['description'] = t('Drupal is unable to generate highly randomized numbers, which means certain security features like password reset URLs are not as secure as they should be. Instead, only a slow, less-secure fallback generator is available. The most likely cause is that open_basedir restrictions are in effect and /dev/urandom is not on the whitelist. See the <a href=":drupal-php">system requirements</a> page for more information. %exception_message', $args);
+      }
+      else {
+        $requirements['php_random_bytes']['description'] = t('Drupal is unable to generate highly randomized numbers, which means certain security features like password reset URLs are not as secure as they should be. Instead, only a slow, less-secure fallback generator is available. See the <a href=":drupal-php">system requirements</a> page for more information. %exception_message', $args);
+      }
+      $requirements['php_random_bytes']['value'] = t('Less secure');
+      $requirements['php_random_bytes']['severity'] = REQUIREMENT_ERROR;
+    }
+  }
+
   if ($phase == 'install' || $phase == 'update') {
     // Test for PDO (database).
     $requirements['database_extensions'] = array(
diff --git a/core/tests/Drupal/Tests/Component/Utility/CryptRandomFallbackTest.php b/core/tests/Drupal/Tests/Component/Utility/CryptRandomFallbackTest.php
new file mode 100644
index 000000000000..de00da6a4b6f
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Utility/CryptRandomFallbackTest.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Drupal\Tests\Component\Utility;
+
+use Drupal\Tests\UnitTestCase;
+use Drupal\Component\Utility\Crypt;
+
+/**
+ * Tests random byte generation fallback exception situations.
+ *
+ * @group Utility
+ *
+ * @runTestsInSeparateProcesses
+ *
+ * @coversDefaultClass \Drupal\Component\Utility\Crypt
+ */
+class CryptRandomFallbackTest extends UnitTestCase {
+
+  static protected $functionCalled = 0;
+
+  /**
+   * Allows the test to confirm that the namespaced random_bytes() was called.
+   */
+  public static function functionCalled() {
+    static::$functionCalled++;
+  }
+
+  /**
+   * Tests random byte generation using the fallback generator.
+   *
+   * If the call to random_bytes() throws an exception, Crypt::random_bytes()
+   * should still return a useful string of random bytes.
+   *
+   * @covers ::randomBytes
+   *
+   * @see \Drupal\Tests\Component\Utility\CryptTest::testRandomBytes()
+   */
+  public function testRandomBytesFallback() {
+    // This loop is a copy of
+    // \Drupal\Tests\Component\Utility\CryptTest::testRandomBytes().
+    for ($i = 0; $i < 10; $i++) {
+      $count = rand(10, 10000);
+      // Check that different values are being generated.
+      $this->assertNotEquals(Crypt::randomBytes($count), Crypt::randomBytes($count));
+      // Check the length.
+      $this->assertEquals($count, strlen(Crypt::randomBytes($count)));
+    }
+    $this->assertEquals(30, static::$functionCalled, 'The namespaced function was called the expected number of times.');
+  }
+
+}
+
+namespace Drupal\Component\Utility;
+
+use  \Drupal\Tests\Component\Utility\CryptRandomFallbackTest;
+
+/**
+ * Defines a function in same namespace as Drupal\Component\Utility\Crypt.
+ *
+ * Forces throwing an exception in this test environment because the function
+ * in the namespace is used in preference to the global function.
+ *
+ * @param int $count
+ *   Matches the global function definition.
+ *
+ * @throws \Exception
+ */
+function random_bytes($count) {
+  CryptRandomFallbackTest::functionCalled();
+  throw new \Exception($count);
+}
diff --git a/core/tests/Drupal/Tests/Component/Utility/CryptTest.php b/core/tests/Drupal/Tests/Component/Utility/CryptTest.php
index ff1e6ccfbfb0..42d6015158a4 100644
--- a/core/tests/Drupal/Tests/Component/Utility/CryptTest.php
+++ b/core/tests/Drupal/Tests/Component/Utility/CryptTest.php
@@ -18,6 +18,8 @@ class CryptTest extends UnitTestCase {
    * Tests random byte generation.
    *
    * @covers ::randomBytes
+   *
+   * @see \Drupal\Tests\Component\Utility\CryptRandomFallbackTest::testRandomBytesFallback
    */
   public function testRandomBytes() {
     for ($i = 1; $i < 10; $i++) {
-- 
GitLab