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 0c90697..25db4e0 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 6681e7d..cd13586 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: cache_factory: class: Drupal\Core\Cache\CacheFactory @@ -663,12 +664,13 @@ services: 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. +# constructor of Password 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..a26b8e4 --- /dev/null +++ b/core/lib/Drupal/Core/Password/PhpPassword.php @@ -0,0 +1,96 @@ +=5.5.0) password hashing + * functions. + * + * @see http://php.net/manual/en/ref.password.php + */ +class PhpPassword implements PasswordInterface { + + /** + * The algorithmic cost that should be used. + * + * @var int + */ + protected $cost; + + /** + * The Drupal 7 password hashing service. + * + * @var \Drupal\Core\Password\PasswordInterface + */ + 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) > 512) { + 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, 3) == 'D7$') { + $stored_hash = substr($stored_hash, 2); + $password = $this->drupal7Password->hash($password); + } + // Password migrated from 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) { + 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/modules/migrate/src/MigratePassword.php b/core/modules/migrate/src/MigratePassword.php index ba42e8a..796c7f1 100644 --- a/core/modules/migrate/src/MigratePassword.php +++ b/core/modules/migrate/src/MigratePassword.php @@ -59,10 +59,22 @@ 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; + } + // @todo Is this enough? Maybe we should pass and test somehow the current + // migration source to detect a Drupal 7 source. This can be done by + // injecting the current migration when enabling prefixing in + // Drupal\migrate\Plugin\migrate\destination\EntityUser. In that case + // ::enableMd5Prefixing() needs to be renamed. Or pass the migration into + // constructor? How? + if (substr($password, 0, 3) == '$S$') { + $hash = 'D7' . $hash; + } } return $hash; diff --git a/core/modules/simpletest/src/KernelTestBase.php b/core/modules/simpletest/src/KernelTestBase.php index 14b839b..d608d05 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; @@ -345,7 +346,7 @@ public function containerBuild(ContainerBuilder $container) { } if ($container->hasDefinition('password')) { - $container->getDefinition('password')->setArguments(array(1)); + $container->getDefinition('password')->setArguments([4, new PhpassHashedPassword(1)]); } // 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..08b89b5 100644 --- a/core/modules/user/src/Tests/UserLoginTest.php +++ b/core/modules/user/src/Tests/UserLoginTest.php @@ -111,13 +111,10 @@ 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 + /** @var \Drupal\Core\Password\Password $password_hasher */ $password_hasher = $this->container->get('password'); // Create a new user and authenticate. @@ -125,22 +122,22 @@ function testPasswordRehashOnLogin() { $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($password_hasher->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); + $password_hasher = $this->container->get('password'); + $this->assertTrue($password_hasher->check($password, $account)); } /** 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 314ce82..5481bc3 100644 --- a/core/vendor/composer/installed.json +++ b/core/vendor/composer/installed.json @@ -3133,5 +3133,49 @@ "serializer", "xml" ] + }, + { + "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 @@ +