diff --git a/core/composer.json b/core/composer.json index c975dc9..067f70c 100644 --- a/core/composer.json +++ b/core/composer.json @@ -31,7 +31,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" }, "autoload": { "psr-4": { diff --git a/core/composer.lock b/core/composer.lock index 26a3e21..8ba7579 100644 --- a/core/composer.lock +++ b/core/composer.lock @@ -4,7 +4,7 @@ "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "377cea36943eae1762c885ef742bc320", + "hash": "a1f5c8c7e6843c8fdde4e03c7a383ecb", "packages": [ { "name": "behat/mink", @@ -940,6 +940,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 589447d..1706367 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -5,6 +5,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: @@ -751,13 +752,14 @@ 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. +# The first argument of the hashing service (constructor of PhpPassword) is the +# 'cost' option of password_hash(). # @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 speed and power of computers available to crack the hashes. password: + class: Drupal\Core\Password\PhpPassword + arguments: ['%password_hash_cost%', '@drupal7_password'] + drupal7_password: class: Drupal\Core\Password\PhpassHashedPassword arguments: [16] accept_header_matcher: diff --git a/core/lib/Drupal/Core/Password/PhpPassword.php b/core/lib/Drupal/Core/Password/PhpPassword.php new file mode 100644 index 0000000..1f65dd4 --- /dev/null +++ b/core/lib/Drupal/Core/Password/PhpPassword.php @@ -0,0 +1,117 @@ +=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 { + + /** + * Maximum password length. + */ + const PASSWORD_MAX_LENGTH = 512; + + /** + * The algorithmic cost that should be used. + * + * @var int + */ + protected $cost; + + /** + * The Drupal 7 password hashing service. + * + * @var \Drupal\Core\Password\PhpassHashedPassword + */ + 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, UserInterface $account) { + $stored_hash = $account->getPassword(); + + // Password migrated from Drupal 7. + if (substr($stored_hash, 0, 5) == 'D7$S$') { + $salt = substr($stored_hash, 2, 12); + $stored_hash = substr($stored_hash, 14); + $password = $this->drupal7Password->crypt('sha512', $password, $salt); + } + // MD5 migrated password (Drupal 6). + elseif (substr($stored_hash, 0, 2) == 'U$') { + $stored_hash = substr($stored_hash, 1); + $password = md5($password); + } + + return password_verify($password, $stored_hash); + } + + /** + * {@inheritdoc} + */ + public function userNeedsNewHash(UserInterface $account) { + // The PHP 5.5 password_needs_rehash() will return TRUE in two cases: + // - The the password is Drupal 6 or 7 password and it has been rehashed + // during the migration. In this case the rehashed legacy hash is prefixed + // to indicate and old-Drupal hash and will not comply 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($account->getPassword(), 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/lib/Drupal/Core/Password/PhpassHashedPassword.php b/core/lib/Drupal/Core/Password/PhpassHashedPassword.php index 17cafdd..e8cd3e9 100644 --- a/core/lib/Drupal/Core/Password/PhpassHashedPassword.php +++ b/core/lib/Drupal/Core/Password/PhpassHashedPassword.php @@ -158,7 +158,7 @@ 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) { + public function crypt($algo, $password, $setting) { // Prevent DoS attacks by refusing to hash large passwords. if (strlen($password) > 512) { return FALSE; diff --git a/core/modules/migrate/src/MigratePassword.php b/core/modules/migrate/src/MigratePassword.php index ba42e8a..b412357 100644 --- a/core/modules/migrate/src/MigratePassword.php +++ b/core/modules/migrate/src/MigratePassword.php @@ -8,12 +8,13 @@ namespace Drupal\migrate; use Drupal\Core\Password\PasswordInterface; +use Drupal\migrate\Entity\MigrationInterface; use Drupal\user\UserInterface; /** - * 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 { @@ -26,10 +27,19 @@ class MigratePassword implements PasswordInterface { /** * Indicates if MD5 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 @@ -59,27 +69,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/simpletest/src/KernelTestBase.php b/core/modules/simpletest/src/KernelTestBase.php index c4d94eb..366478e 100644 --- a/core/modules/simpletest/src/KernelTestBase.php +++ b/core/modules/simpletest/src/KernelTestBase.php @@ -15,6 +15,7 @@ use Drupal\Core\Entity\Sql\SqlEntityStorageInterface; use Drupal\Core\KeyValueStore\KeyValueMemoryFactory; use Drupal\Core\Language\Language; +use Drupal\Core\Password\PhpassHashedPassword; use Drupal\Core\Site\Settings; use Symfony\Component\DependencyInjection\Parameter; use Drupal\Core\StreamWrapper\StreamWrapperInterface; @@ -344,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/Tests/UserLoginTest.php b/core/modules/user/src/Tests/UserLoginTest.php index 27ffa2a..8628780 100644 --- a/core/modules/user/src/Tests/UserLoginTest.php +++ b/core/modules/user/src/Tests/UserLoginTest.php @@ -8,7 +8,8 @@ namespace Drupal\user\Tests; use Drupal\simpletest\WebTestBase; -use Drupal\Core\Password\PhpassHashedPassword; +use Drupal\user\Entity\User; +use Drupal\user\UserInterface; /** * Ensure that login works as expected. @@ -18,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() { @@ -31,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'); } /** @@ -111,36 +129,70 @@ 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. $account = user_load($account->id()); - $this->assertIdentical($password_hasher->getCountLog2($account->getPassword()), $default_count_log2); + $this->assertTrue($this->passwordHasher->check($password, $account)); - // 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; $this->drupalLogin($account); // Load the stored user, which should have a different password hash now. $account = user_load($account->id(), TRUE); - $this->assertIdentical($password_hasher->getCountLog2($account->getPassword()), $overridden_count_log2); + $this->assertTrue($this->passwordHasher->check($password, $account)); + } + + /** + * Test MD5 (Drupal 6) passwords rehashing. + */ + public function testDrupal6MigratedPasswordRehashing() { + $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)); + } + + /** + * Test Drupal 7 passwords rehashing. + */ + public function testDrupal7MigratedPasswordRehashing() { + $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)); } /** @@ -172,4 +224,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/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..ed9c05e --- /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\PhpassHashedPassword + 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/tests/Drupal/Tests/Core/Password/PasswordHashingTest.php b/core/tests/Drupal/Tests/Core/Password/PhpPasswordTest.php similarity index 62% copy from core/tests/Drupal/Tests/Core/Password/PasswordHashingTest.php copy to core/tests/Drupal/Tests/Core/Password/PhpPasswordTest.php index f304be0..6d0d4f3 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\system\Tests\System\PhpPasswordTest. */ namespace Drupal\Tests\Core\Password; +use Drupal\Core\Password\PhpPassword; use Drupal\Core\Password\PhpassHashedPassword; 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,21 +62,12 @@ protected function setUp() { $this->user = $this->getMockBuilder('Drupal\user\Entity\User') ->disableOriginalConstructor() ->getMock(); - $this->passwordHasher = new PhpassHashedPassword(1); - } + $this->passwordHasher = new PhpPassword(4, 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"); + $this->password = $this->randomMachineName(); + $this->md5Password = md5($this->password); } - /** * Test a password needs update. * @@ -86,15 +78,13 @@ public function testPasswordNeedsUpdate() { ->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->userNeedsNewHash($this->user)); } /** * Test password hashing. * * @covers ::hash - * @covers ::getCountLog2 - * @covers ::check * @covers ::userNeedsNewHash */ public function testPasswordHashing() { @@ -102,45 +92,38 @@ public function testPasswordHashing() { $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)); + $this->assertFalse($this->passwordHasher->userNeedsNewHash($this->user)); } /** * 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.'); + // Increment the cost from 4 to 5. + $this->passwordHasher = new PhpPassword(5, new PhpassHashedPassword(1)); + $this->assertTrue($this->passwordHasher->userNeedsNewHash($this->user)); // 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->userNeedsNewHash($this->user)); + $this->assertTrue($this->passwordHasher->check($this->password, $this->user)); } /** * 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/tests/Drupal/Tests/Core/Password/PasswordHashingTest.php b/core/tests/Drupal/Tests/Core/Password/PhpassHashedPasswordTest.php similarity index 97% rename from core/tests/Drupal/Tests/Core/Password/PasswordHashingTest.php rename to core/tests/Drupal/Tests/Core/Password/PhpassHashedPasswordTest.php index f304be0..2a870ed 100644 --- a/core/tests/Drupal/Tests/Core/Password/PasswordHashingTest.php +++ b/core/tests/Drupal/Tests/Core/Password/PhpassHashedPasswordTest.php @@ -2,7 +2,7 @@ /** * @file - * Contains Drupal\system\Tests\System\PasswordHashingTest. + * Contains Drupal\system\Tests\System\PhpassHashedPasswordTest. */ namespace Drupal\Tests\Core\Password; @@ -16,7 +16,7 @@ * @coversDefaultClass \Drupal\Core\Password\PhpassHashedPassword * @group System */ -class PasswordHashingTest extends UnitTestCase { +class PhpassHashedPasswordTest extends UnitTestCase { /** * The user for testing. 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 625c282..2a6dfd0 100644 --- a/core/vendor/composer/installed.json +++ b/core/vendor/composer/installed.json @@ -3156,5 +3156,49 @@ ], "description": "Symfony BrowserKit Component", "homepage": "http://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 @@ +