diff --git a/CHANGELOG.md b/CHANGELOG.md index c54e8e82b9379d553439caea72e6f30a251fb284..ab547231ba01873338adc43c7eb0862548e8cf1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ CHANGELOG ========= +6.1.0 (29/01/2024) +------------------ + +- Suppression de la dépendance à laminas-crypt. +- ZfcUser\Password\Bcrypt remplace l'ancienne Laminas\Crypt\Password\Bcrypt. +- Si laminas-crypt reste nécessaire dans des projets, il faudra y ajouter "laminas/laminas-crypt": "^3.6" + + 6.0.2 (22/11/2024) ------------------ diff --git a/composer.json b/composer.json index 23cbda27ad43be788a50f042e0f09f380210a001..3214e2113b38b1e5b92e470ca6a954b3fdb6bf68 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,6 @@ "require": { "php": "^8.0", "laminas/laminas-authentication": "^2.9", - "laminas/laminas-crypt": "^3.6", "laminas/laminas-form": "^3.1", "laminas/laminas-inputfilter": "^2.13", "laminas/laminas-modulemanager": "^2.11", diff --git a/src/ZfcUser/Authentication/Adapter/Db.php b/src/ZfcUser/Authentication/Adapter/Db.php index 17ba610c66133489260c0fdf37d29fd0dbd89097..2c24ebbac8cab21ebdac494e252e57c539557027 100644 --- a/src/ZfcUser/Authentication/Adapter/Db.php +++ b/src/ZfcUser/Authentication/Adapter/Db.php @@ -6,7 +6,7 @@ use Psr\Container\ContainerInterface; use Laminas\Authentication\Result as AuthenticationResult; use Laminas\EventManager\EventInterface; use Laminas\ServiceManager\ServiceManager; -use Laminas\Crypt\Password\Bcrypt; +use ZfcUser\Password\Bcrypt; use Laminas\Session\Container as SessionContainer; use ZfcUser\Entity\UserInterface; use ZfcUser\Mapper\UserInterface as UserMapperInterface; diff --git a/src/ZfcUser/Password/Bcrypt.php b/src/ZfcUser/Password/Bcrypt.php new file mode 100644 index 0000000000000000000000000000000000000000..533f9dd7c1c45e6bdf8b750b6b3a95bed8f715a9 --- /dev/null +++ b/src/ZfcUser/Password/Bcrypt.php @@ -0,0 +1,221 @@ +<?php + +namespace ZfcUser\Password; + +use Laminas\Math\Rand; +use Laminas\Stdlib\ArrayUtils; +use Traversable; +use Error; +use TypeError; + +use function is_array; +use function mb_strlen; +use function microtime; +use function password_hash; +use function password_verify; +use function sprintf; +use function strtolower; +use function trigger_error; + +use const E_USER_DEPRECATED; +use const PASSWORD_BCRYPT; +use const PHP_VERSION_ID; + +/** + * Bcrypt algorithm using crypt() function of PHP + */ +class Bcrypt +{ + public const MIN_SALT_SIZE = 22; + + /** @var string */ + protected $cost = '10'; + + /** @var string */ + protected $salt; + + /** + * Constructor + * + * @param array|Traversable $options + * @throws Exception\InvalidArgumentException + */ + public function __construct($options = []) + { + if (! empty($options)) { + if ($options instanceof Traversable) { + $options = ArrayUtils::iteratorToArray($options); + } + + if (! is_array($options)) { + throw new Exception\InvalidArgumentException( + 'The options parameter must be an array or a Traversable' + ); + } + + foreach ($options as $key => $value) { + switch (strtolower($key)) { + case 'salt': + $this->setSalt($value); + break; + case 'cost': + $this->setCost($value); + break; + } + } + } + } + + /** + * Bcrypt + * + * @param string $password + * @throws Exception\RuntimeException + * @return string + */ + public function create($password) + { + $options = ['cost' => (int) $this->cost]; + if (PHP_VERSION_ID < 70000) { // salt is deprecated from PHP 7.0 + $salt = $this->salt ?: self::getBytes(self::MIN_SALT_SIZE); + $options['salt'] = $salt; + } + return password_hash($password, PASSWORD_BCRYPT, $options); + } + + + /** + * Generate random bytes using different approaches + * If PHP 7 is running we use the random_bytes() function + * + * @param int $length + * @return string + * @throws Exception\RuntimeException + */ + public static function getBytes($length) + { + try { + return random_bytes($length); + } catch (TypeError $e) { + throw new Exception\InvalidArgumentException( + 'Invalid parameter provided to getBytes(length)', + 0, + $e + ); + } catch (Error $e) { + throw new Exception\DomainException( + 'The length must be a positive number in getBytes(length)', + 0, + $e + ); + } + } + + + /** + * Verify if a password is correct against a hash value + * + * @param string $password + * @param string $hash + * @return bool + */ + public function verify($password, $hash) + { + return password_verify($password, $hash); + } + + /** + * Set the cost parameter + * + * @param int|string $cost + * @throws Exception\InvalidArgumentException + * @return Bcrypt Provides a fluent interface + */ + public function setCost($cost) + { + if (! empty($cost)) { + $cost = (int) $cost; + if ($cost < 4 || $cost > 31) { + throw new Exception\InvalidArgumentException( + 'The cost parameter of bcrypt must be in range 04-31' + ); + } + $this->cost = sprintf('%1$02d', $cost); + } + return $this; + } + + /** + * Get the cost parameter + * + * @return string + */ + public function getCost() + { + return $this->cost; + } + + /** + * Set the salt value + * + * @param string $salt + * @throws Exception\InvalidArgumentException + * @return Bcrypt Provides a fluent interface + */ + public function setSalt($salt) + { + if (PHP_VERSION_ID >= 70000) { + trigger_error('Salt support is deprecated starting with PHP 7.0.0', E_USER_DEPRECATED); + } + + if (mb_strlen($salt, '8bit') < self::MIN_SALT_SIZE) { + throw new Exception\InvalidArgumentException( + 'The length of the salt must be at least ' . self::MIN_SALT_SIZE . ' bytes' + ); + } + + $this->salt = $salt; + return $this; + } + + /** + * Get the salt value + * + * @return string + */ + public function getSalt() + { + if (PHP_VERSION_ID >= 70000) { + trigger_error('Salt support is deprecated starting with PHP 7.0.0', E_USER_DEPRECATED); + } + + return $this->salt; + } + + /** + * Benchmark the bcrypt hash generation to determine the cost parameter based on time to target. + * + * The default time to test is 50 milliseconds which is a good baseline for + * systems handling interactive logins. If you increase the time, you will + * get high cost with better security, but potentially expose your system + * to DoS attacks. + * + * @see php.net/manual/en/function.password-hash.php#refsect1-function.password-hash-examples + * + * @param float $timeTarget Defaults to 50ms (0.05) + * @return int Maximum cost value that falls within the time to target. + */ + public function benchmarkCost($timeTarget = 0.05) + { + $cost = 8; + + do { + $cost++; + $start = microtime(true); + password_hash('test', PASSWORD_BCRYPT, ['cost' => $cost]); + $end = microtime(true); + } while (($end - $start) < $timeTarget); + + return $cost; + } +} diff --git a/src/ZfcUser/Service/User.php b/src/ZfcUser/Service/User.php index ed6f7dd25698f09419cc1665f459ec2594fa086c..a1b54a109c868ef5035a88bd8ddd5126908a2436 100644 --- a/src/ZfcUser/Service/User.php +++ b/src/ZfcUser/Service/User.php @@ -6,7 +6,7 @@ use Psr\Container\ContainerInterface; use Laminas\Authentication\AuthenticationService; use Laminas\Form\Form; use Laminas\ServiceManager\ServiceManager; -use Laminas\Crypt\Password\Bcrypt; +use ZfcUser\Password\Bcrypt; use Laminas\Hydrator; use ZfcUser\EventManager\EventProvider; use ZfcUser\Mapper\UserInterface as UserMapperInterface; diff --git a/tests/ZfcUserTest/Authentication/Adapter/DbTest.php b/tests/ZfcUserTest/Authentication/Adapter/DbTest.php index 345105c90f3708fbd118183182935ac7af8f33a0..29b414c20fc6bdba20f51b0315c7b8a4659a7f97 100644 --- a/tests/ZfcUserTest/Authentication/Adapter/DbTest.php +++ b/tests/ZfcUserTest/Authentication/Adapter/DbTest.php @@ -328,7 +328,7 @@ class DbTest extends \PHPUnit_Framework_TestCase ->method('getPassword') ->will($this->returnValue('$2a$10$x05G2P803MrB3jaORBXBn.QHtiYzGQOBjQ7unpEIge.Mrz6c3KiVm')); - $bcrypt = $this->getMock('Laminas\Crypt\Password\Bcrypt'); + $bcrypt = $this->getMock('ZfcUSer\Password\Bcrypt'); $bcrypt->expects($this->once()) ->method('getCost') ->will($this->returnValue('10')); @@ -356,7 +356,7 @@ class DbTest extends \PHPUnit_Framework_TestCase ->method('setPassword') ->with('$2a$10$D41KPuDCn6iGoESjnLee/uE/2Xo985sotVySo2HKDz6gAO4hO/Gh6'); - $bcrypt = $this->getMock('Laminas\Crypt\Password\Bcrypt'); + $bcrypt = $this->getMock('ZfcUSer\Password\Bcrypt'); $bcrypt->expects($this->once()) ->method('getCost') ->will($this->returnValue('5')); diff --git a/tests/ZfcUserTest/Service/UserTest.php b/tests/ZfcUserTest/Service/UserTest.php index 1a0326644a0f5a76add18dc10d7d52e0380cc6ab..3b935812f0c4a098e5d472b2e710c421a97a881d 100644 --- a/tests/ZfcUserTest/Service/UserTest.php +++ b/tests/ZfcUserTest/Service/UserTest.php @@ -3,7 +3,7 @@ namespace ZfcUserTest\Service; use ZfcUser\Service\User as Service; -use Laminas\Crypt\Password\Bcrypt; +use ZfcUser\Password\Bcrypt; class UserTest extends \PHPUnit_Framework_TestCase {