diff --git a/UPGRADE-4.3.md b/UPGRADE-4.3.md index 5205e9ac93..c52cb7436c 100644 --- a/UPGRADE-4.3.md +++ b/UPGRADE-4.3.md @@ -174,7 +174,7 @@ Security SecurityBundle -------------- - * Configuring encoders using `argon2i` as algorithm has been deprecated, use `sodium` instead. + * Configuring encoders using `argon2i` as algorithm has been deprecated, use `auto` instead. TwigBridge ---------- diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index 28bbd1286e..e26a56b788 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -4,11 +4,12 @@ CHANGELOG 4.3.0 ----- + * Added new encoder types: `auto` (recommended), `native` and `sodium` * The normalization of the cookie names configured in the `logout.delete_cookies` option is deprecated and will be disabled in Symfony 5.0. This affects to cookies with dashes in their names. For example, starting from Symfony 5.0, the `my-cookie` name will delete `my-cookie` (with a dash) instead of `my_cookie` (with an underscore). - * Deprecated configuring encoders using `argon2i` as algorithm, use `sodium` instead + * Deprecated configuring encoders using `argon2i` as algorithm, use `auto` instead 4.2.0 ----- diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 60de7afc59..eda62ed9ac 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -394,9 +394,10 @@ class MainConfiguration implements ConfigurationInterface ->children() ->arrayNode('encoders') ->example([ - 'App\Entity\User1' => 'bcrypt', + 'App\Entity\User1' => 'auto', 'App\Entity\User2' => [ - 'algorithm' => 'bcrypt', + 'algorithm' => 'auto', + 'time_cost' => 8, 'cost' => 13, ], ]) @@ -416,11 +417,14 @@ class MainConfiguration implements ConfigurationInterface ->integerNode('cost') ->min(4) ->max(31) - ->defaultValue(13) + ->defaultNull() ->end() ->scalarNode('memory_cost')->defaultNull()->end() ->scalarNode('time_cost')->defaultNull()->end() - ->scalarNode('threads')->defaultNull()->end() + ->scalarNode('threads') + ->defaultNull() + ->setDeprecated('The "%path%.%node%" configuration key has no effect since Symfony 4.3 and will be removed in 5.0.') + ->end() ->scalarNode('id')->end() ->end() ->end() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 4519b66a94..0463e9011d 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -30,6 +30,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder; +use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Controller\UserValueResolver; @@ -559,20 +560,20 @@ class SecurityExtension extends Extension implements PrependExtensionInterface if ('bcrypt' === $config['algorithm']) { return [ 'class' => 'Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder', - 'arguments' => [$config['cost']], + 'arguments' => [$config['cost'] ?? 13], ]; } // Argon2i encoder if ('argon2i' === $config['algorithm']) { - @trigger_error('Configuring an encoder with "argon2i" as algorithm is deprecated since Symfony 4.3, use "sodium" instead.', E_USER_DEPRECATED); + @trigger_error('Configuring an encoder with "argon2i" as algorithm is deprecated since Symfony 4.3, use "auto" instead.', E_USER_DEPRECATED); if (!Argon2iPasswordEncoder::isSupported()) { if (\extension_loaded('sodium') && !\defined('SODIUM_CRYPTO_PWHASH_SALTBYTES')) { - throw new InvalidConfigurationException('The installed libsodium version does not have support for Argon2i. Use Bcrypt instead.'); + throw new InvalidConfigurationException('The installed libsodium version does not have support for Argon2i. Use "auto" instead.'); } - throw new InvalidConfigurationException('Argon2i algorithm is not supported. Install the libsodium extension or use BCrypt instead.'); + throw new InvalidConfigurationException('Argon2i algorithm is not supported. Install the libsodium extension or use "auto" instead.'); } return [ @@ -585,14 +586,28 @@ class SecurityExtension extends Extension implements PrependExtensionInterface ]; } + if ('native' === $config['algorithm']) { + return [ + 'class' => NativePasswordEncoder::class, + 'arguments' => [ + $config['time_cost'], + (($config['memory_cost'] ?? 0) << 10) ?: null, + $config['cost'], + ], + ]; + } + if ('sodium' === $config['algorithm']) { if (!SodiumPasswordEncoder::isSupported()) { - throw new InvalidConfigurationException('Libsodium is not available. Install the sodium extension or use BCrypt instead.'); + throw new InvalidConfigurationException('Libsodium is not available. Install the sodium extension or use "auto" instead.'); } return [ 'class' => SodiumPasswordEncoder::class, - 'arguments' => [], + 'arguments' => [ + $config['time_cost'], + (($config['memory_cost'] ?? 0) << 10) ?: null, + ], ]; } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php index 0727c79a52..1405ef4bd8 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php @@ -283,7 +283,7 @@ abstract class CompleteConfigurationTest extends TestCase 'hash_algorithm' => 'sha512', 'key_length' => 40, 'ignore_case' => false, - 'cost' => 13, + 'cost' => null, 'memory_cost' => null, 'time_cost' => null, 'threads' => null, @@ -295,7 +295,7 @@ abstract class CompleteConfigurationTest extends TestCase 'ignore_case' => false, 'encode_as_base64' => true, 'iterations' => 5000, - 'cost' => 13, + 'cost' => null, 'memory_cost' => null, 'time_cost' => null, 'threads' => null, @@ -309,6 +309,22 @@ abstract class CompleteConfigurationTest extends TestCase 'class' => 'Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder', 'arguments' => [15], ], + 'JMS\FooBundle\Entity\User7' => [ + 'class' => 'Symfony\Component\Security\Core\Encoder\NativePasswordEncoder', + 'arguments' => [8, 102400, 15], + ], + 'JMS\FooBundle\Entity\User8' => [ + 'algorithm' => 'auto', + 'hash_algorithm' => 'sha512', + 'key_length' => 40, + 'ignore_case' => false, + 'encode_as_base64' => true, + 'iterations' => 5000, + 'cost' => null, + 'memory_cost' => null, + 'time_cost' => null, + 'threads' => null, + ], ]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); } @@ -332,7 +348,7 @@ abstract class CompleteConfigurationTest extends TestCase 'hash_algorithm' => 'sha512', 'key_length' => 40, 'ignore_case' => false, - 'cost' => 13, + 'cost' => null, 'memory_cost' => null, 'time_cost' => null, 'threads' => null, @@ -344,7 +360,7 @@ abstract class CompleteConfigurationTest extends TestCase 'ignore_case' => false, 'encode_as_base64' => true, 'iterations' => 5000, - 'cost' => 13, + 'cost' => null, 'memory_cost' => null, 'time_cost' => null, 'threads' => null, @@ -360,7 +376,19 @@ abstract class CompleteConfigurationTest extends TestCase ], 'JMS\FooBundle\Entity\User7' => [ 'class' => 'Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder', - 'arguments' => [], + 'arguments' => [8, 128 * 1024 * 1024], + ], + 'JMS\FooBundle\Entity\User8' => [ + 'algorithm' => 'auto', + 'hash_algorithm' => 'sha512', + 'key_length' => 40, + 'ignore_case' => false, + 'encode_as_base64' => true, + 'iterations' => 5000, + 'cost' => null, + 'memory_cost' => null, + 'time_cost' => null, + 'threads' => null, ], ]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); } @@ -368,7 +396,7 @@ abstract class CompleteConfigurationTest extends TestCase /** * @group legacy * - * @expectedDeprecation Configuring an encoder with "argon2i" as algorithm is deprecated since Symfony 4.3, use "sodium" instead. + * @expectedDeprecation Configuring an encoder with "argon2i" as algorithm is deprecated since Symfony 4.3, use "auto" instead. */ public function testEncodersWithArgon2i() { @@ -390,7 +418,7 @@ abstract class CompleteConfigurationTest extends TestCase 'hash_algorithm' => 'sha512', 'key_length' => 40, 'ignore_case' => false, - 'cost' => 13, + 'cost' => null, 'memory_cost' => null, 'time_cost' => null, 'threads' => null, @@ -402,7 +430,7 @@ abstract class CompleteConfigurationTest extends TestCase 'ignore_case' => false, 'encode_as_base64' => true, 'iterations' => 5000, - 'cost' => 13, + 'cost' => null, 'memory_cost' => null, 'time_cost' => null, 'threads' => null, @@ -420,6 +448,18 @@ abstract class CompleteConfigurationTest extends TestCase 'class' => 'Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder', 'arguments' => [256, 1, 2], ], + 'JMS\FooBundle\Entity\User8' => [ + 'algorithm' => 'auto', + 'hash_algorithm' => 'sha512', + 'key_length' => 40, + 'ignore_case' => false, + 'encode_as_base64' => true, + 'iterations' => 5000, + 'cost' => null, + 'memory_cost' => null, + 'time_cost' => null, + 'threads' => null, + ], ]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php index f420d4d8af..06afe665d2 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php @@ -25,6 +25,15 @@ $container->loadFromExtension('security', [ 'algorithm' => 'bcrypt', 'cost' => 15, ], + 'JMS\FooBundle\Entity\User7' => [ + 'algorithm' => 'native', + 'time_cost' => 8, + 'memory_cost' => 100, + 'cost' => 15, + ], + 'JMS\FooBundle\Entity\User8' => [ + 'algorithm' => 'auto', + ], ], 'providers' => [ 'default' => [ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/sodium_encoder.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/sodium_encoder.php index e32969df23..5fdef4b804 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/sodium_encoder.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/sodium_encoder.php @@ -6,6 +6,8 @@ $container->loadFromExtension('security', [ 'encoders' => [ 'JMS\FooBundle\Entity\User7' => [ 'algorithm' => 'sodium', + 'time_cost' => 8, + 'memory_cost' => 128 * 1024, ], ], ]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml index 97631ae318..9a3cae7159 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml @@ -18,6 +18,10 @@ + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/sodium_encoder.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/sodium_encoder.xml index 3fb4721300..11682f7c95 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/sodium_encoder.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/sodium_encoder.xml @@ -10,7 +10,7 @@ - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml index 9a1535ebb2..a51b0c49ca 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml @@ -18,6 +18,13 @@ security: JMS\FooBundle\Entity\User6: algorithm: bcrypt cost: 15 + JMS\FooBundle\Entity\User7: + algorithm: native + time_cost: 8 + memory_cost: 100 + cost: 15 + JMS\FooBundle\Entity\User8: + algorithm: auto providers: default: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/sodium_encoder.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/sodium_encoder.yml index 175d945b7d..2d70ef0d9b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/sodium_encoder.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/sodium_encoder.yml @@ -5,3 +5,5 @@ security: encoders: JMS\FooBundle\Entity\User7: algorithm: sodium + time_cost: 8 + memory_cost: 131072 diff --git a/src/Symfony/Component/Security/CHANGELOG.md b/src/Symfony/Component/Security/CHANGELOG.md index 69f4c6301a..5dc7856073 100644 --- a/src/Symfony/Component/Security/CHANGELOG.md +++ b/src/Symfony/Component/Security/CHANGELOG.md @@ -4,6 +4,8 @@ CHANGELOG 4.3.0 ----- + * Added methods `__serialize` and `__unserialize` to the `TokenInterface` + * Added `SodiumPasswordEncoder` and `NativePasswordEncoder` * The `Role` and `SwitchUserRole` classes are deprecated and will be removed in 5.0. Use strings for roles instead. * The `getReachableRoles()` method of the `RoleHierarchyInterface` is deprecated and will be removed in 5.0. @@ -19,8 +21,7 @@ CHANGELOG * Dispatch `AuthenticationFailureEvent` on `security.authentication.failure` * Dispatch `InteractiveLoginEvent` on `security.interactive_login` * Dispatch `SwitchUserEvent` on `security.switch_user` - * deprecated `Argon2iPasswordEncoder`, use `SodiumPasswordEncoder` instead - * Added methods `__serialize` and `__unserialize` to the `TokenInterface` + * Deprecated `Argon2iPasswordEncoder`, use `SodiumPasswordEncoder` 4.2.0 ----- diff --git a/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php b/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php index 01dd33f053..7d87c6f904 100644 --- a/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php +++ b/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php @@ -84,6 +84,10 @@ class EncoderFactory implements EncoderFactoryInterface private function getEncoderConfigFromAlgorithm($config) { + if ('auto' === $config['algorithm']) { + $config['algorithm'] = SodiumPasswordEncoder::isSupported() ? 'sodium' : 'native'; + } + switch ($config['algorithm']) { case 'plaintext': return [ @@ -108,10 +112,23 @@ class EncoderFactory implements EncoderFactoryInterface 'arguments' => [$config['cost']], ]; + case 'native': + return [ + 'class' => NativePasswordEncoder::class, + 'arguments' => [ + $config['time_cost'] ?? null, + (($config['memory_cost'] ?? 0) << 10) ?: null, + $config['cost'] ?? null, + ], + ]; + case 'sodium': return [ 'class' => SodiumPasswordEncoder::class, - 'arguments' => [], + 'arguments' => [ + $config['time_cost'] ?? null, + (($config['memory_cost'] ?? 0) << 10) ?: null, + ], ]; /* @deprecated since Symfony 4.3 */ diff --git a/src/Symfony/Component/Security/Core/Encoder/NativePasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/NativePasswordEncoder.php new file mode 100644 index 0000000000..a99d064eeb --- /dev/null +++ b/src/Symfony/Component/Security/Core/Encoder/NativePasswordEncoder.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Encoder; + +use Symfony\Component\Security\Core\Exception\BadCredentialsException; + +/** + * Hashes passwords using password_hash(). + * + * @author Elnur Abdurrakhimov + * @author Terje Bråten + * @author Nicolas Grekas + */ +final class NativePasswordEncoder implements PasswordEncoderInterface, SelfSaltingEncoderInterface +{ + private const MAX_PASSWORD_LENGTH = 4096; + + private $algo; + private $options; + + public function __construct(int $opsLimit = null, int $memLimit = null, int $cost = null) + { + $cost = $cost ?? 13; + $opsLimit = $opsLimit ?? max(6, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE : 6); + $memLimit = $memLimit ?? max(64 * 1024 * 1024, \defined('SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE : 64 * 1024 * 1024); + + if (2 > $opsLimit) { + throw new \InvalidArgumentException('$opsLimit must be 2 or greater.'); + } + + if (10 * 1024 > $memLimit) { + throw new \InvalidArgumentException('$memLimit must be 10k or greater.'); + } + + if ($cost < 4 || 31 < $cost) { + throw new \InvalidArgumentException('$cost must be in the range of 4-31.'); + } + + $this->algo = \defined('PASSWORD_ARGON2I') ? max(PASSWORD_DEFAULT, \defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_ARGON2I) : PASSWORD_DEFAULT; + $this->options = [ + 'cost' => $cost, + 'time_cost' => $opsLimit, + 'memory_cost' => $memLimit >> 10, + 'threads' => 1, + ]; + } + + /** + * {@inheritdoc} + */ + public function encodePassword($raw, $salt) + { + if (\strlen($raw) > self::MAX_PASSWORD_LENGTH) { + throw new BadCredentialsException('Invalid password.'); + } + + // Ignore $salt, the auto-generated one is always the best + + $encoded = password_hash($raw, $this->algo, $this->options); + + if (72 < \strlen($raw) && 0 === strpos($encoded, '$2')) { + // BCrypt encodes only the first 72 chars + throw new BadCredentialsException('Invalid password.'); + } + + return $encoded; + } + + /** + * {@inheritdoc} + */ + public function isPasswordValid($encoded, $raw, $salt) + { + if (72 < \strlen($raw) && 0 === strpos($encoded, '$2')) { + // BCrypt encodes only the first 72 chars + return false; + } + + return \strlen($raw) <= self::MAX_PASSWORD_LENGTH && password_verify($raw, $encoded); + } +} diff --git a/src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php index febca05dd0..96fbdca173 100644 --- a/src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php +++ b/src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php @@ -20,11 +20,32 @@ use Symfony\Component\Security\Core\Exception\LogicException; * @author Robin Chalas * @author Zan Baldwin * @author Dominik Müller - * - * @final */ -class SodiumPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface +final class SodiumPasswordEncoder implements PasswordEncoderInterface, SelfSaltingEncoderInterface { + private const MAX_PASSWORD_LENGTH = 4096; + + private $opsLimit; + private $memLimit; + + public function __construct(int $opsLimit = null, int $memLimit = null) + { + if (!self::isSupported()) { + throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.'); + } + + $this->opsLimit = $opsLimit ?? max(6, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE : 6); + $this->memLimit = $memLimit ?? max(64 * 1024 * 1024, \defined('SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE : 64 * 1024 * 2014); + + if (2 > $this->opsLimit) { + throw new \InvalidArgumentException('$opsLimit must be 2 or greater.'); + } + + if (10 * 1024 > $this->memLimit) { + throw new \InvalidArgumentException('$memLimit must be 10k or greater.'); + } + } + public static function isSupported(): bool { if (\class_exists('ParagonIE_Sodium_Compat') && \method_exists('ParagonIE_Sodium_Compat', 'crypto_pwhash_is_available')) { @@ -39,24 +60,16 @@ class SodiumPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEn */ public function encodePassword($raw, $salt) { - if ($this->isPasswordTooLong($raw)) { + if (\strlen($raw) > self::MAX_PASSWORD_LENGTH) { throw new BadCredentialsException('Invalid password.'); } if (\function_exists('sodium_crypto_pwhash_str')) { - return \sodium_crypto_pwhash_str( - $raw, - \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, - \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE - ); + return \sodium_crypto_pwhash_str($raw, $this->opsLimit, $this->memLimit); } if (\extension_loaded('libsodium')) { - return \Sodium\crypto_pwhash_str( - $raw, - \Sodium\CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, - \Sodium\CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE - ); + return \Sodium\crypto_pwhash_str($raw, $this->opsLimit, $this->memLimit); } throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.'); @@ -67,7 +80,7 @@ class SodiumPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEn */ public function isPasswordValid($encoded, $raw, $salt) { - if ($this->isPasswordTooLong($raw)) { + if (\strlen($raw) > self::MAX_PASSWORD_LENGTH) { return false; } diff --git a/src/Symfony/Component/Security/Core/Tests/Encoder/NativePasswordEncoderTest.php b/src/Symfony/Component/Security/Core/Tests/Encoder/NativePasswordEncoderTest.php new file mode 100644 index 0000000000..681b91a1ee --- /dev/null +++ b/src/Symfony/Component/Security/Core/Tests/Encoder/NativePasswordEncoderTest.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Tests\Encoder; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; + +/** + * @author Elnur Abdurrakhimov + */ +class NativePasswordEncoderTest extends TestCase +{ + /** + * @expectedException \InvalidArgumentException + */ + public function testCostBelowRange() + { + new NativePasswordEncoder(null, null, 3); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testCostAboveRange() + { + new NativePasswordEncoder(null, null, 32); + } + + /** + * @dataProvider validRangeData + */ + public function testCostInRange($cost) + { + $this->assertInstanceOf(NativePasswordEncoder::class, new NativePasswordEncoder(null, null, $cost)); + } + + public function validRangeData() + { + $costs = range(4, 31); + array_walk($costs, function (&$cost) { $cost = [$cost]; }); + + return $costs; + } + + public function testValidation() + { + $encoder = new NativePasswordEncoder(); + $result = $encoder->encodePassword('password', null); + $this->assertTrue($encoder->isPasswordValid($result, 'password', null)); + $this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword', null)); + } + + public function testCheckPasswordLength() + { + $encoder = new NativePasswordEncoder(null, null, 4); + $result = password_hash(str_repeat('a', 72), PASSWORD_BCRYPT, ['cost' => 4]); + + $this->assertFalse($encoder->isPasswordValid($result, str_repeat('a', 73), 'salt')); + $this->assertTrue($encoder->isPasswordValid($result, str_repeat('a', 72), 'salt')); + } +}