diff --git a/core/composer.json b/core/composer.json index 0a6874f..484718e 100644 --- a/core/composer.json +++ b/core/composer.json @@ -33,7 +33,8 @@ "behat/mink": "~1.6", "behat/mink-goutte-driver": "~1.1", "fabpot/goutte": "^2.0.3", - "masterminds/html5": "~2.1" + "masterminds/html5": "~2.1", + "ircmaxell/password-compat": "~1.0" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/core/composer.lock b/core/composer.lock index 5363898..7e88f46 100644 --- a/core/composer.lock +++ b/core/composer.lock @@ -941,6 +941,48 @@ "time": "2014-10-12 19:18:40" }, { + "name": "ircmaxell/password-compat", + "version": "v1.0.4", + "source": { + "type": "git", + "url": "https://github.com/ircmaxell/password_compat.git", + "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ircmaxell/password_compat/zipball/5c5cde8822a69545767f7c7f3058cb15ff84614c", + "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "4.*" + }, + "type": "library", + "autoload": { + "files": [ + "lib/password.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anthony Ferrara", + "email": "ircmaxell@php.net", + "homepage": "http://blog.ircmaxell.com" + } + ], + "description": "A compatibility library for the proposed simplified password hashing algorithm: https://wiki.php.net/rfc/password_hash", + "homepage": "https://github.com/ircmaxell/password_compat", + "keywords": [ + "hashing", + "password" + ], + "time": "2014-11-20 16:49:30" + }, + { "name": "masterminds/html5", "version": "2.1.0", "source": { diff --git a/core/core.services.yml b/core/core.services.yml index 7b41a1d..edcfb71 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -7,6 +7,7 @@ parameters: default: keyvalue.database factory.keyvalue.expirable: default: keyvalue.expirable.database + password_hash_cost: 10 services: # Simple cache contexts, directly derived from the request context. cache_context.ip: @@ -749,15 +750,16 @@ services: class: Drupal\Core\Path\PathValidator arguments: ['@router', '@router.no_access_checks', '@current_user', '@path_processor_manager'] -# The argument to the hashing service defined in services.yml, to the -# constructor of PhpassHashedPassword is the log2 number of iterations for -# password stretching. -# @todo increase by 1 every Drupal version in order to counteract increases in -# the speed and power of computers available to crack the hashes. The current -# password hashing method was introduced in Drupal 7 with a log2 count of 15. + # The first argument of the hashing service (constructor of PhpPassword) is + # the 'cost' option of password_hash(). In Drupal 8 the 'cost' has the default + # value used by password_hash(), which is 10. Future versions of Drupal may + # increase this value in order to counteract increases in the speed and power + # of computers available to crack the hashes. Note that an increase of 1 will + # double the time needed for password hashing. password: - class: Drupal\Core\Password\PhpassHashedPassword - arguments: [16] + class: Drupal\Core\Password\MultiFormatPassword + arguments: ['%password_hash_cost%'] + lazy: true accept_header_matcher: class: Drupal\Core\Routing\AcceptHeaderMatcher tags: diff --git a/core/lib/Drupal/Core/Password/MultiFormatPassword.php b/core/lib/Drupal/Core/Password/MultiFormatPassword.php new file mode 100644 index 0000000..700a522 --- /dev/null +++ b/core/lib/Drupal/Core/Password/MultiFormatPassword.php @@ -0,0 +1,79 @@ += 5.5.0) password_hash() function. + * + * @var int + * + * @see password_hash(). + * @see http://php.net/manual/en/ref.password.php + */ + protected $cost; + + /** + * The PHP password hashing instance. + * + * @var \Drupal\Core\Password\PHPPassword + */ + protected $phpPassword; + + /** + * Constructs a new password hashing instance. + * + * @param int $cost + * The algorithmic cost that should be used. + */ + function __construct($cost) { + $this->cost = $cost; + $this->phpPassword = new PHPPassword($cost); + } + + /** + * {@inheritdoc} + */ + public function hash($password) { + return $this->phpPassword->hash($password); + } + + /** + * {@inheritdoc} + */ + public function check($password, $hash) { + if (substr($hash, 0, 2) == 'U$') { + // A migrated password from Drupal 6. + $hash = substr($hash, 1); + $password = md5($password); + } + + switch (substr($hash, 0, 3)) { + case '$S$': + // A Drupal 7 password using sha512. + return PhpassHashedPassword::check('sha512', $password, $hash); + case '$H$': + // phpBB3 uses "$H$" for the same thing as "$P$". + case '$P$': + // A phpass password generated using md5. This is an + // imported password or from an earlier Drupal version. + return PhpassHashedPassword::check('md5', $password, $hash); + default: + return $this->phpPassword->check($password, $hash); + } + } + + /** + * {@inheritdoc} + */ + public function needsRehash($hash) { + return $this->phpPassword->needsRehash($hash); + } + +} diff --git a/core/lib/Drupal/Core/Password/PasswordInterface.php b/core/lib/Drupal/Core/Password/PasswordInterface.php index 9726f69..17aa26d 100644 --- a/core/lib/Drupal/Core/Password/PasswordInterface.php +++ b/core/lib/Drupal/Core/Password/PasswordInterface.php @@ -2,48 +2,47 @@ /** * @file - * Definition of Drupal\Core\Password\PasswordInterface + * Contains \Drupal\Core\Password\PasswordInterface. */ namespace Drupal\Core\Password; -use Drupal\user\UserInterface; - /** * Secure password hashing functions for user authentication. */ interface PasswordInterface { /** + * Maximum password length. + */ + const PASSWORD_MAX_LENGTH = 512; + + /** * Hash a password using a secure hash. * * @param string $password * A plain-text password. * * @return string - * A string containing the hashed password (and a salt), or FALSE on failure. + * A string containing the hashed password, or FALSE on failure. */ public function hash($password); /** - * Check whether a plain text password matches a stored hashed password. - * - * Alternative implementations of this function may use other data in the - * $account object, for example the uid to look up the hash in a custom table - * or remote database. + * Check whether a plain text password matches a hashed password. * * @param string $password * A plain-text password - * @param \Drupal\user\UserInterface $account - * A user entity. + * @param string $hash + * A hashed password. * * @return bool * TRUE if the password is valid, FALSE if not. */ - public function check($password, UserInterface $account); + public function check($password, $hash); /** - * Check whether a user's hashed password needs to be replaced with a new hash. + * Check whether a hashed password needs to be replaced with a new hash. * * This is typically called during the login process when the plain text * password is available. A new hash is needed when the desired iteration @@ -52,15 +51,12 @@ public function check($password, UserInterface $account); * generated in an update like user_update_7000() (see the Drupal 7 * documentation). * - * Alternative implementations of this function might use other criteria based - * on the fields in $account. - * - * @param \Drupal\user\UserInterface $account - * A user entity. + * @param string $hash + * The existing hash to be checked. * - * @return boolean - * TRUE or FALSE. + * @return bool + * TRUE if the hash is outdated and needs rehash. */ - public function userNeedsNewHash(UserInterface $account); + public function needsRehash($hash); } diff --git a/core/lib/Drupal/Core/Password/PhpPassword.php b/core/lib/Drupal/Core/Password/PhpPassword.php new file mode 100644 index 0000000..05eadea --- /dev/null +++ b/core/lib/Drupal/Core/Password/PhpPassword.php @@ -0,0 +1,81 @@ +=5.5.0) password hashing + * functions. + * + * NOTE: Because password hashing functions are available only since PHP 5.5, on + * PHP 5.4 the compatibility is assured by the 'password_compat' library (see + * https://github.com/ircmaxell/password_compat). + * + * @see http://php.net/manual/en/ref.password.php + * @see https://github.com/ircmaxell/password_compat + * + * @todo Remove 'password_compat' library when Drupal will require PHP >= 5.5.0 + */ +class PhpPassword implements PasswordInterface { + + /** + * The algorithmic cost that should be used. This is the same 'cost' option as + * is used by the PHP (>= 5.5.0) password_hash() function. + * + * @var array + * + * @see password_hash(). + * @see http://php.net/manual/en/ref.password.php + */ + protected $options = []; + + /** + * Constructs a new password hashing instance. + * + * @param int $cost + * The algorithmic cost that should be used. + */ + function __construct($cost) { + $this->options = ['cost' => $cost]; + } + + /** + * {@inheritdoc} + */ + public function hash($password) { + // Prevent DoS attacks by refusing to hash large passwords. + if (strlen($password) > static::PASSWORD_MAX_LENGTH) { + return FALSE; + } + + // Use PASSWORD_BCRYPT since we want the same format generated for + // all Drupal 8.x installations, regardless of changes to the PHP default. + return password_hash($password, PASSWORD_BCRYPT, $this->options); + } + + /** + * {@inheritdoc} + */ + public function check($password, $hash) { + return password_verify($password, $hash); + } + + /** + * {@inheritdoc} + */ + public function needsRehash($hash) { + // The PHP 5.5 password_needs_rehash() will return TRUE in two cases: + // - The hash does not match the bcrypt signature, such as if this is a + // Drupal 7 password or a migrated Drupal 6 password. + // - The parameters of hashing engine were changed. For example the + // parameter 'password_hash_cost' (the hashing cost) has been increased in + // core.services.yml. + return password_needs_rehash($hash, PASSWORD_BCRYPT, $this->options); + } + + +} diff --git a/core/lib/Drupal/Core/Password/PhpassHashedPassword.php b/core/lib/Drupal/Core/Password/PhpassHashedPassword.php index 17cafdd..15b8246 100644 --- a/core/lib/Drupal/Core/Password/PhpassHashedPassword.php +++ b/core/lib/Drupal/Core/Password/PhpassHashedPassword.php @@ -7,21 +7,13 @@ namespace Drupal\Core\Password; -use Drupal\Component\Utility\Crypt; -use Drupal\user\UserInterface; - /** - * Secure password hashing functions based on the Portable PHP password + * Check a password hash from Drupal 7 or from the Portable PHP password * hashing framework. * * @see http://www.openwall.com/phpass/ */ -class PhpassHashedPassword implements PasswordInterface { - /** - * The minimum allowed log2 number of iterations for password stretching. - */ - const MIN_HASH_COUNT = 7; - +class PhpassHashedPassword { /** * The maximum allowed log2 number of iterations for password stretching. */ @@ -33,30 +25,10 @@ class PhpassHashedPassword implements PasswordInterface { const HASH_LENGTH = 55; /** - * Returns a string for mapping an int to the corresponding base 64 character. + * A string for mapping an int to the corresponding base 64 character. */ - static $ITOA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + protected static $ITOA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; - /** - * Specifies the number of times the hashing function will be applied when - * generating new password hashes. The number of times is calculated by - * raising 2 to the power of the given value. - */ - protected $countLog2; - - /** - * Constructs a new phpass password hashing instance. - * - * @param int $countLog2 - * Password stretching iteration count. Specifies the number of times the - * hashing function will be applied when generating new password hashes. - * The number of times is calculated by raising 2 to the power of the given - * value. - */ - function __construct($countLog2) { - // Ensure that $countLog2 is within set bounds. - $this->countLog2 = $this->enforceLog2Boundaries($countLog2); - } /** * Encodes bytes into printable base 64 using the *nix standard from crypt(). @@ -69,7 +41,7 @@ function __construct($countLog2) { * @return String * Encoded string */ - protected function base64Encode($input, $count) { + protected static function base64Encode($input, $count) { $output = ''; $i = 0; do { @@ -96,48 +68,6 @@ protected function base64Encode($input, $count) { } /** - * Generates a random base 64-encoded salt prefixed with settings for the hash. - * - * Proper use of salts may defeat a number of attacks, including: - * - The ability to try candidate passwords against multiple hashes at once. - * - The ability to use pre-hashed lists of candidate passwords. - * - The ability to determine whether two users have the same (or different) - * password without actually having to guess one of the passwords. - * - * @return String - * A 12 character string containing the iteration count and a random salt. - */ - protected function generateSalt() { - $output = '$S$'; - // We encode the final log2 iteration count in base 64. - $output .= static::$ITOA64[$this->countLog2]; - // 6 bytes is the standard salt for a portable phpass hash. - $output .= $this->base64Encode(Crypt::randomBytes(6), 6); - return $output; - } - - /** - * Ensures that $count_log2 is within set bounds. - * - * @param Integer $count_log2 - * Integer that determines the number of iterations used in the hashing - * process. A larger value is more secure, but takes more time to complete. - * - * @return Integer - * Integer within set bounds that is closest to $count_log2. - */ - protected function enforceLog2Boundaries($count_log2) { - if ($count_log2 < static::MIN_HASH_COUNT) { - return static::MIN_HASH_COUNT; - } - elseif ($count_log2 > static::MAX_HASH_COUNT) { - return static::MAX_HASH_COUNT; - } - - return (int) $count_log2; - } - - /** * Hash a password using a secure stretched hash. * * By using a salt and repeated hashing the password is "stretched". Its @@ -158,23 +88,22 @@ protected function enforceLog2Boundaries($count_log2) { * A string containing the hashed password (and salt) or FALSE on failure. * The return string will be truncated at HASH_LENGTH characters max. */ - protected function crypt($algo, $password, $setting) { - // Prevent DoS attacks by refusing to hash large passwords. + protected static function crypt($algo, $password, $setting) { + // Prevent DoS attacks by refusing to hash large passwords. This value is + // the same as PasswordInterface::PASSWORD_MAX_LENGTH if (strlen($password) > 512) { return FALSE; } // The first 12 characters of an existing hash are its setting string. $setting = substr($setting, 0, 12); - if ($setting[0] != '$' || $setting[2] != '$') { return FALSE; } - $count_log2 = $this->getCountLog2($setting); + $count_log2 = static::getCountLog2($setting); // Stored hashes may have been crypted with any iteration count. However we - // do not allow applying the algorithm for unreasonable low and high values - // respectively. - if ($count_log2 != $this->enforceLog2Boundaries($count_log2)) { + // do not allow applying the algorithm for unreasonably high values. + if ($count_log2 > static::MAX_HASH_COUNT) { return FALSE; } $salt = substr($setting, 4, 8); @@ -193,9 +122,9 @@ protected function crypt($algo, $password, $setting) { } while (--$count); $len = strlen($hash); - $output = $setting . $this->base64Encode($hash, $len); - // $this->base64Encode() of a 16 byte MD5 will always be 22 characters. - // $this->base64Encode() of a 64 byte sha512 will always be 86 characters. + $output = $setting . static::base64Encode($hash, $len); + // static::base64Encode() of a 16 byte MD5 will always be 22 characters. + // static::base64Encode() of a 64 byte sha512 will always be 86 characters. $expected = 12 + ceil((8 * $len) / 6); return (strlen($output) == $expected) ? substr($output, 0, static::HASH_LENGTH) : FALSE; } @@ -204,65 +133,28 @@ protected function crypt($algo, $password, $setting) { * Parse the log2 iteration count from a stored hash or setting string. * * @param String $setting - * An existing hash or the output of $this->generateSalt(). Must be - * at least 12 characters (the settings and salt). + * An existing hash. Must be at least 12 characters (the settings and salt). + * @return int */ - public function getCountLog2($setting) { + public static function getCountLog2($setting) { return strpos(static::$ITOA64, $setting[3]); } /** - * Implements Drupal\Core\Password\PasswordInterface::hash(). - */ - public function hash($password) { - return $this->crypt('sha512', $password, $this->generateSalt()); - } - - /** - * Implements Drupal\Core\Password\PasswordInterface::checkPassword(). - */ - public function check($password, UserInterface $account) { - if (substr($account->getPassword(), 0, 2) == 'U$') { - // This may be an updated password from user_update_7000(). Such hashes - // have 'U' added as the first character and need an extra md5() (see the - // Drupal 7 documentation). - $stored_hash = substr($account->getPassword(), 1); - $password = md5($password); - } - else { - $stored_hash = $account->getPassword(); - } - - $type = substr($stored_hash, 0, 3); - switch ($type) { - case '$S$': - // A normal Drupal 7 password using sha512. - $hash = $this->crypt('sha512', $password, $stored_hash); - break; - case '$H$': - // phpBB3 uses "$H$" for the same thing as "$P$". - case '$P$': - // A phpass password generated using md5. This is an - // imported password or from an earlier Drupal version. - $hash = $this->crypt('md5', $password, $stored_hash); - break; - default: - return FALSE; - } - return ($hash && $stored_hash == $hash); - } - - /** - * Implements Drupal\Core\Password\PasswordInterface::userNeedsNewHash(). + * Check a plain text password against a stored hash. + * + * @param String $algo + * The string name of a hashing algorithm usable by hash(), like 'sha256'. + * @param String $password + * Plain-text password up to 512 bytes (128 to 512 UTF-8 characters) to + * be checked. + * @param String $stored_hash + * An existing hash. Must be at least 12 characters (the settings and salt). + * + * @return boolean */ - public function userNeedsNewHash(UserInterface $account) { - // Check whether this was an updated password. - if ((substr($account->getPassword(), 0, 3) != '$S$') || (strlen($account->getPassword()) != static::HASH_LENGTH)) { - return TRUE; - } - // Ensure that $count_log2 is within set bounds. - $count_log2 = $this->enforceLog2Boundaries($this->countLog2); - // Check whether the iteration count used differs from the standard number. - return ($this->getCountLog2($account->getPassword()) !== $count_log2); + public static function check($algo, $password, $stored_hash) { + $hash = static::crypt($algo, $password, $stored_hash); + return ($hash && $stored_hash === $hash); } } diff --git a/core/modules/migrate/src/MigratePassword.php b/core/modules/migrate/src/MigratePassword.php index ba42e8a..6e69887 100644 --- a/core/modules/migrate/src/MigratePassword.php +++ b/core/modules/migrate/src/MigratePassword.php @@ -8,7 +8,7 @@ namespace Drupal\migrate; use Drupal\Core\Password\PasswordInterface; -use Drupal\user\UserInterface; +use Drupal\migrate\Entity\MigrationInterface; /** * Replaces the original 'password' service in order to prefix the MD5 re-hashed @@ -42,15 +42,15 @@ public function __construct(PasswordInterface $original_password) { /** * {@inheritdoc} */ - public function check($password, UserInterface $account) { - return $this->originalPassword->check($password, $account); + public function check($password, $hash) { + return $this->originalPassword->check($password, $hash); } /** * {@inheritdoc} */ - public function userNeedsNewHash(UserInterface $account) { - return $this->originalPassword->userNeedsNewHash($account); + public function needsRehash($hash) { + return $this->originalPassword->needsRehash($hash); } /** diff --git a/core/modules/migrate/src/MigrateServiceProvider.php b/core/modules/migrate/src/MigrateServiceProvider.php index 78a60bb..f1c1bd9 100644 --- a/core/modules/migrate/src/MigrateServiceProvider.php +++ b/core/modules/migrate/src/MigrateServiceProvider.php @@ -15,7 +15,7 @@ * user migrations that have passwords hashed to MD5. * * @see \Drupal\migrate\MigratePassword - * @see \Drupal\Core\Password\PhpassHashedPassword + * @see \Drupal\Core\Password\PhpPassword */ class MigrateServiceProvider implements ServiceModifierInterface { diff --git a/core/modules/migrate_drupal/src/Tests/d6/MigrateUserTest.php b/core/modules/migrate_drupal/src/Tests/d6/MigrateUserTest.php index 0e781ce..d7ebec0 100644 --- a/core/modules/migrate_drupal/src/Tests/d6/MigrateUserTest.php +++ b/core/modules/migrate_drupal/src/Tests/d6/MigrateUserTest.php @@ -156,6 +156,7 @@ public function testUser() { $roles[] = reset($role); } + /** @var \Drupal\user\UserInterface $user */ $user = User::load($source->uid); $this->assertIdentical($source->uid, $user->id()); $this->assertIdentical($source->name, $user->label()); @@ -183,7 +184,7 @@ public function testUser() { // Use the API to check if the password has been salted and re-hashed to // conform the Drupal >= 7. - $this->assertTrue(\Drupal::service('password')->check($source->pass_plain, $user)); + $this->assertTrue(\Drupal::service('password')->check($source->pass_plain, $user->getPassword())); } } diff --git a/core/modules/simpletest/src/KernelTestBase.php b/core/modules/simpletest/src/KernelTestBase.php index e8d1ba2..b259849 100644 --- a/core/modules/simpletest/src/KernelTestBase.php +++ b/core/modules/simpletest/src/KernelTestBase.php @@ -346,7 +346,7 @@ public function containerBuild(ContainerBuilder $container) { } if ($container->hasDefinition('password')) { - $container->getDefinition('password')->setArguments(array(1)); + $container->getDefinition('password')->setArguments([4]); } // Register the stream wrapper manager. diff --git a/core/modules/user/src/Entity/User.php b/core/modules/user/src/Entity/User.php index 05b642a..53155a0 100644 --- a/core/modules/user/src/Entity/User.php +++ b/core/modules/user/src/Entity/User.php @@ -393,7 +393,7 @@ public function setExistingPassword($password) { * {@inheritdoc} */ public function checkExistingPassword(UserInterface $account_unchanged) { - return !empty($this->get('pass')->existing) && \Drupal::service('password')->check(trim($this->get('pass')->existing), $account_unchanged); + return !empty($this->get('pass')->existing) && \Drupal::service('password')->check(trim($this->get('pass')->existing), $account_unchanged->getPassword()); } /** diff --git a/core/modules/user/src/Tests/UserLoginTest.php b/core/modules/user/src/Tests/UserLoginTest.php index 7981f26..92b566e 100644 --- a/core/modules/user/src/Tests/UserLoginTest.php +++ b/core/modules/user/src/Tests/UserLoginTest.php @@ -2,13 +2,13 @@ /** * @file - * Definition of Drupal\user\Tests\UserLoginTest. + * Contains \Drupal\user\Tests\UserLoginTest. */ namespace Drupal\user\Tests; use Drupal\simpletest\WebTestBase; -use Drupal\Core\Password\PhpassHashedPassword; +use Drupal\user\UserInterface; use Drupal\user\Entity\User; /** @@ -19,6 +19,13 @@ class UserLoginTest extends WebTestBase { /** + * Drupal password hasher service. + * + * @var \Drupal\Core\Password\PasswordInterface + */ + private $passwordHasher; + + /** * Tests login with destination. */ function testLoginCacheTagsAndDestination() { @@ -31,7 +38,9 @@ function testLoginCacheTagsAndDestination() { $this->drupalGet('user/login', array('query' => array('destination' => 'foo'))); $edit = array('name' => $user->getUserName(), 'pass' => $user->pass_raw); $this->drupalPostForm(NULL, $edit, t('Log in')); - $this->assertUrl('foo', [], 'Redirected to the correct URL'); + $this->assertUrl('foo', [], 'Redirected to the correct URL'); + + $this->passwordHasher = $this->container->get('password'); } /** @@ -112,30 +121,24 @@ function testPerUserLoginFloodControl() { } /** - * Test that user password is re-hashed upon login after changing $count_log2. + * Test that user password is re-hashed upon login after changing the cost. */ function testPasswordRehashOnLogin() { - // Determine default log2 for phpass hashing algorithm - $default_count_log2 = 16; - - // Retrieve instance of password hashing algorithm - $password_hasher = $this->container->get('password'); - // Create a new user and authenticate. $account = $this->drupalCreateUser(array()); $password = $account->pass_raw; $this->drupalLogin($account); $this->drupalLogout(); - // Load the stored user. The password hash should reflect $default_count_log2. + // Load the stored user. The password hash should reflect $default_cost. $user_storage = $this->container->get('entity.manager')->getStorage('user'); - $account = User::load($account->id()); - $this->assertIdentical($password_hasher->getCountLog2($account->getPassword()), $default_count_log2); + /** @var \Drupal\user\UserInterface $account */ + $account = $user_storage->load($account->id()); + $this->assertTrue($this->passwordHasher->check($password, $account->getPassword())); - // Change the required number of iterations by loading a test-module - // containing the necessary container builder code and then verify that the - // users password gets rehashed during the login. - $overridden_count_log2 = 19; - \Drupal::service('module_installer')->install(array('user_custom_phpass_params_test')); + // Change the required cost by loading a test-module containing the + // necessary container builder code and then verify that the users password + // gets rehashed during the login. + \Drupal::service('module_installer')->install(array('user_custom_pass_hash_params_test')); $this->resetAll(); $account->pass_raw = $password; @@ -143,7 +146,31 @@ function testPasswordRehashOnLogin() { // Load the stored user, which should have a different password hash now. $user_storage->resetCache(array($account->id())); $account = $user_storage->load($account->id()); - $this->assertIdentical($password_hasher->getCountLog2($account->getPassword()), $overridden_count_log2); + $this->assertTrue($this->passwordHasher->check($password, $account->getPassword())); + } + + /** + * Test MD5 (Drupal 6) passwords rehashing. + */ + public function testDrupal6MigratedPasswordRehashing() { + /** @var \Drupal\user\UserInterface $account */ + $account = $this->drupalCreateUser(); + $plain = $account->pass_raw; + + // We pretend that the user was migrated from Drupal 6. + $md5_pass = md5($plain); + $migrated_pass = 'U' . $this->passwordHasher->hash($md5_pass); + $this->storeHashedPassword($account, $migrated_pass); + + // User first login after migration. + $this->drupalLogin($account); + $this->drupalLogout(); + // Re-load the account object. + $account = User::load($account->id()); + + // After logging in the user password has been rehashed and is valid. + $this->assertFalse($this->passwordHasher->needsRehash($account->getPassword())); + $this->assertTrue($this->passwordHasher->check($plain, $account->getPassword())); } /** @@ -179,4 +206,23 @@ function assertFailedLogin($account, $flood_trigger = NULL) { $this->assertText(t('Sorry, unrecognized username or password. Have you forgotten your password?')); } } + + /** + * Updates the hashed user password bypassing the API. + * + * We want to set an already hashed password. + * + * @param \Drupal\user\UserInterface $account + * The user account. + * @param string $hashed_password + * An already hashed password. + */ + protected function storeHashedPassword(UserInterface $account, $hashed_password) { + $account->setPassword($hashed_password); + db_update('users_field_data') + ->fields(['pass' => $hashed_password]) + ->condition('uid', $account->id()) + ->execute(); + } + } diff --git a/core/modules/user/src/UserAuth.php b/core/modules/user/src/UserAuth.php index 625b444..0d04512 100644 --- a/core/modules/user/src/UserAuth.php +++ b/core/modules/user/src/UserAuth.php @@ -8,7 +8,6 @@ namespace Drupal\user; use Drupal\Core\Entity\EntityManagerInterface; -use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Password\PasswordInterface; /** @@ -24,7 +23,7 @@ class UserAuth implements UserAuthInterface { protected $entityManager; /** - * The password service. + * The password hashing service. * * @var \Drupal\Core\Password\PasswordInterface */ @@ -33,8 +32,8 @@ class UserAuth implements UserAuthInterface { /** * Constructs a UserAuth object. * - * @param \Drupal\Core\Entity\EntityStorageInterface $storage - * The user storage. + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager + * The entity manager. * @param \Drupal\Core\Password\PasswordInterface $password_checker * The password service. */ @@ -53,12 +52,12 @@ public function authenticate($username, $password) { $account_search = $this->entityManager->getStorage('user')->loadByProperties(array('name' => $username)); if ($account = reset($account_search)) { - if ($this->passwordChecker->check($password, $account)) { + if ($this->passwordChecker->check($password, $account->getPassword())) { // Successful authentication. $uid = $account->id(); // Update user to new password scheme if needed. - if ($this->passwordChecker->userNeedsNewHash($account)) { + if ($this->passwordChecker->needsRehash($account->getPassword())) { $account->setPassword($password); $account->save(); } diff --git a/core/modules/user/tests/modules/user_custom_pass_hash_params_test/user_custom_pass_hash_params_test.info.yml b/core/modules/user/tests/modules/user_custom_pass_hash_params_test/user_custom_pass_hash_params_test.info.yml new file mode 100644 index 0000000..aca50c4 --- /dev/null +++ b/core/modules/user/tests/modules/user_custom_pass_hash_params_test/user_custom_pass_hash_params_test.info.yml @@ -0,0 +1,6 @@ +name: 'User custom password hash params test' +type: module +description: 'Support module for testing custom hashing password algorithm parameters.' +package: Testing +version: VERSION +core: 8.x diff --git a/core/modules/user/tests/modules/user_custom_pass_hash_params_test/user_custom_pass_hash_params_test.services.yml b/core/modules/user/tests/modules/user_custom_pass_hash_params_test/user_custom_pass_hash_params_test.services.yml new file mode 100644 index 0000000..1c0a8c9 --- /dev/null +++ b/core/modules/user/tests/modules/user_custom_pass_hash_params_test/user_custom_pass_hash_params_test.services.yml @@ -0,0 +1,4 @@ +services: + password: + class: Drupal\Core\Password\PhpPassword + arguments: [9] diff --git a/core/modules/user/tests/modules/user_custom_phpass_params_test/user_custom_phpass_params_test.info.yml b/core/modules/user/tests/modules/user_custom_phpass_params_test/user_custom_phpass_params_test.info.yml deleted file mode 100644 index 68b8ff9..0000000 --- a/core/modules/user/tests/modules/user_custom_phpass_params_test/user_custom_phpass_params_test.info.yml +++ /dev/null @@ -1,6 +0,0 @@ -name: 'User custom phpass params test' -type: module -description: 'Support module for testing custom phpass password algorithm parameters.' -package: Testing -version: VERSION -core: 8.x diff --git a/core/modules/user/tests/modules/user_custom_phpass_params_test/user_custom_phpass_params_test.services.yml b/core/modules/user/tests/modules/user_custom_phpass_params_test/user_custom_phpass_params_test.services.yml deleted file mode 100644 index 8950bfe..0000000 --- a/core/modules/user/tests/modules/user_custom_phpass_params_test/user_custom_phpass_params_test.services.yml +++ /dev/null @@ -1,4 +0,0 @@ -services: - password: - class: Drupal\Core\Password\PhpassHashedPassword - arguments: [19] diff --git a/core/modules/user/tests/src/Unit/UserAuthTest.php b/core/modules/user/tests/src/Unit/UserAuthTest.php index d70824d..3699f15 100644 --- a/core/modules/user/tests/src/Unit/UserAuthTest.php +++ b/core/modules/user/tests/src/Unit/UserAuthTest.php @@ -52,7 +52,7 @@ class UserAuthTest extends UnitTestCase { protected $username = 'test_user'; /** - * The test password + * The test password. * * @var string */ @@ -74,7 +74,7 @@ protected function setUp() { $this->testUser = $this->getMockBuilder('Drupal\user\Entity\User') ->disableOriginalConstructor() - ->setMethods(array('id', 'setPassword', 'save')) + ->setMethods(array('id', 'setPassword', 'save', 'getPassword')) ->getMock(); $this->userAuth = new UserAuth($entity_manager, $this->passwordService); @@ -135,7 +135,7 @@ public function testAuthenticateWithIncorrectPassword() { $this->passwordService->expects($this->once()) ->method('check') - ->with($this->password, $this->testUser) + ->with($this->password, $this->testUser->getPassword()) ->will($this->returnValue(FALSE)); $this->assertFalse($this->userAuth->authenticate($this->username, $this->password)); @@ -158,7 +158,7 @@ public function testAuthenticateWithCorrectPassword() { $this->passwordService->expects($this->once()) ->method('check') - ->with($this->password, $this->testUser) + ->with($this->password, $this->testUser->getPassword()) ->will($this->returnValue(TRUE)); $this->assertsame(1, $this->userAuth->authenticate($this->username, $this->password)); @@ -186,11 +186,11 @@ public function testAuthenticateWithCorrectPasswordAndNewPasswordHash() { $this->passwordService->expects($this->once()) ->method('check') - ->with($this->password, $this->testUser) + ->with($this->password, $this->testUser->getPassword()) ->will($this->returnValue(TRUE)); $this->passwordService->expects($this->once()) - ->method('userNeedsNewHash') - ->with($this->testUser) + ->method('needsRehash') + ->with($this->testUser->getPassword()) ->will($this->returnValue(TRUE)); $this->assertsame(1, $this->userAuth->authenticate($this->username, $this->password)); diff --git a/core/tests/Drupal/Tests/Core/Password/PasswordHashingTest.php b/core/tests/Drupal/Tests/Core/Password/PasswordHashingTest.php deleted file mode 100644 index f304be0..0000000 --- a/core/tests/Drupal/Tests/Core/Password/PasswordHashingTest.php +++ /dev/null @@ -1,196 +0,0 @@ -user = $this->getMockBuilder('Drupal\user\Entity\User') - ->disableOriginalConstructor() - ->getMock(); - $this->passwordHasher = new PhpassHashedPassword(1); - } - - /** - * Tests the hash count boundaries are enforced. - * - * @covers ::enforceLog2Boundaries - */ - public function testWithinBounds() { - $hasher = new FakePhpassHashedPassword(); - $this->assertEquals(PhpassHashedPassword::MIN_HASH_COUNT, $hasher->enforceLog2Boundaries(1), "Min hash count enforced"); - $this->assertEquals(PhpassHashedPassword::MAX_HASH_COUNT, $hasher->enforceLog2Boundaries(100), "Max hash count enforced"); - } - - - /** - * Test a password needs update. - * - * @covers ::userNeedsNewHash - */ - public function testPasswordNeedsUpdate() { - $this->user->expects($this->any()) - ->method('getPassword') - ->will($this->returnValue($this->md5Password)); - // The md5 password should be flagged as needing an update. - $this->assertTrue($this->passwordHasher->userNeedsNewHash($this->user), 'User with md5 password needs a new hash.'); - } - - /** - * Test password hashing. - * - * @covers ::hash - * @covers ::getCountLog2 - * @covers ::check - * @covers ::userNeedsNewHash - */ - public function testPasswordHashing() { - $this->hashedPassword = $this->passwordHasher->hash($this->password); - $this->user->expects($this->any()) - ->method('getPassword') - ->will($this->returnValue($this->hashedPassword)); - $this->assertSame($this->passwordHasher->getCountLog2($this->hashedPassword), PhpassHashedPassword::MIN_HASH_COUNT, 'Hashed password has the minimum number of log2 iterations.'); - $this->assertNotEquals($this->hashedPassword, $this->md5Password, 'Password hash changed.'); - $this->assertTrue($this->passwordHasher->check($this->password, $this->user), 'Password check succeeds.'); - // Since the log2 setting hasn't changed and the user has a valid password, - // userNeedsNewHash() should return FALSE. - $this->assertFalse($this->passwordHasher->userNeedsNewHash($this->user), 'User does not need a new hash.'); - } - - /** - * Tests password rehashing. - * - * @covers ::hash - * @covers ::getCountLog2 - * @covers ::check - * @covers ::userNeedsNewHash - */ - public function testPasswordRehashing() { - - // Increment the log2 iteration to MIN + 1. - $this->passwordHasher = new PhpassHashedPassword(PhpassHashedPassword::MIN_HASH_COUNT + 1); - $this->assertTrue($this->passwordHasher->userNeedsNewHash($this->user), 'User needs a new hash after incrementing the log2 count.'); - // Re-hash the password. - $rehashed_password = $this->passwordHasher->hash($this->password); - - $this->user->expects($this->any()) - ->method('getPassword') - ->will($this->returnValue($rehashed_password)); - $this->assertSame($this->passwordHasher->getCountLog2($rehashed_password), PhpassHashedPassword::MIN_HASH_COUNT + 1, 'Re-hashed password has the correct number of log2 iterations.'); - $this->assertNotEquals($rehashed_password, $this->hashedPassword, 'Password hash changed again.'); - - // Now the hash should be OK. - $this->assertFalse($this->passwordHasher->userNeedsNewHash($this->user), 'Re-hashed password does not need a new hash.'); - $this->assertTrue($this->passwordHasher->check($this->password, $this->user), 'Password check succeeds with re-hashed password.'); - } - - /** - * Verifies that passwords longer than 512 bytes are not hashed. - * - * @covers ::crypt - * - * @dataProvider providerLongPasswords - */ - public function testLongPassword($password, $allowed) { - - $hashed_password = $this->passwordHasher->hash($password); - - if ($allowed) { - $this->assertNotFalse($hashed_password); - } - else { - $this->assertFalse($hashed_password); - } - } - - /** - * Provides the test matrix for testLongPassword(). - */ - public function providerLongPasswords() { - // '512 byte long password is allowed.' - $passwords['allowed'] = array(str_repeat('x', 512), TRUE); - // 513 byte long password is not allowed. - $passwords['too_long'] = array(str_repeat('x', 513), FALSE); - - // Check a string of 3-byte UTF-8 characters, 510 byte long password is - // allowed. - $passwords['utf8'] = array(str_repeat('€', 170), TRUE); - // 512 byte long password is allowed. - $passwords['ut8_extended'] = array($passwords['utf8'][0] . 'xx', TRUE); - - // Check a string of 3-byte UTF-8 characters, 513 byte long password is - // allowed. - $passwords['utf8_too_long'] = array(str_repeat('€', 171), FALSE); - return $passwords; - } - -} - -/** - * A fake class for tests. - */ -class FakePhpassHashedPassword extends PhpassHashedPassword { - - function __construct() { - // Noop. - } - - // Expose this method as public for tests. - public function enforceLog2Boundaries($count_log2) { - return parent::enforceLog2Boundaries($count_log2); - } - -} diff --git a/core/tests/Drupal/Tests/Core/Password/PhpPasswordTest.php b/core/tests/Drupal/Tests/Core/Password/PhpPasswordTest.php new file mode 100644 index 0000000..5663d7a --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Password/PhpPasswordTest.php @@ -0,0 +1,144 @@ +passwordHasher = new PhpPassword(4); + + $this->password = $this->randomMachineName(); + $this->md5HashedPassword = 'U' . $this->passwordHasher->hash(md5($this->password)); + } + + /** + * Test a password needs update. + * + * @covers ::needsRehash + */ + public function testPasswordNeedsUpdate() { + // The md5 password should be flagged as needing an update. + $this->assertTrue($this->passwordHasher->needsRehash($this->md5HashedPassword)); + } + + /** + * Test password hashing. + * + * @covers ::hash + * @covers ::needsRehash + */ + public function testPasswordHashing() { + $this->hashedPassword = $this->passwordHasher->hash($this->password); + $this->assertNotEquals($this->hashedPassword, $this->md5HashedPassword); + $this->assertTrue($this->passwordHasher->check($this->password, $this->hashedPassword)); + $this->assertFalse($this->passwordHasher->needsRehash($this->hashedPassword)); + } + + /** + * Tests password rehashing. + * + * @covers ::hash + * @covers ::needsRehash + */ + public function testPasswordRehashing() { + // Increment the cost from 4 to 5. + $this->passwordHasher = new PhpPassword(5); + $this->assertTrue($this->passwordHasher->needsRehash($this->hashedPassword)); + // Re-hash the password. + $rehashed_password = $this->passwordHasher->hash($this->password); + + $this->assertNotEquals($rehashed_password, $this->hashedPassword); + + // Now the hash should be OK. + $this->assertFalse($this->passwordHasher->needsRehash($rehashed_password)); + $this->assertTrue($this->passwordHasher->check($this->password, $rehashed_password)); + } + + /** + * Verifies that passwords longer than 512 bytes are not hashed. + * + * @covers ::hash + * + * @dataProvider providerLongPasswords + */ + public function testLongPassword($password, $allowed) { + + $hashed_password = $this->passwordHasher->hash($password); + + if ($allowed) { + $this->assertNotFalse($hashed_password); + } + else { + $this->assertFalse($hashed_password); + } + } + + /** + * Provides the test matrix for testLongPassword(). + */ + public function providerLongPasswords() { + // '512 byte long password is allowed.' + $passwords['allowed'] = array(str_repeat('x', 512), TRUE); + // 513 byte long password is not allowed. + $passwords['too_long'] = array(str_repeat('x', 513), FALSE); + + // Check a string of 3-byte UTF-8 characters, 510 byte long password is + // allowed. + $passwords['utf8'] = array(str_repeat('€', 170), TRUE); + // 512 byte long password is allowed. + $passwords['ut8_extended'] = array($passwords['utf8'][0] . 'xx', TRUE); + + // Check a string of 3-byte UTF-8 characters, 513 byte long password is + // allowed. + $passwords['utf8_too_long'] = array(str_repeat('€', 171), FALSE); + return $passwords; + } + +} diff --git a/core/vendor/composer/autoload_files.php b/core/vendor/composer/autoload_files.php index 853f9a0..d257539 100644 --- a/core/vendor/composer/autoload_files.php +++ b/core/vendor/composer/autoload_files.php @@ -7,5 +7,6 @@ return array( $vendorDir . '/react/promise/src/functions_include.php', + $vendorDir . '/ircmaxell/password-compat/lib/password.php', $baseDir . '/lib/Drupal.php', ); diff --git a/core/vendor/composer/installed.json b/core/vendor/composer/installed.json index 457c12e..3700362 100644 --- a/core/vendor/composer/installed.json +++ b/core/vendor/composer/installed.json @@ -3397,5 +3397,49 @@ ], "description": "Symfony BrowserKit Component", "homepage": "https://symfony.com" + }, + { + "name": "ircmaxell/password-compat", + "version": "v1.0.4", + "version_normalized": "1.0.4.0", + "source": { + "type": "git", + "url": "https://github.com/ircmaxell/password_compat.git", + "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ircmaxell/password_compat/zipball/5c5cde8822a69545767f7c7f3058cb15ff84614c", + "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "4.*" + }, + "time": "2014-11-20 16:49:30", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "lib/password.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anthony Ferrara", + "email": "ircmaxell@php.net", + "homepage": "http://blog.ircmaxell.com" + } + ], + "description": "A compatibility library for the proposed simplified password hashing algorithm: https://wiki.php.net/rfc/password_hash", + "homepage": "https://github.com/ircmaxell/password_compat", + "keywords": [ + "hashing", + "password" + ] } ] diff --git a/core/vendor/ircmaxell/password-compat/LICENSE.md b/core/vendor/ircmaxell/password-compat/LICENSE.md new file mode 100644 index 0000000..1efc565 --- /dev/null +++ b/core/vendor/ircmaxell/password-compat/LICENSE.md @@ -0,0 +1,7 @@ +Copyright (c) 2012 Anthony Ferrara + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/core/vendor/ircmaxell/password-compat/composer.json b/core/vendor/ircmaxell/password-compat/composer.json new file mode 100644 index 0000000..822fd1f --- /dev/null +++ b/core/vendor/ircmaxell/password-compat/composer.json @@ -0,0 +1,20 @@ +{ + "name": "ircmaxell/password-compat", + "description": "A compatibility library for the proposed simplified password hashing algorithm: https://wiki.php.net/rfc/password_hash", + "keywords": ["password", "hashing"], + "homepage": "https://github.com/ircmaxell/password_compat", + "license": "MIT", + "authors": [ + { + "name": "Anthony Ferrara", + "email": "ircmaxell@php.net", + "homepage": "http://blog.ircmaxell.com" + } + ], + "require-dev": { + "phpunit/phpunit": "4.*" + }, + "autoload": { + "files": ["lib/password.php"] + } +} diff --git a/core/vendor/ircmaxell/password-compat/lib/password.php b/core/vendor/ircmaxell/password-compat/lib/password.php new file mode 100644 index 0000000..cc6896c --- /dev/null +++ b/core/vendor/ircmaxell/password-compat/lib/password.php @@ -0,0 +1,314 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @copyright 2012 The Authors + */ + +namespace { + + if (!defined('PASSWORD_BCRYPT')) { + /** + * PHPUnit Process isolation caches constants, but not function declarations. + * So we need to check if the constants are defined separately from + * the functions to enable supporting process isolation in userland + * code. + */ + define('PASSWORD_BCRYPT', 1); + define('PASSWORD_DEFAULT', PASSWORD_BCRYPT); + define('PASSWORD_BCRYPT_DEFAULT_COST', 10); + } + + if (!function_exists('password_hash')) { + + /** + * Hash the password using the specified algorithm + * + * @param string $password The password to hash + * @param int $algo The algorithm to use (Defined by PASSWORD_* constants) + * @param array $options The options for the algorithm to use + * + * @return string|false The hashed password, or false on error. + */ + function password_hash($password, $algo, array $options = array()) { + if (!function_exists('crypt')) { + trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING); + return null; + } + if (is_null($password) || is_int($password)) { + $password = (string) $password; + } + if (!is_string($password)) { + trigger_error("password_hash(): Password must be a string", E_USER_WARNING); + return null; + } + if (!is_int($algo)) { + trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING); + return null; + } + $resultLength = 0; + switch ($algo) { + case PASSWORD_BCRYPT: + $cost = PASSWORD_BCRYPT_DEFAULT_COST; + if (isset($options['cost'])) { + $cost = $options['cost']; + if ($cost < 4 || $cost > 31) { + trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING); + return null; + } + } + // The length of salt to generate + $raw_salt_len = 16; + // The length required in the final serialization + $required_salt_len = 22; + $hash_format = sprintf("$2y$%02d$", $cost); + // The expected length of the final crypt() output + $resultLength = 60; + break; + default: + trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING); + return null; + } + $salt_requires_encoding = false; + if (isset($options['salt'])) { + switch (gettype($options['salt'])) { + case 'NULL': + case 'boolean': + case 'integer': + case 'double': + case 'string': + $salt = (string) $options['salt']; + break; + case 'object': + if (method_exists($options['salt'], '__tostring')) { + $salt = (string) $options['salt']; + break; + } + case 'array': + case 'resource': + default: + trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING); + return null; + } + if (PasswordCompat\binary\_strlen($salt) < $required_salt_len) { + trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", PasswordCompat\binary\_strlen($salt), $required_salt_len), E_USER_WARNING); + return null; + } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) { + $salt_requires_encoding = true; + } + } else { + $buffer = ''; + $buffer_valid = false; + if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) { + $buffer = mcrypt_create_iv($raw_salt_len, MCRYPT_DEV_URANDOM); + if ($buffer) { + $buffer_valid = true; + } + } + if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) { + $buffer = openssl_random_pseudo_bytes($raw_salt_len); + if ($buffer) { + $buffer_valid = true; + } + } + if (!$buffer_valid && @is_readable('/dev/urandom')) { + $f = fopen('/dev/urandom', 'r'); + $read = PasswordCompat\binary\_strlen($buffer); + while ($read < $raw_salt_len) { + $buffer .= fread($f, $raw_salt_len - $read); + $read = PasswordCompat\binary\_strlen($buffer); + } + fclose($f); + if ($read >= $raw_salt_len) { + $buffer_valid = true; + } + } + if (!$buffer_valid || PasswordCompat\binary\_strlen($buffer) < $raw_salt_len) { + $bl = PasswordCompat\binary\_strlen($buffer); + for ($i = 0; $i < $raw_salt_len; $i++) { + if ($i < $bl) { + $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255)); + } else { + $buffer .= chr(mt_rand(0, 255)); + } + } + } + $salt = $buffer; + $salt_requires_encoding = true; + } + if ($salt_requires_encoding) { + // encode string with the Base64 variant used by crypt + $base64_digits = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + $bcrypt64_digits = + './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + $base64_string = base64_encode($salt); + $salt = strtr(rtrim($base64_string, '='), $base64_digits, $bcrypt64_digits); + } + $salt = PasswordCompat\binary\_substr($salt, 0, $required_salt_len); + + $hash = $hash_format . $salt; + + $ret = crypt($password, $hash); + + if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != $resultLength) { + return false; + } + + return $ret; + } + + /** + * Get information about the password hash. Returns an array of the information + * that was used to generate the password hash. + * + * array( + * 'algo' => 1, + * 'algoName' => 'bcrypt', + * 'options' => array( + * 'cost' => PASSWORD_BCRYPT_DEFAULT_COST, + * ), + * ) + * + * @param string $hash The password hash to extract info from + * + * @return array The array of information about the hash. + */ + function password_get_info($hash) { + $return = array( + 'algo' => 0, + 'algoName' => 'unknown', + 'options' => array(), + ); + if (PasswordCompat\binary\_substr($hash, 0, 4) == '$2y$' && PasswordCompat\binary\_strlen($hash) == 60) { + $return['algo'] = PASSWORD_BCRYPT; + $return['algoName'] = 'bcrypt'; + list($cost) = sscanf($hash, "$2y$%d$"); + $return['options']['cost'] = $cost; + } + return $return; + } + + /** + * Determine if the password hash needs to be rehashed according to the options provided + * + * If the answer is true, after validating the password using password_verify, rehash it. + * + * @param string $hash The hash to test + * @param int $algo The algorithm used for new password hashes + * @param array $options The options array passed to password_hash + * + * @return boolean True if the password needs to be rehashed. + */ + function password_needs_rehash($hash, $algo, array $options = array()) { + $info = password_get_info($hash); + if ($info['algo'] != $algo) { + return true; + } + switch ($algo) { + case PASSWORD_BCRYPT: + $cost = isset($options['cost']) ? $options['cost'] : PASSWORD_BCRYPT_DEFAULT_COST; + if ($cost != $info['options']['cost']) { + return true; + } + break; + } + return false; + } + + /** + * Verify a password against a hash using a timing attack resistant approach + * + * @param string $password The password to verify + * @param string $hash The hash to verify against + * + * @return boolean If the password matches the hash + */ + function password_verify($password, $hash) { + if (!function_exists('crypt')) { + trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING); + return false; + } + $ret = crypt($password, $hash); + if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != PasswordCompat\binary\_strlen($hash) || PasswordCompat\binary\_strlen($ret) <= 13) { + return false; + } + + $status = 0; + for ($i = 0; $i < PasswordCompat\binary\_strlen($ret); $i++) { + $status |= (ord($ret[$i]) ^ ord($hash[$i])); + } + + return $status === 0; + } + } + +} + +namespace PasswordCompat\binary { + + if (!function_exists('PasswordCompat\\binary\\_strlen')) { + + /** + * Count the number of bytes in a string + * + * We cannot simply use strlen() for this, because it might be overwritten by the mbstring extension. + * In this case, strlen() will count the number of *characters* based on the internal encoding. A + * sequence of bytes might be regarded as a single multibyte character. + * + * @param string $binary_string The input string + * + * @internal + * @return int The number of bytes + */ + function _strlen($binary_string) { + if (function_exists('mb_strlen')) { + return mb_strlen($binary_string, '8bit'); + } + return strlen($binary_string); + } + + /** + * Get a substring based on byte limits + * + * @see _strlen() + * + * @param string $binary_string The input string + * @param int $start + * @param int $length + * + * @internal + * @return string The substring + */ + function _substr($binary_string, $start, $length) { + if (function_exists('mb_substr')) { + return mb_substr($binary_string, $start, $length, '8bit'); + } + return substr($binary_string, $start, $length); + } + + /** + * Check if current PHP version is compatible with the library + * + * @return boolean the check result + */ + function check() { + static $pass = NULL; + + if (is_null($pass)) { + if (function_exists('crypt')) { + $hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG'; + $test = crypt("password", $hash); + $pass = $test == $hash; + } else { + $pass = false; + } + } + return $pass; + } + + } +} \ No newline at end of file diff --git a/core/vendor/ircmaxell/password-compat/version-test.php b/core/vendor/ircmaxell/password-compat/version-test.php new file mode 100644 index 0000000..96f60ca --- /dev/null +++ b/core/vendor/ircmaxell/password-compat/version-test.php @@ -0,0 +1,6 @@ +