From 0c82173b241723d6183b57a7ab0b369209128007 Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Mon, 17 Dec 2018 15:40:40 +0100 Subject: [PATCH] [Security] Add Argon2idPasswordEncoder --- UPGRADE-4.3.md | 11 ++- UPGRADE-5.0.md | 5 ++ .../Bundle/SecurityBundle/CHANGELOG.md | 3 + .../DependencyInjection/SecurityExtension.php | 19 +++++ .../CompleteConfigurationTest.php | 56 +++++++++++- .../Fixtures/php/argon2id_encoder.php | 14 +++ .../Fixtures/xml/argon2id_encoder.xml | 16 ++++ .../Fixtures/yml/argon2id_encoder.yml | 10 +++ .../UserPasswordEncoderCommandTest.php | 57 ++++++++++++- .../app/PasswordEncode/argon2id.yml | 7 ++ src/Symfony/Component/Security/CHANGELOG.md | 3 + .../Security/Core/Encoder/Argon2Trait.php | 52 ++++++++++++ .../Core/Encoder/Argon2iPasswordEncoder.php | 66 +++++--------- .../Core/Encoder/Argon2idPasswordEncoder.php | 85 +++++++++++++++++++ .../Security/Core/Encoder/EncoderFactory.php | 9 ++ .../Encoder/Argon2iPasswordEncoderTest.php | 2 +- .../Encoder/Argon2idPasswordEncoderTest.php | 65 ++++++++++++++ 17 files changed, 430 insertions(+), 50 deletions(-) create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2id_encoder.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2id_encoder.xml create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2id_encoder.yml create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/argon2id.yml create mode 100644 src/Symfony/Component/Security/Core/Encoder/Argon2Trait.php create mode 100644 src/Symfony/Component/Security/Core/Encoder/Argon2idPasswordEncoder.php create mode 100644 src/Symfony/Component/Security/Core/Tests/Encoder/Argon2idPasswordEncoderTest.php diff --git a/UPGRADE-4.3.md b/UPGRADE-4.3.md index 4cbf221623..9967dda854 100644 --- a/UPGRADE-4.3.md +++ b/UPGRADE-4.3.md @@ -145,8 +145,17 @@ Security } ``` + * Using `Argon2iPasswordEncoder` while only the `argon2id` algorithm is supported + is deprecated, use `Argon2idPasswordEncoder` instead + +SecurityBundle +-------------- + + * Configuring encoders using `argon2i` as algorithm while only `argon2id` is + supported is deprecated, use `argon2id` instead + TwigBridge -========== +---------- * deprecated the `$requestStack` and `$requestContext` arguments of the `HttpFoundationExtension`, pass a `Symfony\Component\HttpFoundation\UrlHelper` diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index aa44884623..50eb486153 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -323,6 +323,9 @@ Security } ``` + * Using `Argon2iPasswordEncoder` while only the `argon2id` algorithm is supported + now throws a \LogicException`, use `Argon2idPasswordEncoder` instead + SecurityBundle -------------- @@ -342,6 +345,8 @@ 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` as algorithm while only `argon2id` is supported + now throws a `\LogicException`, use `argon2id` instead Serializer ---------- diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index 84219a99f0..fd0edcb7dd 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -8,6 +8,9 @@ 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 while only `argon2id` is supported, + use `argon2id` instead + 4.2.0 ----- diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 4d74daa4d4..b8b3358c02 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -29,6 +29,7 @@ use Symfony\Component\DependencyInjection\Parameter; 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\Argon2idPasswordEncoder; use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Controller\UserValueResolver; @@ -570,6 +571,8 @@ class SecurityExtension extends Extension implements PrependExtensionInterface } throw new InvalidConfigurationException('Argon2i algorithm is not supported. Install the libsodium extension or use BCrypt instead.'); + } elseif (\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) { + @trigger_error('Configuring an encoder based on the "argon2i" algorithm while only "argon2id" is supported is deprecated since Symfony 4.3, use "argon2id" instead.', E_USER_DEPRECATED); } return [ @@ -582,6 +585,22 @@ class SecurityExtension extends Extension implements PrependExtensionInterface ]; } + // Argon2id encoder + if ('argon2id' === $config['algorithm']) { + if (!Argon2idPasswordEncoder::isSupported()) { + throw new InvalidConfigurationException('Argon2i algorithm is not supported. Install the libsodium extension or use BCrypt instead.'); + } + + return [ + 'class' => Argon2idPasswordEncoder::class, + 'arguments' => [ + $config['memory_cost'], + $config['time_cost'], + $config['threads'], + ], + ]; + } + // run-time configured encoder return $config; } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php index f9102edfb0..5c95500dc3 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php @@ -18,6 +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\Argon2idPasswordEncoder; use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder; abstract class CompleteConfigurationTest extends TestCase @@ -313,7 +314,7 @@ abstract class CompleteConfigurationTest extends TestCase public function testEncodersWithLibsodium() { - if (!Argon2iPasswordEncoder::isSupported()) { + if (!Argon2iPasswordEncoder::isSupported() || \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) { $this->markTestSkipped('Argon2i algorithm is not supported.'); } @@ -364,6 +365,59 @@ abstract class CompleteConfigurationTest extends TestCase ]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); } + public function testEncodersWithArgon2id() + { + if (!Argon2idPasswordEncoder::isSupported()) { + $this->markTestSkipped('Argon2i algorithm is not supported.'); + } + + $container = $this->getContainer('argon2id_encoder'); + + $this->assertEquals([[ + 'JMS\FooBundle\Entity\User1' => [ + 'class' => 'Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder', + 'arguments' => [false], + ], + 'JMS\FooBundle\Entity\User2' => [ + 'algorithm' => 'sha1', + 'encode_as_base64' => false, + 'iterations' => 5, + 'hash_algorithm' => 'sha512', + 'key_length' => 40, + 'ignore_case' => false, + 'cost' => 13, + 'memory_cost' => null, + 'time_cost' => null, + 'threads' => null, + ], + 'JMS\FooBundle\Entity\User3' => [ + 'algorithm' => 'md5', + 'hash_algorithm' => 'sha512', + 'key_length' => 40, + 'ignore_case' => false, + 'encode_as_base64' => true, + 'iterations' => 5000, + 'cost' => 13, + 'memory_cost' => null, + 'time_cost' => null, + 'threads' => null, + ], + 'JMS\FooBundle\Entity\User4' => new Reference('security.encoder.foo'), + 'JMS\FooBundle\Entity\User5' => [ + 'class' => 'Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder', + 'arguments' => ['sha1', false, 5, 30], + ], + 'JMS\FooBundle\Entity\User6' => [ + 'class' => 'Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder', + 'arguments' => [15], + ], + 'JMS\FooBundle\Entity\User7' => [ + 'class' => 'Symfony\Component\Security\Core\Encoder\Argon2idPasswordEncoder', + 'arguments' => [256, 1, 2], + ], + ]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); + } + public function testRememberMeThrowExceptionsDefault() { $container = $this->getContainer('container1'); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2id_encoder.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2id_encoder.php new file mode 100644 index 0000000000..df63deb92e --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2id_encoder.php @@ -0,0 +1,14 @@ +load('container1.php', $container); + +$container->loadFromExtension('security', [ + 'encoders' => [ + 'JMS\FooBundle\Entity\User7' => [ + 'algorithm' => 'argon2id', + 'memory_cost' => 256, + 'time_cost' => 1, + 'threads' => 2, + ], + ], +]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2id_encoder.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2id_encoder.xml new file mode 100644 index 0000000000..8bb8fa91c9 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2id_encoder.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2id_encoder.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2id_encoder.yml new file mode 100644 index 0000000000..f13de5ff63 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2id_encoder.yml @@ -0,0 +1,10 @@ +imports: + - { resource: container1.yml } + +security: + encoders: + JMS\FooBundle\Entity\User7: + algorithm: argon2id + 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 faec77550b..80d3348124 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php @@ -15,6 +15,7 @@ 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\Argon2idPasswordEncoder; use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder; use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; @@ -72,7 +73,7 @@ class UserPasswordEncoderCommandTest extends WebTestCase public function testEncodePasswordArgon2i() { - if (!Argon2iPasswordEncoder::isSupported()) { + if (!Argon2iPasswordEncoder::isSupported() || \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) { $this->markTestSkipped('Argon2i algorithm not available.'); } $this->setupArgon2i(); @@ -85,6 +86,27 @@ class UserPasswordEncoderCommandTest extends WebTestCase $output = $this->passwordEncoderCommandTester->getDisplay(); $this->assertContains('Password encoding succeeded', $output); + $encoder = new Argon2iPasswordEncoder(); + 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 (!Argon2idPasswordEncoder::isSupported()) { + $this->markTestSkipped('Argon2i 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->assertContains('Password encoding succeeded', $output); + $encoder = new Argon2iPasswordEncoder(); preg_match('# Encoded password\s+(\$argon2id?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches); $hash = $matches[1]; @@ -153,8 +175,8 @@ class UserPasswordEncoderCommandTest extends WebTestCase public function testEncodePasswordArgon2iOutput() { - if (!Argon2iPasswordEncoder::isSupported()) { - $this->markTestSkipped('Argon2i algorithm not available.'); + if (!Argon2iPasswordEncoder::isSupported() || \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) { + $this->markTestSkipped('Argon2id algorithm not available.'); } $this->setupArgon2i(); @@ -167,6 +189,22 @@ class UserPasswordEncoderCommandTest extends WebTestCase $this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); } + public function testEncodePasswordArgon2idOutput() + { + if (!Argon2idPasswordEncoder::isSupported()) { + $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->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); + } + public function testEncodePasswordNoConfigForGivenUserClass() { if (method_exists($this, 'expectException')) { @@ -259,4 +297,17 @@ 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); + } } 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 fc6b60a3a1..c8063c270d 100644 --- a/src/Symfony/Component/Security/CHANGELOG.md +++ b/src/Symfony/Component/Security/CHANGELOG.md @@ -19,6 +19,9 @@ CHANGELOG * Dispatch `AuthenticationFailureEvent` on `security.authentication.failure` * Dispatch `InteractiveLoginEvent` on `security.interactive_login` * Dispatch `SwitchUserEvent` on `security.switch_user` + * Added `Argon2idPasswordEncoder` + * Deprecated using `Argon2iPasswordEncoder` while only the `argon2id` algorithm + is supported, use `Argon2idPasswordEncoder` instead 4.2.0 ----- diff --git a/src/Symfony/Component/Security/Core/Encoder/Argon2Trait.php b/src/Symfony/Component/Security/Core/Encoder/Argon2Trait.php new file mode 100644 index 0000000000..de14becf47 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Encoder/Argon2Trait.php @@ -0,0 +1,52 @@ + + * + * 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; + +/** + * @internal + * + * @author Robin Chalas + */ +trait Argon2Trait +{ + private $memoryCost; + private $timeCost; + private $threads; + + public function __construct(int $memoryCost = null, int $timeCost = null, int $threads = null) + { + $this->memoryCost = $memoryCost; + $this->timeCost = $timeCost; + $this->threads = $threads; + } + + private function encodePasswordNative(string $raw, int $algorithm) + { + return password_hash($raw, $algorithm, [ + 'memory_cost' => $this->memoryCost ?? \PASSWORD_ARGON2_DEFAULT_MEMORY_COST, + 'time_cost' => $this->timeCost ?? \PASSWORD_ARGON2_DEFAULT_TIME_COST, + 'threads' => $this->threads ?? \PASSWORD_ARGON2_DEFAULT_THREADS, + ]); + } + + private function encodePasswordSodiumFunction(string $raw) + { + $hash = \sodium_crypto_pwhash_str( + $raw, + \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, + \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE + ); + \sodium_memzero($raw); + + return $hash; + } +} diff --git a/src/Symfony/Component/Security/Core/Encoder/Argon2iPasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/Argon2iPasswordEncoder.php index 333d3ddb11..1694e8fd65 100644 --- a/src/Symfony/Component/Security/Core/Encoder/Argon2iPasswordEncoder.php +++ b/src/Symfony/Component/Security/Core/Encoder/Argon2iPasswordEncoder.php @@ -21,25 +21,7 @@ use Symfony\Component\Security\Core\Exception\BadCredentialsException; */ class Argon2iPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface { - private $config = []; - - /** - * Argon2iPasswordEncoder constructor. - * - * @param int|null $memoryCost memory usage of the algorithm - * @param int|null $timeCost number of iterations - * @param int|null $threads number of parallel threads - */ - public function __construct(int $memoryCost = null, int $timeCost = null, int $threads = null) - { - if (\defined('PASSWORD_ARGON2I')) { - $this->config = [ - 'memory_cost' => $memoryCost ?? \PASSWORD_ARGON2_DEFAULT_MEMORY_COST, - 'time_cost' => $timeCost ?? \PASSWORD_ARGON2_DEFAULT_TIME_COST, - 'threads' => $threads ?? \PASSWORD_ARGON2_DEFAULT_THREADS, - ]; - } - } + use Argon2Trait; public static function isSupported() { @@ -64,10 +46,13 @@ class Argon2iPasswordEncoder extends BasePasswordEncoder implements SelfSaltingE } if (\PHP_VERSION_ID >= 70200 && \defined('PASSWORD_ARGON2I')) { - return $this->encodePasswordNative($raw); - } - if (\function_exists('sodium_crypto_pwhash_str')) { - return $this->encodePasswordSodiumFunction($raw); + return $this->encodePasswordNative($raw, \PASSWORD_ARGON2I); + } elseif (\function_exists('sodium_crypto_pwhash_str')) { + if (0 === strpos($hash = $this->encodePasswordSodiumFunction($raw), Argon2idPasswordEncoder::HASH_PREFIX)) { + @trigger_error(sprintf('Using "%s" while only the "argon2id" algorithm is supported is deprecated since Symfony 4.3, use "%s" instead.', __CLASS__, Argon2idPasswordEncoder::class), E_USER_DEPRECATED); + } + + return $hash; } if (\extension_loaded('libsodium')) { return $this->encodePasswordSodiumExtension($raw); @@ -81,10 +66,20 @@ class Argon2iPasswordEncoder extends BasePasswordEncoder implements SelfSaltingE */ public function isPasswordValid($encoded, $raw, $salt) { - // If $encoded was created via "sodium_crypto_pwhash_str()", the hashing algorithm may be "argon2id" instead of "argon2i". - // In this case, "password_verify()" cannot be used. - if (\PHP_VERSION_ID >= 70200 && \defined('PASSWORD_ARGON2I') && (false === strpos($encoded, '$argon2id$'))) { - return !$this->isPasswordTooLong($raw) && password_verify($raw, $encoded); + if ($this->isPasswordTooLong($raw)) { + return false; + } + + if (\PHP_VERSION_ID >= 70200 && \defined('PASSWORD_ARGON2I')) { + // If $encoded was created via "sodium_crypto_pwhash_str()", the hashing algorithm may be "argon2id" instead of "argon2i" + if ($isArgon2id = (0 === strpos($encoded, Argon2idPasswordEncoder::HASH_PREFIX))) { + @trigger_error(sprintf('Calling "%s()" with a password hashed using argon2id is deprecated since Symfony 4.3, use "%s" instead.', __METHOD__, Argon2idPasswordEncoder::class), E_USER_DEPRECATED); + } + + // Remove the right part of the OR in 5.0 + if (\defined('PASSWORD_ARGON2I') || $isArgon2id && \defined('PASSWORD_ARGON2ID')) { + return password_verify($raw, $encoded); + } } if (\function_exists('sodium_crypto_pwhash_str_verify')) { $valid = !$this->isPasswordTooLong($raw) && \sodium_crypto_pwhash_str_verify($encoded, $raw); @@ -102,23 +97,6 @@ class Argon2iPasswordEncoder extends BasePasswordEncoder implements SelfSaltingE throw new \LogicException('Argon2i algorithm is not supported. Please install the libsodium extension or upgrade to PHP 7.2+.'); } - private function encodePasswordNative($raw) - { - return password_hash($raw, \PASSWORD_ARGON2I, $this->config); - } - - private function encodePasswordSodiumFunction($raw) - { - $hash = \sodium_crypto_pwhash_str( - $raw, - \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, - \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE - ); - \sodium_memzero($raw); - - return $hash; - } - private function encodePasswordSodiumExtension($raw) { $hash = \Sodium\crypto_pwhash_str( diff --git a/src/Symfony/Component/Security/Core/Encoder/Argon2idPasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/Argon2idPasswordEncoder.php new file mode 100644 index 0000000000..14fa7b4cec --- /dev/null +++ b/src/Symfony/Component/Security/Core/Encoder/Argon2idPasswordEncoder.php @@ -0,0 +1,85 @@ + + * + * 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; +use Symfony\Component\Security\Core\Exception\LogicException; + +/** + * Hashes passwords using the Argon2id algorithm. + * + * @author Robin Chalas + * + * @final + */ +class Argon2idPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface +{ + use Argon2Trait; + + /** + * @internal + */ + public const HASH_PREFIX = '$argon2id'; + + public static function isSupported() + { + return \defined('PASSWORD_ARGON2ID') || \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13'); + } + + /** + * {@inheritdoc} + */ + public function encodePassword($raw, $salt) + { + if ($this->isPasswordTooLong($raw)) { + throw new BadCredentialsException('Invalid password.'); + } + if (\defined('PASSWORD_ARGON2ID')) { + return $this->encodePasswordNative($raw, \PASSWORD_ARGON2ID); + } + if (\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) { + $hash = \sodium_crypto_pwhash_str( + $raw, + \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, + \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE + ); + \sodium_memzero($raw); + + return $hash; + } + + throw new LogicException('Algorithm "argon2id" is not supported. Please install the libsodium extension or upgrade to PHP 7.3+.'); + } + + /** + * {@inheritdoc} + */ + public function isPasswordValid($encoded, $raw, $salt) + { + if (0 !== strpos($encoded, self::HASH_PREFIX)) { + return false; + } + + if (\defined('PASSWORD_ARGON2ID')) { + return !$this->isPasswordTooLong($raw) && password_verify($raw, $encoded); + } + + if (\function_exists('sodium_crypto_pwhash_str_verify')) { + $valid = !$this->isPasswordTooLong($raw) && \sodium_crypto_pwhash_str_verify($encoded, $raw); + \sodium_memzero($raw); + + return $valid; + } + + throw new LogicException('Algorithm "argon2id" is not supported. Please install the libsodium extension or upgrade to PHP 7.3+.'); + } +} diff --git a/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php b/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php index 8695ba3401..c5770b1e58 100644 --- a/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php +++ b/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php @@ -117,6 +117,15 @@ class EncoderFactory implements EncoderFactoryInterface $config['threads'], ], ]; + case 'argon2id': + return [ + 'class' => Argon2idPasswordEncoder::class, + 'arguments' => [ + $config['memory_cost'], + $config['time_cost'], + $config['threads'], + ], + ]; } return [ diff --git a/src/Symfony/Component/Security/Core/Tests/Encoder/Argon2iPasswordEncoderTest.php b/src/Symfony/Component/Security/Core/Tests/Encoder/Argon2iPasswordEncoderTest.php index 1b033cfacc..93917c5b59 100644 --- a/src/Symfony/Component/Security/Core/Tests/Encoder/Argon2iPasswordEncoderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Encoder/Argon2iPasswordEncoderTest.php @@ -23,7 +23,7 @@ class Argon2iPasswordEncoderTest extends TestCase protected function setUp() { - if (!Argon2iPasswordEncoder::isSupported()) { + if (!Argon2iPasswordEncoder::isSupported() || \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) { $this->markTestSkipped('Argon2i algorithm is not supported.'); } } diff --git a/src/Symfony/Component/Security/Core/Tests/Encoder/Argon2idPasswordEncoderTest.php b/src/Symfony/Component/Security/Core/Tests/Encoder/Argon2idPasswordEncoderTest.php new file mode 100644 index 0000000000..460777c124 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Tests/Encoder/Argon2idPasswordEncoderTest.php @@ -0,0 +1,65 @@ + + * + * 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\Argon2idPasswordEncoder; + +class Argon2idPasswordEncoderTest extends TestCase +{ + protected function setUp() + { + if (!Argon2idPasswordEncoder::isSupported()) { + $this->markTestSkipped('Argon2i algorithm is not supported.'); + } + } + + public function testValidationWithConfig() + { + $encoder = new Argon2idPasswordEncoder(8, 4, 1); + $result = $encoder->encodePassword('password', null); + $this->assertTrue($encoder->isPasswordValid($result, 'password', null)); + $this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword', null)); + } + + public function testValidation() + { + $encoder = new Argon2idPasswordEncoder(); + $result = $encoder->encodePassword('password', null); + $this->assertTrue($encoder->isPasswordValid($result, 'password', null)); + $this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword', null)); + } + + /** + * @expectedException \Symfony\Component\Security\Core\Exception\BadCredentialsException + */ + public function testEncodePasswordLength() + { + $encoder = new Argon2idPasswordEncoder(); + $encoder->encodePassword(str_repeat('a', 4097), 'salt'); + } + + public function testCheckPasswordLength() + { + $encoder = new Argon2idPasswordEncoder(); + $result = $encoder->encodePassword(str_repeat('a', 4096), null); + $this->assertFalse($encoder->isPasswordValid($result, str_repeat('a', 4097), null)); + $this->assertTrue($encoder->isPasswordValid($result, str_repeat('a', 4096), null)); + } + + public function testUserProvidedSaltIsNotUsed() + { + $encoder = new Argon2idPasswordEncoder(); + $result = $encoder->encodePassword('password', 'salt'); + $this->assertTrue($encoder->isPasswordValid($result, 'password', 'anotherSalt')); + } +}