From 6712d1e5040c5959eafe7641f469fffc8003c093 Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Sun, 27 Oct 2019 11:08:13 +0100 Subject: [PATCH] [Security] Allow to set a fixed algorithm --- UPGRADE-4.3.md | 5 -- UPGRADE-5.0.md | 1 - .../Bundle/SecurityBundle/CHANGELOG.md | 5 +- .../DependencyInjection/SecurityExtension.php | 50 +++++++------- .../CompleteConfigurationTest.php | 21 ++---- .../Fixtures/php/argon2i_encoder.php | 1 - .../Fixtures/xml/argon2i_encoder.xml | 2 +- .../Fixtures/yml/argon2i_encoder.yml | 1 - .../UserPasswordEncoderCommandTest.php | 69 +++++++++++++++---- .../app/PasswordEncode/argon2id.yml | 7 ++ src/Symfony/Component/Security/CHANGELOG.md | 1 + .../Security/Core/Encoder/EncoderFactory.php | 46 ++++++++----- .../Core/Encoder/NativePasswordEncoder.php | 7 +- .../Encoder/NativePasswordEncoderTest.php | 8 +++ 14 files changed, 141 insertions(+), 83 deletions(-) create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/argon2id.yml diff --git a/UPGRADE-4.3.md b/UPGRADE-4.3.md index 86155031fa..f0bb56d78a 100644 --- a/UPGRADE-4.3.md +++ b/UPGRADE-4.3.md @@ -209,11 +209,6 @@ Security * Not implementing the methods `__serialize` and `__unserialize` in classes implementing the `TokenInterface` is deprecated -SecurityBundle --------------- - - * Configuring encoders using `argon2i` or `bcrypt` as algorithm has been deprecated, use `auto` instead. - TwigBridge ---------- diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index 44557336d7..125d9baccd 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -509,7 +509,6 @@ SecurityBundle changed to underscores. Before: `my-cookie` deleted the `my_cookie` cookie (with an underscore). After: `my-cookie` deletes the `my-cookie` cookie (with a dash). - * Configuring encoders using `argon2i` or `bcrypt` as algorithm is not supported anymore, use `auto` instead. Serializer ---------- diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index 5d00820859..b036de7cd2 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -2,8 +2,10 @@ CHANGELOG ========= 4.4.0 +----- -* Deprecated the usage of "query_string" without a "search_dn" and a "search_password" config key in Ldap factories. + * Added new `argon2id` encoder, undeprecated the `bcrypt` and `argon2i` ones (using `auto` is still recommended by default.) + * Deprecated the usage of "query_string" without a "search_dn" and a "search_password" config key in Ldap factories. 4.3.0 ----- @@ -14,7 +16,6 @@ CHANGELOG 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 `auto` instead 4.2.0 ----- diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index a2aa67c958..b238962eb6 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -28,7 +28,6 @@ use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; 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; @@ -538,34 +537,37 @@ class SecurityExtension extends Extension implements PrependExtensionInterface // bcrypt encoder if ('bcrypt' === $config['algorithm']) { - @trigger_error('Configuring an encoder with "bcrypt" as algorithm is deprecated since Symfony 4.3, use "auto" instead.', E_USER_DEPRECATED); + $config['algorithm'] = 'native'; + $config['native_algorithm'] = PASSWORD_BCRYPT; - return [ - 'class' => 'Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder', - 'arguments' => [$config['cost'] ?? 13], - ]; + return $this->createEncoder($config); } // Argon2i encoder if ('argon2i' === $config['algorithm']) { - @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 "auto" instead.'); - } - - throw new InvalidConfigurationException('Argon2i algorithm is not supported. Install the libsodium extension or use "auto" instead.'); + if (SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) { + $config['algorithm'] = 'sodium'; + } elseif (\defined('PASSWORD_ARGON2I')) { + $config['algorithm'] = 'native'; + $config['native_algorithm'] = PASSWORD_ARGON2I; + } else { + throw new InvalidConfigurationException(sprintf('Algorithm "argon2i" is not available. Either use %s"auto" or upgrade to PHP 7.2+ instead.', \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13') ? '"argon2id", ' : '')); } - return [ - 'class' => 'Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder', - 'arguments' => [ - $config['memory_cost'], - $config['time_cost'], - $config['threads'], - ], - ]; + return $this->createEncoder($config); + } + + if ('argon2id' === $config['algorithm']) { + if (($hasSodium = SodiumPasswordEncoder::isSupported()) && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) { + $config['algorithm'] = 'sodium'; + } elseif (\defined('PASSWORD_ARGON2ID')) { + $config['algorithm'] = 'native'; + $config['native_algorithm'] = PASSWORD_ARGON2ID; + } else { + throw new InvalidConfigurationException(sprintf('Algorithm "argon2id" is not available. Either use %s"auto", upgrade to PHP 7.3+ or use libsodium 1.0.15+ instead.', \defined('PASSWORD_ARGON2I') || $hasSodium ? '"argon2i", ' : '')); + } + + return $this->createEncoder($config); } if ('native' === $config['algorithm']) { @@ -574,8 +576,8 @@ class SecurityExtension extends Extension implements PrependExtensionInterface 'arguments' => [ $config['time_cost'], (($config['memory_cost'] ?? 0) << 10) ?: null, - $config['cost'], - ], + $config['cost'] + ] + (isset($config['native_algorithm']) ? [3 => $config['native_algorithm']] : []), ]; } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php index 859ea51b74..66a6c6842a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php @@ -18,7 +18,7 @@ use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; -use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder; +use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; abstract class CompleteConfigurationTest extends TestCase @@ -377,14 +377,9 @@ abstract class CompleteConfigurationTest extends TestCase ]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); } - /** - * @group legacy - * - * @expectedDeprecation Configuring an encoder with "argon2i" as algorithm is deprecated since Symfony 4.3, use "auto" instead. - */ public function testEncodersWithArgon2i() { - if (!Argon2iPasswordEncoder::isSupported()) { + if (!($sodium = SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) { $this->markTestSkipped('Argon2i algorithm is not supported.'); } @@ -429,19 +424,15 @@ abstract class CompleteConfigurationTest extends TestCase 'arguments' => [8, 102400, 15], ], 'JMS\FooBundle\Entity\User7' => [ - 'class' => 'Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder', - 'arguments' => [256, 1, 2], + 'class' => $sodium ? SodiumPasswordEncoder::class : NativePasswordEncoder::class, + 'arguments' => $sodium ? [256, 1] : [1, 262144, null, \PASSWORD_ARGON2I], ], ]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); } - /** - * @group legacy - */ public function testEncodersWithBCrypt() { $container = $this->getContainer('bcrypt_encoder'); - $this->assertEquals([[ 'JMS\FooBundle\Entity\User1' => [ 'class' => 'Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder', @@ -481,8 +472,8 @@ abstract class CompleteConfigurationTest extends TestCase 'arguments' => [8, 102400, 15], ], 'JMS\FooBundle\Entity\User7' => [ - 'class' => 'Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder', - 'arguments' => [15], + 'class' => NativePasswordEncoder::class, + 'arguments' => [null, null, 15, \PASSWORD_BCRYPT], ], ]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2i_encoder.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2i_encoder.php index 88543b7da9..7276f97e6b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2i_encoder.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2i_encoder.php @@ -8,7 +8,6 @@ $container->loadFromExtension('security', [ 'algorithm' => 'argon2i', 'memory_cost' => 256, 'time_cost' => 1, - 'threads' => 2, ], ], ]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2i_encoder.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2i_encoder.xml index 21b0c27443..6a7c2a5041 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2i_encoder.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2i_encoder.xml @@ -10,7 +10,7 @@ - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2i_encoder.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2i_encoder.yml index 6abd4d0798..cadf8eb1e9 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2i_encoder.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2i_encoder.yml @@ -7,4 +7,3 @@ security: algorithm: argon2i memory_cost: 256 time_cost: 1 - threads: 2 diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php index 25307c88d6..50ab3abb81 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php @@ -15,8 +15,6 @@ use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand; use Symfony\Component\Console\Application as ConsoleApplication; use Symfony\Component\Console\Tester\CommandTester; -use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder; -use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder; @@ -55,9 +53,6 @@ class UserPasswordEncoderCommandTest extends AbstractWebTestCase $this->assertEquals($statusCode, 1); } - /** - * @group legacy - */ public function testEncodePasswordBcrypt() { $this->setupBcrypt(); @@ -70,18 +65,15 @@ class UserPasswordEncoderCommandTest extends AbstractWebTestCase $output = $this->passwordEncoderCommandTester->getDisplay(); $this->assertStringContainsString('Password encoding succeeded', $output); - $encoder = new BCryptPasswordEncoder(17); + $encoder = new NativePasswordEncoder(null, null, 17, PASSWORD_BCRYPT); preg_match('# Encoded password\s{1,}([\w+\/$.]+={0,2})\s+#', $output, $matches); $hash = $matches[1]; $this->assertTrue($encoder->isPasswordValid($hash, 'password', null)); } - /** - * @group legacy - */ public function testEncodePasswordArgon2i() { - if (!Argon2iPasswordEncoder::isSupported()) { + if (!($sodium = SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) { $this->markTestSkipped('Argon2i algorithm not available.'); } $this->setupArgon2i(); @@ -94,7 +86,28 @@ class UserPasswordEncoderCommandTest extends AbstractWebTestCase $output = $this->passwordEncoderCommandTester->getDisplay(); $this->assertStringContainsString('Password encoding succeeded', $output); - $encoder = new Argon2iPasswordEncoder(); + $encoder = $sodium ? new SodiumPasswordEncoder() : new NativePasswordEncoder(null, null, null, PASSWORD_ARGON2I); + preg_match('# Encoded password\s+(\$argon2i?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches); + $hash = $matches[1]; + $this->assertTrue($encoder->isPasswordValid($hash, 'password', null)); + } + + public function testEncodePasswordArgon2id() + { + if (!($sodium = (SodiumPasswordEncoder::isSupported() && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13'))) && !\defined('PASSWORD_ARGON2ID')) { + $this->markTestSkipped('Argon2id algorithm not available.'); + } + $this->setupArgon2id(); + $this->passwordEncoderCommandTester->execute([ + 'command' => 'security:encode-password', + 'password' => 'password', + 'user-class' => 'Custom\Class\Argon2id\User', + ], ['interactive' => false]); + + $output = $this->passwordEncoderCommandTester->getDisplay(); + $this->assertStringContainsString('Password encoding succeeded', $output); + + $encoder = $sodium ? new SodiumPasswordEncoder() : new NativePasswordEncoder(null, null, null, PASSWORD_ARGON2ID); preg_match('# Encoded password\s+(\$argon2id?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches); $hash = $matches[1]; $this->assertTrue($encoder->isPasswordValid($hash, 'password', null)); @@ -195,12 +208,9 @@ class UserPasswordEncoderCommandTest extends AbstractWebTestCase $this->assertStringNotContainsString(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); } - /** - * @group legacy - */ public function testEncodePasswordArgon2iOutput() { - if (!Argon2iPasswordEncoder::isSupported()) { + if (!(SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) { $this->markTestSkipped('Argon2i algorithm not available.'); } @@ -214,6 +224,22 @@ class UserPasswordEncoderCommandTest extends AbstractWebTestCase $this->assertStringNotContainsString(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); } + public function testEncodePasswordArgon2idOutput() + { + if (!(SodiumPasswordEncoder::isSupported() && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2ID')) { + $this->markTestSkipped('Argon2id algorithm not available.'); + } + + $this->setupArgon2id(); + $this->passwordEncoderCommandTester->execute([ + 'command' => 'security:encode-password', + 'password' => 'p@ssw0rd', + 'user-class' => 'Custom\Class\Argon2id\User', + ], ['interactive' => false]); + + $this->assertStringNotContainsString(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); + } + public function testEncodePasswordSodiumOutput() { if (!SodiumPasswordEncoder::isSupported()) { @@ -317,6 +343,19 @@ EOTXT $this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand); } + private function setupArgon2id() + { + putenv('COLUMNS='.(119 + \strlen(PHP_EOL))); + $kernel = $this->createKernel(['test_case' => 'PasswordEncode', 'root_config' => 'argon2id.yml']); + $kernel->boot(); + + $application = new Application($kernel); + + $passwordEncoderCommand = $application->get('security:encode-password'); + + $this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand); + } + private function setupBcrypt() { putenv('COLUMNS='.(119 + \strlen(PHP_EOL))); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/argon2id.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/argon2id.yml new file mode 100644 index 0000000000..481262acb7 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/argon2id.yml @@ -0,0 +1,7 @@ +imports: + - { resource: config.yml } + +security: + encoders: + Custom\Class\Argon2id\User: + algorithm: argon2id diff --git a/src/Symfony/Component/Security/CHANGELOG.md b/src/Symfony/Component/Security/CHANGELOG.md index 3e9bbe14f9..cc30a5608f 100644 --- a/src/Symfony/Component/Security/CHANGELOG.md +++ b/src/Symfony/Component/Security/CHANGELOG.md @@ -13,6 +13,7 @@ CHANGELOG * Marked all dispatched event classes as `@final` * Deprecated returning a non-boolean value when implementing `Guard\AuthenticatorInterface::checkCredentials()`. * Deprecated passing more than one attribute to `AccessDecisionManager::decide()` and `AuthorizationChecker::isGranted()` + * Added new `argon2id` encoder, undeprecated the `bcrypt` and `argon2i` ones (using `auto` is still recommended by default.) 4.3.0 ----- diff --git a/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php b/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php index c3121a241f..c2dc5609fb 100644 --- a/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php +++ b/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Security\Core\Encoder; +use Symfony\Component\Security\Core\Exception\LogicException; + /** * A generic encoder factory implementation. * @@ -114,12 +116,11 @@ class EncoderFactory implements EncoderFactoryInterface ], ]; - /* @deprecated since Symfony 4.3 */ case 'bcrypt': - return [ - 'class' => BCryptPasswordEncoder::class, - 'arguments' => [$config['cost']], - ]; + $config['algorithm'] = 'native'; + $config['native_algorithm'] = PASSWORD_BCRYPT; + + return $this->getEncoderConfigFromAlgorithm($config); case 'native': return [ @@ -127,8 +128,8 @@ class EncoderFactory implements EncoderFactoryInterface 'arguments' => [ $config['time_cost'] ?? null, (($config['memory_cost'] ?? 0) << 10) ?: null, - $config['cost'] ?? null, - ], + $config['cost'] ?? null + ] + (isset($config['native_algorithm']) ? [3 => $config['native_algorithm']] : []), ]; case 'sodium': @@ -140,16 +141,29 @@ class EncoderFactory implements EncoderFactoryInterface ], ]; - /* @deprecated since Symfony 4.3 */ case 'argon2i': - return [ - 'class' => Argon2iPasswordEncoder::class, - 'arguments' => [ - $config['memory_cost'], - $config['time_cost'], - $config['threads'], - ], - ]; + if (SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) { + $config['algorithm'] = 'sodium'; + } elseif (\defined('PASSWORD_ARGON2I')) { + $config['algorithm'] = 'native'; + $config['native_algorithm'] = PASSWORD_ARGON2I; + } else { + throw new LogicException(sprintf('Algorithm "argon2i" is not available. Either use %s"auto" or upgrade to PHP 7.2+ instead.', \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13') ? '"argon2id", ' : '')); + } + + return $this->getEncoderConfigFromAlgorithm($config); + + case 'argon2id': + if (($hasSodium = SodiumPasswordEncoder::isSupported()) && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) { + $config['algorithm'] = 'sodium'; + } elseif (\defined('PASSWORD_ARGON2ID')) { + $config['algorithm'] = 'native'; + $config['native_algorithm'] = PASSWORD_ARGON2ID; + } else { + throw new LogicException(sprintf('Algorithm "argon2id" is not available. Either use %s"auto", upgrade to PHP 7.3+ or use libsodium 1.0.15+ instead.', \defined('PASSWORD_ARGON2I') || $hasSodium ? '"argon2i", ' : '')); + } + + return $this->getEncoderConfigFromAlgorithm($config); } return [ diff --git a/src/Symfony/Component/Security/Core/Encoder/NativePasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/NativePasswordEncoder.php index bc900427af..f805dd4d0f 100644 --- a/src/Symfony/Component/Security/Core/Encoder/NativePasswordEncoder.php +++ b/src/Symfony/Component/Security/Core/Encoder/NativePasswordEncoder.php @@ -27,7 +27,10 @@ final class NativePasswordEncoder implements PasswordEncoderInterface, SelfSalti private $algo; private $options; - public function __construct(int $opsLimit = null, int $memLimit = null, int $cost = null) + /** + * @param string|null $algo An algorithm supported by password_hash() or null to use the stronger available algorithm + */ + public function __construct(int $opsLimit = null, int $memLimit = null, int $cost = null, string $algo = null) { $cost = $cost ?? 13; $opsLimit = $opsLimit ?? max(4, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE : 4); @@ -45,7 +48,7 @@ final class NativePasswordEncoder implements PasswordEncoderInterface, SelfSalti 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->algo = $algo ?? (\defined('PASSWORD_ARGON2I') ? max(PASSWORD_DEFAULT, \defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_ARGON2I) : PASSWORD_DEFAULT); $this->options = [ 'cost' => $cost, 'time_cost' => $opsLimit, diff --git a/src/Symfony/Component/Security/Core/Tests/Encoder/NativePasswordEncoderTest.php b/src/Symfony/Component/Security/Core/Tests/Encoder/NativePasswordEncoderTest.php index ae65f99764..8b15a47b82 100644 --- a/src/Symfony/Component/Security/Core/Tests/Encoder/NativePasswordEncoderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Encoder/NativePasswordEncoderTest.php @@ -55,6 +55,14 @@ class NativePasswordEncoderTest extends TestCase $this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword', null)); } + public function testConfiguredAlgorithm() + { + $encoder = new NativePasswordEncoder(null, null, null, PASSWORD_BCRYPT); + $result = $encoder->encodePassword('password', null); + $this->assertTrue($encoder->isPasswordValid($result, 'password', null)); + $this->assertStringStartsWith('$2', $result); + } + public function testCheckPasswordLength() { $encoder = new NativePasswordEncoder(null, null, 4);