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..51e86db 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,20 @@ 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 + class: Drupal\Core\Password\PhpPassword + arguments: ['%password_hash_cost%', '@drupal7_password'] + lazy: true + drupal7_password: + class: Drupal\Core\Password\Drupal7Password arguments: [16] + lazy: true accept_header_matcher: class: Drupal\Core\Routing\AcceptHeaderMatcher tags: diff --git a/core/lib/Drupal/Core/Password/PhpassHashedPassword.php b/core/lib/Drupal/Core/Password/Drupal7Password.php similarity index 80% rename from core/lib/Drupal/Core/Password/PhpassHashedPassword.php rename to core/lib/Drupal/Core/Password/Drupal7Password.php index 17cafdd..92bdd9d 100644 --- a/core/lib/Drupal/Core/Password/PhpassHashedPassword.php +++ b/core/lib/Drupal/Core/Password/Drupal7Password.php @@ -2,21 +2,25 @@ /** * @file - * Definition of Drupal\Core\Password\PhpassHashedPassword + * Contains \Drupal\Core\Password\Drupal7Password. */ namespace Drupal\Core\Password; use Drupal\Component\Utility\Crypt; -use Drupal\user\UserInterface; /** * Secure password hashing functions based on the Portable PHP password * hashing framework. * + * This is a custom password hashing mechanism used in Drupal 7. We keep this + * password hasher in order to validate passwords migrated from Drupal 7 that + * are being rehashed on first user login. + * * @see http://www.openwall.com/phpass/ */ -class PhpassHashedPassword implements PasswordInterface { +class Drupal7Password implements PasswordInterface { + /** * The minimum allowed log2 number of iterations for password stretching. */ @@ -45,7 +49,7 @@ class PhpassHashedPassword implements PasswordInterface { protected $countLog2; /** - * Constructs a new phpass password hashing instance. + * Constructs a new password hashing instance. * * @param int $countLog2 * Password stretching iteration count. Specifies the number of times the @@ -61,13 +65,13 @@ function __construct($countLog2) { /** * Encodes bytes into printable base 64 using the *nix standard from crypt(). * - * @param String $input + * @param string $input * The string containing bytes to encode. - * @param Integer $count + * @param int $count * The number of characters (bytes) to encode. * - * @return String - * Encoded string + * @return string + * Encoded string. */ protected function base64Encode($input, $count) { $output = ''; @@ -96,7 +100,8 @@ protected function base64Encode($input, $count) { } /** - * Generates a random base 64-encoded salt prefixed with settings for the hash. + * 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. @@ -104,7 +109,7 @@ protected function base64Encode($input, $count) { * - The ability to determine whether two users have the same (or different) * password without actually having to guess one of the passwords. * - * @return String + * @return string * A 12 character string containing the iteration count and a random salt. */ protected function generateSalt() { @@ -119,11 +124,11 @@ protected function generateSalt() { /** * Ensures that $count_log2 is within set bounds. * - * @param Integer $count_log2 + * @param int $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 + * @return int * Integer within set bounds that is closest to $count_log2. */ protected function enforceLog2Boundaries($count_log2) { @@ -145,20 +150,20 @@ protected function enforceLog2Boundaries($count_log2) { * for an attacker to try to break the hash by brute-force computation of the * hashes of a large number of plain-text words or strings to find a match. * - * @param String $algo + * @param string $algo * The string name of a hashing algorithm usable by hash(), like 'sha256'. - * @param String $password + * @param string $password * Plain-text password up to 512 bytes (128 to 512 UTF-8 characters) to * hash. - * @param String $setting - * An existing hash or the output of $this->generateSalt(). Must be - * at least 12 characters (the settings and salt). + * @param string $setting + * An existing hash or the output of $this->generateSalt(). Must be at least + * 12 characters (the settings and salt). * - * @return String + * @return string * 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) { + public function crypt($algo, $password, $setting) { // Prevent DoS attacks by refusing to hash large passwords. if (strlen($password) > 512) { return FALSE; @@ -201,68 +206,69 @@ protected function crypt($algo, $password, $setting) { } /** - * Parse the log2 iteration count from a stored hash or setting string. + * Parses 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). * - * @param String $setting - * An existing hash or the output of $this->generateSalt(). Must be - * at least 12 characters (the settings and salt). + * @return int + * The log2 iteration count. */ public function getCountLog2($setting) { return strpos(static::$ITOA64, $setting[3]); } /** - * Implements Drupal\Core\Password\PasswordInterface::hash(). + * {@inheritdoc} */ public function hash($password) { return $this->crypt('sha512', $password, $this->generateSalt()); } /** - * Implements Drupal\Core\Password\PasswordInterface::checkPassword(). + * {@inheritdoc} */ - public function check($password, UserInterface $account) { - if (substr($account->getPassword(), 0, 2) == 'U$') { + public function check($password, $hash) { + if (substr($hash, 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); + $hash = substr($hash, 1); $password = md5($password); } - else { - $stored_hash = $account->getPassword(); - } - $type = substr($stored_hash, 0, 3); + $type = substr($hash, 0, 3); switch ($type) { case '$S$': // A normal Drupal 7 password using sha512. - $hash = $this->crypt('sha512', $password, $stored_hash); + $hashed = $this->crypt('sha512', $password, $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); + $hashed = $this->crypt('md5', $password, $hash); break; default: return FALSE; } - return ($hash && $stored_hash == $hash); + return ($hashed && $hash == $hashed); } /** - * Implements Drupal\Core\Password\PasswordInterface::userNeedsNewHash(). + * {@inheritdoc} */ - public function userNeedsNewHash(UserInterface $account) { + public function needsRehash($hash) { // Check whether this was an updated password. - if ((substr($account->getPassword(), 0, 3) != '$S$') || (strlen($account->getPassword()) != static::HASH_LENGTH)) { + if ((substr($hash, 0, 3) != '$S$') || (strlen($hash) != 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); + return ($this->getCountLog2($hash) !== $count_log2); } + } 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..8d74a11 --- /dev/null +++ b/core/lib/Drupal/Core/Password/PhpPassword.php @@ -0,0 +1,112 @@ +=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 int + * + * @see password_hash(). + * @see http://php.net/manual/en/ref.password.php + */ + protected $cost; + + /** + * The Drupal 7 password hashing service. + * + * @var \Drupal\Core\Password\Drupal7Password + */ + protected $drupal7Password; + + /** + * Constructs a new password hashing instance. + * + * @param int $cost + * The algorithmic cost that should be used. + * @param \Drupal\Core\Password\PasswordInterface $drupal7_password + * The Drupal7 password hashing service. + */ + function __construct($cost, PasswordInterface $drupal7_password) { + $this->cost = $cost; + $this->drupal7Password = $drupal7_password; + } + + /** + * {@inheritdoc} + */ + public function hash($password) { + // Prevent DoS attacks by refusing to hash large passwords. + if (strlen($password) > static::PASSWORD_MAX_LENGTH) { + return FALSE; + } + + return password_hash($password, PASSWORD_DEFAULT, $this->getOptions()); + } + + /** + * {@inheritdoc} + */ + public function check($password, $hash) { + // Password migrated from Drupal 7. + if (substr($hash, 0, 5) == 'D7$S$') { + $salt = substr($hash, 2, 12); + $hash = substr($hash, 14); + $password = $this->drupal7Password->crypt('sha512', $password, $salt); + } + // MD5 migrated password (Drupal 6). + elseif (substr($hash, 0, 2) == 'U$') { + $hash = substr($hash, 1); + $password = md5($password); + } + + return password_verify($password, $hash); + } + + /** + * {@inheritdoc} + */ + public function needsRehash($hash) { + // The PHP 5.5 password_needs_rehash() will return TRUE in two cases: + // - The password is a Drupal 6 or 7 password and it has been rehashed + // during the migration. In this case the rehashed legacy hash is prefixed + // to indicate an old Drupal hash and will not comply with the expected + // password_needs_rehash() format. + // - 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_DEFAULT, $this->getOptions()); + } + + /** + * Returns password options. + * + * @return array + * Associative array with password options. + */ + protected function getOptions() { + return ['cost' => $this->cost]; + } + +} diff --git a/core/modules/migrate/migrate.services.yml b/core/modules/migrate/migrate.services.yml index 3f2d32d..d8ecc7f 100644 --- a/core/modules/migrate/migrate.services.yml +++ b/core/modules/migrate/migrate.services.yml @@ -20,3 +20,4 @@ services: password_migrate: class: Drupal\migrate\MigratePassword arguments: ['@password_original'] + lazy: true diff --git a/core/modules/migrate/src/MigratePassword.php b/core/modules/migrate/src/MigratePassword.php index ba42e8a..71948ea 100644 --- a/core/modules/migrate/src/MigratePassword.php +++ b/core/modules/migrate/src/MigratePassword.php @@ -8,12 +8,12 @@ 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 - * passwords with the 'U' flag. The new salted hash is recreated on first login - * similarly to the D6->D7 upgrade path. + * Replaces the original 'password' service in order to prefix the MD5 or + * Drupal 7 re-hashed passwords. The new salted hash will be recreated on first + * login similarly to the D6->D7 upgrade path. */ class MigratePassword implements PasswordInterface { @@ -25,11 +25,20 @@ class MigratePassword implements PasswordInterface { protected $originalPassword; /** - * Indicates if MD5 password prefixing is enabled. + * Indicates if password prefixing is enabled. + * + * @var bool */ protected $enabled = FALSE; /** + * The current migration. + * + * @var \Drupal\migrate\Entity\MigrationInterface + */ + protected $migration; + + /** * Builds the replacement password service class. * * @param \Drupal\Core\Password\PasswordInterface $original_password @@ -42,15 +51,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); } /** @@ -59,27 +68,46 @@ public function userNeedsNewHash(UserInterface $account) { public function hash($password) { $hash = $this->originalPassword->hash($password); - // Allow prefixing only if the service was asked to prefix. Check also if - // the $password pattern is conforming to a MD5 result. - if ($this->enabled && preg_match('/^[0-9a-f]{32}$/', $password)) { - $hash = 'U' . $hash; + // Allow prefixing only if the service was asked to prefix. + if ($this->enabled) { + // If the $password pattern is conforming to a MD5 result this is most + // likely a Drupal 6 hashed password. + if (preg_match('/^[0-9a-f]{32}$/', $password)) { + $hash = 'U' . $hash; + } + // Prefix user hashed passwords coming from Drupal 7. + $source = $this->migration->getSourcePlugin()->getPluginId(); + if (substr($password, 0, 3) == '$S$' && $source == 'd7_user') { + $salt = substr($password, 0, 12); + $hash = 'D7' . $salt . $hash; + } } return $hash; } /** - * Enables the MD5 password prefixing. + * Enables password prefixing. */ - public function enableMd5Prefixing() { + public function enablePrefixing() { $this->enabled = TRUE; } /** - * Disables the MD5 password prefixing. + * Disables password prefixing. */ - public function disableMd5Prefixing() { + public function disablePrefixing() { $this->enabled = FALSE; } + /** + * Sets the current migration. + * + * @param \Drupal\migrate\Entity\MigrationInterface $migration + * The current migration. + */ + public function setMigration(MigrationInterface $migration) { + $this->migration = $migration; + } + } diff --git a/core/modules/migrate/src/MigrateServiceProvider.php b/core/modules/migrate/src/MigrateServiceProvider.php index 78a60bb..705f9d2 100644 --- a/core/modules/migrate/src/MigrateServiceProvider.php +++ b/core/modules/migrate/src/MigrateServiceProvider.php @@ -12,10 +12,10 @@ /** * Swaps the original 'password' service in order to handle password hashing for - * user migrations that have passwords hashed to MD5. + * user migrations that have passwords hashed with MD5 or Drupal 7 passwords. * * @see \Drupal\migrate\MigratePassword - * @see \Drupal\Core\Password\PhpassHashedPassword + * @see \Drupal\Core\Password\PhpPassword */ class MigrateServiceProvider implements ServiceModifierInterface { diff --git a/core/modules/migrate/src/Plugin/migrate/destination/EntityUser.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityUser.php index 786988f..34fd890 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/EntityUser.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityUser.php @@ -13,7 +13,6 @@ use Drupal\migrate\Entity\MigrationInterface; use Drupal\migrate\MigrateException; use Drupal\migrate\MigratePassword; -use Drupal\migrate\Plugin\MigratePluginManager; use Drupal\migrate\Row; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -79,12 +78,14 @@ public static function create(ContainerInterface $container, array $configuratio /** * {@inheritdoc} + * * @throws \Drupal\migrate\MigrateException */ public function import(Row $row, array $old_destination_id_values = array()) { if ($this->password) { if ($this->password instanceof MigratePassword) { - $this->password->enableMd5Prefixing(); + $this->password->setMigration($this->migration); + $this->password->enablePrefixing(); } else { throw new MigrateException('Password service has been altered by another module, aborting.'); @@ -92,7 +93,7 @@ public function import(Row $row, array $old_destination_id_values = array()) { } $ids = parent::import($row, $old_destination_id_values); if ($this->password) { - $this->password->disableMd5Prefixing(); + $this->password->disablePrefixing(); } return $ids; 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..fae6dc4 100644 --- a/core/modules/simpletest/src/KernelTestBase.php +++ b/core/modules/simpletest/src/KernelTestBase.php @@ -345,8 +345,9 @@ public function containerBuild(ContainerBuilder $container) { $definition->clearTag('path_processor_inbound')->clearTag('path_processor_outbound'); } - if ($container->hasDefinition('password')) { - $container->getDefinition('password')->setArguments(array(1)); + if ($container->hasDefinition('password') && $container->hasDefinition('drupal7_password')) { + $container->getDefinition('drupal7_password')->setArguments([1]); + $container->getDefinition('password')->setArguments([4, $container->get('drupal7_password')]); } // 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..fdbbc99 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,20 @@ class UserLoginTest extends WebTestBase { /** + * Drupal password hasher service. + * + * @var \Drupal\Core\Password\PasswordInterface + */ + private $passwordHasher; + + /** + * Drupal 7 password hasher service. + * + * @var \Drupal\Core\Password\PasswordInterface + */ + private $drupal7PasswordHasher; + + /** * Tests login with destination. */ function testLoginCacheTagsAndDestination() { @@ -32,6 +46,9 @@ function testLoginCacheTagsAndDestination() { $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->passwordHasher = $this->container->get('password'); + $this->drupal7PasswordHasher = $this->container->get('drupal7_password'); } /** @@ -112,30 +129,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'); + /** @var \Drupal\user\UserInterface $account */ $account = User::load($account->id()); - $this->assertIdentical($password_hasher->getCountLog2($account->getPassword()), $default_count_log2); + $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 +154,50 @@ 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(); + + // After logging in the user password has been rehashed and is valid. + $this->assertTrue($this->passwordHasher->check($plain, $account->getPassword())); + } + + /** + * Test Drupal 7 passwords rehashing. + */ + public function testDrupal7MigratedPasswordRehashing() { + /** @var \Drupal\user\UserInterface $account */ + $account = $this->drupalCreateUser(); + $plain = $account->pass_raw; + + // We pretend that the user was migrated from Drupal 7. + $d7_pass = $this->drupal7PasswordHasher->hash($plain); + $salt = substr($d7_pass, 0, 12); + $migrated_pass = 'D7' . $salt . $this->passwordHasher->hash($d7_pass); + $this->storeHashedPassword($account, $migrated_pass); + + // User first login after migration. + $this->drupalLogin($account); + $this->drupalLogout(); + + // After logging in the user password has been rehashed and is valid. + $this->assertTrue($this->passwordHasher->check($plain, $account->getPassword())); } /** @@ -179,4 +233,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..56c1281 --- /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,7 @@ +services: + password: + class: Drupal\Core\Password\PhpPassword + arguments: [11, '@drupal7_password'] + drupal7_password: + class: Drupal\Core\Password\Drupal7Password + arguments: [16] 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/Drupal7PasswordTest.php similarity index 68% copy from core/tests/Drupal/Tests/Core/Password/PasswordHashingTest.php copy to core/tests/Drupal/Tests/Core/Password/Drupal7PasswordTest.php index f304be0..cb69edf 100644 --- a/core/tests/Drupal/Tests/Core/Password/PasswordHashingTest.php +++ b/core/tests/Drupal/Tests/Core/Password/Drupal7PasswordTest.php @@ -2,21 +2,20 @@ /** * @file - * Contains Drupal\system\Tests\System\PasswordHashingTest. + * Contains Drupal7PasswordTest.php */ - namespace Drupal\Tests\Core\Password; -use Drupal\Core\Password\PhpassHashedPassword; use Drupal\Tests\UnitTestCase; +use Drupal\Core\Password\Drupal7Password; /** * Unit tests for password hashing API. * - * @coversDefaultClass \Drupal\Core\Password\PhpassHashedPassword + * @coversDefaultClass \Drupal\Core\Password\Drupal7Password * @group System */ -class PasswordHashingTest extends UnitTestCase { +class Drupal7PasswordTest extends UnitTestCase { /** * The user for testing. @@ -49,7 +48,7 @@ class PasswordHashingTest extends UnitTestCase { /** * The password hasher under test. * - * @var \Drupal\Core\Password\PhpassHashedPassword + * @var \Drupal\Core\Password\Drupal7Password */ protected $passwordHasher; @@ -61,7 +60,7 @@ protected function setUp() { $this->user = $this->getMockBuilder('Drupal\user\Entity\User') ->disableOriginalConstructor() ->getMock(); - $this->passwordHasher = new PhpassHashedPassword(1); + $this->passwordHasher = new Drupal7Password(1); } /** @@ -70,23 +69,22 @@ protected function setUp() { * @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"); + $hasher = new FakeDrupal7Password(); + $this->assertEquals(Drupal7Password::MIN_HASH_COUNT, $hasher->enforceLog2Boundaries(1)); + $this->assertEquals(Drupal7Password::MAX_HASH_COUNT, $hasher->enforceLog2Boundaries(100)); } - /** * Test a password needs update. * - * @covers ::userNeedsNewHash + * @covers ::needsRehash */ 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.'); + $this->assertTrue($this->passwordHasher->needsRehash($this->user->getPassword())); } /** @@ -95,19 +93,19 @@ public function testPasswordNeedsUpdate() { * @covers ::hash * @covers ::getCountLog2 * @covers ::check - * @covers ::userNeedsNewHash + * @covers ::needsRehash */ 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.'); + $this->assertSame($this->passwordHasher->getCountLog2($this->hashedPassword), Drupal7Password::MIN_HASH_COUNT); + $this->assertNotEquals($this->hashedPassword, $this->md5Password); + $this->assertTrue($this->passwordHasher->check($this->password, $this->user->getPassword())); // 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.'); + // needsRehash() should return FALSE. + $this->assertFalse($this->passwordHasher->needsRehash($this->user->getPassword())); } /** @@ -116,25 +114,25 @@ public function testPasswordHashing() { * @covers ::hash * @covers ::getCountLog2 * @covers ::check - * @covers ::userNeedsNewHash + * @covers ::needsRehash */ 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.'); + $this->passwordHasher = new Drupal7Password(Drupal7Password::MIN_HASH_COUNT + 1); + $this->assertTrue($this->passwordHasher->needsRehash($this->user->getPassword())); // 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.'); + $this->assertSame($this->passwordHasher->getCountLog2($rehashed_password), Drupal7Password::MIN_HASH_COUNT + 1); + $this->assertNotEquals($rehashed_password, $this->hashedPassword); // 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.'); + $this->assertFalse($this->passwordHasher->needsRehash($this->user->getPassword())); + $this->assertTrue($this->passwordHasher->check($this->password, $this->user->getPassword())); } /** @@ -182,7 +180,7 @@ public function providerLongPasswords() { /** * A fake class for tests. */ -class FakePhpassHashedPassword extends PhpassHashedPassword { +class FakeDrupal7Password extends Drupal7Password { function __construct() { // Noop. diff --git a/core/tests/Drupal/Tests/Core/Password/PasswordHashingTest.php b/core/tests/Drupal/Tests/Core/Password/PhpPasswordTest.php similarity index 55% rename from core/tests/Drupal/Tests/Core/Password/PasswordHashingTest.php rename to core/tests/Drupal/Tests/Core/Password/PhpPasswordTest.php index f304be0..923da55 100644 --- a/core/tests/Drupal/Tests/Core/Password/PasswordHashingTest.php +++ b/core/tests/Drupal/Tests/Core/Password/PhpPasswordTest.php @@ -2,21 +2,22 @@ /** * @file - * Contains Drupal\system\Tests\System\PasswordHashingTest. + * Contains \Drupal\Tests\Core\Password\PhpPasswordTest. */ namespace Drupal\Tests\Core\Password; -use Drupal\Core\Password\PhpassHashedPassword; +use Drupal\Core\Password\PhpPassword; +use Drupal\Core\Password\Drupal7Password; use Drupal\Tests\UnitTestCase; /** * Unit tests for password hashing API. * - * @coversDefaultClass \Drupal\Core\Password\PhpassHashedPassword + * @coversDefaultClass \Drupal\Core\Password\PhpPassword * @group System */ -class PasswordHashingTest extends UnitTestCase { +class PhpPasswordTest extends UnitTestCase { /** * The user for testing. @@ -49,7 +50,7 @@ class PasswordHashingTest extends UnitTestCase { /** * The password hasher under test. * - * @var \Drupal\Core\Password\PhpassHashedPassword + * @var \Drupal\Core\Password\PasswordInterface */ protected $passwordHasher; @@ -61,86 +62,68 @@ protected function setUp() { $this->user = $this->getMockBuilder('Drupal\user\Entity\User') ->disableOriginalConstructor() ->getMock(); - $this->passwordHasher = new PhpassHashedPassword(1); - } + $this->passwordHasher = new PhpPassword(4, new Drupal7Password(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"); + $this->password = $this->randomMachineName(); + $this->md5Password = md5($this->password); } - /** * Test a password needs update. * - * @covers ::userNeedsNewHash + * @covers ::needsRehash */ 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.'); + $this->assertTrue($this->passwordHasher->needsRehash($this->user->getPassword())); } /** * Test password hashing. * * @covers ::hash - * @covers ::getCountLog2 - * @covers ::check - * @covers ::userNeedsNewHash + * @covers ::needsRehash */ 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.'); + $this->assertNotEquals($this->hashedPassword, $this->md5Password); + $this->assertTrue($this->passwordHasher->check($this->password, $this->user->getPassword())); + $this->assertFalse($this->passwordHasher->needsRehash($this->user->getPassword())); } /** * Tests password rehashing. * * @covers ::hash - * @covers ::getCountLog2 - * @covers ::check - * @covers ::userNeedsNewHash + * @covers ::needsRehash */ 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.'); + // Increment the cost from 4 to 5. + $this->passwordHasher = new PhpPassword(5, new Drupal7Password(1)); + $this->assertTrue($this->passwordHasher->needsRehash($this->user->getPassword())); // 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.'); + $this->assertNotEquals($rehashed_password, $this->hashedPassword); // 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.'); + $this->assertFalse($this->passwordHasher->needsRehash($this->user->getPassword())); + $this->assertTrue($this->passwordHasher->check($this->password, $this->user->getPassword())); } /** * Verifies that passwords longer than 512 bytes are not hashed. * - * @covers ::crypt + * @covers ::hash * * @dataProvider providerLongPasswords */ @@ -178,19 +161,3 @@ public function providerLongPasswords() { } } - -/** - * 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/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 @@ +