From 529211d7eded6592ba769b105ff663064b0a3201 Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Mon, 8 Apr 2019 19:01:45 +0200 Subject: [PATCH] [Security] Replace Argon2*PasswordEncoder by SodiumPasswordEncoder This reverts commit dc95a6fec6d44e1dfcd7ab5895d6e68bf71676bb. --- UPGRADE-4.3.md | 6 +- UPGRADE-5.0.md | 6 +- .../Bundle/SecurityBundle/CHANGELOG.md | 4 +- .../DependencyInjection/SecurityExtension.php | 21 ++-- .../CompleteConfigurationTest.php | 115 +++++++++--------- ...rgon2id_encoder.php => sodium_encoder.php} | 5 +- ...rgon2id_encoder.xml => sodium_encoder.xml} | 2 +- .../Fixtures/yml/argon2id_encoder.yml | 10 -- .../Fixtures/yml/sodium_encoder.yml | 7 ++ .../UserPasswordEncoderCommandTest.php | 45 ++++--- .../app/PasswordEncode/argon2id.yml | 7 -- .../Functional/app/PasswordEncode/sodium.yml | 7 ++ src/Symfony/Component/Security/CHANGELOG.md | 4 +- .../Security/Core/Encoder/Argon2Trait.php | 52 -------- .../Core/Encoder/Argon2iPasswordEncoder.php | 68 +++++++---- .../Core/Encoder/Argon2idPasswordEncoder.php | 88 -------------- .../Security/Core/Encoder/EncoderFactory.php | 16 ++- .../Core/Encoder/SodiumPasswordEncoder.php | 84 +++++++++++++ .../Encoder/Argon2iPasswordEncoderTest.php | 43 ++----- ...Test.php => SodiumPasswordEncoderTest.php} | 24 ++-- 20 files changed, 268 insertions(+), 346 deletions(-) rename src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/{argon2id_encoder.php => sodium_encoder.php} (57%) rename src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/{argon2id_encoder.xml => sodium_encoder.xml} (89%) delete mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2id_encoder.yml create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/sodium_encoder.yml delete mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/argon2id.yml create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/sodium.yml delete mode 100644 src/Symfony/Component/Security/Core/Encoder/Argon2Trait.php delete mode 100644 src/Symfony/Component/Security/Core/Encoder/Argon2idPasswordEncoder.php create mode 100644 src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php rename src/Symfony/Component/Security/Core/Tests/Encoder/{Argon2idPasswordEncoderTest.php => SodiumPasswordEncoderTest.php} (64%) diff --git a/UPGRADE-4.3.md b/UPGRADE-4.3.md index 7442600260..1191a75218 100644 --- a/UPGRADE-4.3.md +++ b/UPGRADE-4.3.md @@ -151,14 +151,12 @@ Security } ``` - * Using `Argon2iPasswordEncoder` while only the `argon2id` algorithm is supported - is deprecated, use `Argon2idPasswordEncoder` instead + * The `Argon2iPasswordEncoder` class has been deprecated, use `SodiumPasswordEncoder` instead. SecurityBundle -------------- - * Configuring encoders using `argon2i` as algorithm while only `argon2id` is - supported is deprecated, use `argon2id` instead + * Configuring encoders using `argon2i` as algorithm has been deprecated, use `sodium` instead. TwigBridge ---------- diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index b163e76678..fbcc53cf4a 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -326,8 +326,7 @@ Security } ``` - * Using `Argon2iPasswordEncoder` while only the `argon2id` algorithm is supported - now throws a `\LogicException`, use `Argon2idPasswordEncoder` instead + * The `Argon2iPasswordEncoder` class has been removed, use `SodiumPasswordEncoder` instead. SecurityBundle -------------- @@ -348,8 +347,7 @@ 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 + * Configuring encoders using `argon2i` as algorithm is not supported anymore, use `sodium` instead. Serializer ---------- diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index fd0edcb7dd..28bbd1286e 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -8,9 +8,7 @@ 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 - + * Deprecated configuring encoders using `argon2i` as algorithm, use `sodium` instead 4.2.0 ----- diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index dd7fe75cc8..4519b66a94 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -29,8 +29,8 @@ 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\Encoder\SodiumPasswordEncoder; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Controller\UserValueResolver; use Symfony\Component\Templating\PhpEngine; @@ -565,14 +565,14 @@ class SecurityExtension extends Extension implements PrependExtensionInterface // 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); + 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('Argon2i algorithm is not supported. Install the libsodium extension or use BCrypt instead.'); - } elseif (!\defined('PASSWORD_ARGON2I') && Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) { - @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 [ @@ -585,19 +585,14 @@ 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.'); + if ('sodium' === $config['algorithm']) { + if (!SodiumPasswordEncoder::isSupported()) { + throw new InvalidConfigurationException('Libsodium is not available. Install the sodium extension or use BCrypt instead.'); } return [ - 'class' => Argon2idPasswordEncoder::class, - 'arguments' => [ - $config['memory_cost'], - $config['time_cost'], - $config['threads'], - ], + 'class' => SodiumPasswordEncoder::class, + 'arguments' => [], ]; } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php index 5c95500dc3..0727c79a52 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php @@ -18,8 +18,8 @@ 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; +use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; abstract class CompleteConfigurationTest extends TestCase { @@ -314,7 +314,65 @@ abstract class CompleteConfigurationTest extends TestCase public function testEncodersWithLibsodium() { - if (!Argon2iPasswordEncoder::isSupported() || \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) { + if (!SodiumPasswordEncoder::isSupported()) { + $this->markTestSkipped('Libsodium is not available.'); + } + + $container = $this->getContainer('sodium_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\SodiumPasswordEncoder', + 'arguments' => [], + ], + ]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); + } + + /** + * @group legacy + * + * @expectedDeprecation Configuring an encoder with "argon2i" as algorithm is deprecated since Symfony 4.3, use "sodium" instead. + */ + public function testEncodersWithArgon2i() + { + if (!Argon2iPasswordEncoder::isSupported()) { $this->markTestSkipped('Argon2i algorithm is not supported.'); } @@ -365,59 +423,6 @@ 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/sodium_encoder.php similarity index 57% rename from src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2id_encoder.php rename to src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/sodium_encoder.php index df63deb92e..e32969df23 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2id_encoder.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/sodium_encoder.php @@ -5,10 +5,7 @@ $this->load('container1.php', $container); $container->loadFromExtension('security', [ 'encoders' => [ 'JMS\FooBundle\Entity\User7' => [ - 'algorithm' => 'argon2id', - 'memory_cost' => 256, - 'time_cost' => 1, - 'threads' => 2, + 'algorithm' => 'sodium', ], ], ]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2id_encoder.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/sodium_encoder.xml similarity index 89% rename from src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2id_encoder.xml rename to src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/sodium_encoder.xml index 8bb8fa91c9..3fb4721300 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2id_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/argon2id_encoder.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2id_encoder.yml deleted file mode 100644 index f13de5ff63..0000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2id_encoder.yml +++ /dev/null @@ -1,10 +0,0 @@ -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/DependencyInjection/Fixtures/yml/sodium_encoder.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/sodium_encoder.yml new file mode 100644 index 0000000000..175d945b7d --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/sodium_encoder.yml @@ -0,0 +1,7 @@ +imports: + - { resource: container1.yml } + +security: + encoders: + JMS\FooBundle\Entity\User7: + algorithm: sodium diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php index 54d355cf0f..6685d34948 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php @@ -15,11 +15,11 @@ 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; use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder; +use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; /** * Tests UserPasswordEncoderCommand. @@ -71,9 +71,12 @@ class UserPasswordEncoderCommandTest extends WebTestCase $this->assertTrue($encoder->isPasswordValid($hash, 'password', null)); } + /** + * @group legacy + */ public function testEncodePasswordArgon2i() { - if (!Argon2iPasswordEncoder::isSupported() || !\defined('PASSWORD_ARGON2I') && Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) { + if (!Argon2iPasswordEncoder::isSupported()) { $this->markTestSkipped('Argon2i algorithm not available.'); } $this->setupArgon2i(); @@ -87,30 +90,29 @@ class UserPasswordEncoderCommandTest extends WebTestCase $this->assertContains('Password encoding succeeded', $output); $encoder = new Argon2iPasswordEncoder(); - preg_match('# Encoded password\s+(\$argon2i?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches); + preg_match('# Encoded password\s+(\$argon2id?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches); $hash = $matches[1]; $this->assertTrue($encoder->isPasswordValid($hash, 'password', null)); } - public function testEncodePasswordArgon2id() + public function testEncodePasswordSodium() { - if (!Argon2idPasswordEncoder::isSupported()) { - $this->markTestSkipped('Argon2id algorithm not available.'); + if (!SodiumPasswordEncoder::isSupported()) { + $this->markTestSkipped('Libsodium is not available.'); } - $this->setupArgon2id(); + $this->setupSodium(); $this->passwordEncoderCommandTester->execute([ 'command' => 'security:encode-password', 'password' => 'password', - 'user-class' => 'Custom\Class\Argon2id\User', + 'user-class' => 'Custom\Class\Sodium\User', ], ['interactive' => false]); $output = $this->passwordEncoderCommandTester->getDisplay(); $this->assertContains('Password encoding succeeded', $output); - $encoder = new Argon2idPasswordEncoder(); - preg_match('# Encoded password\s+(\$argon2id?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches); + preg_match('# Encoded password\s+(\$?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches); $hash = $matches[1]; - $this->assertTrue($encoder->isPasswordValid($hash, 'password', null)); + $this->assertTrue((new SodiumPasswordEncoder())->isPasswordValid($hash, 'password', null)); } public function testEncodePasswordPbkdf2() @@ -173,10 +175,13 @@ class UserPasswordEncoderCommandTest extends WebTestCase $this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); } + /** + * @group legacy + */ public function testEncodePasswordArgon2iOutput() { - if (!Argon2iPasswordEncoder::isSupported() || !\defined('PASSWORD_ARGON2I') && Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) { - $this->markTestSkipped('Argon2id algorithm not available.'); + if (!Argon2iPasswordEncoder::isSupported()) { + $this->markTestSkipped('Argon2i algorithm not available.'); } $this->setupArgon2i(); @@ -189,17 +194,17 @@ class UserPasswordEncoderCommandTest extends WebTestCase $this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); } - public function testEncodePasswordArgon2idOutput() + public function testEncodePasswordSodiumOutput() { - if (!Argon2idPasswordEncoder::isSupported()) { - $this->markTestSkipped('Argon2id algorithm not available.'); + if (!SodiumPasswordEncoder::isSupported()) { + $this->markTestSkipped('Libsodium is not available.'); } - $this->setupArgon2id(); + $this->setupSodium(); $this->passwordEncoderCommandTester->execute([ 'command' => 'security:encode-password', 'password' => 'p@ssw0rd', - 'user-class' => 'Custom\Class\Argon2id\User', + 'user-class' => 'Custom\Class\Sodium\User', ], ['interactive' => false]); $this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); @@ -298,10 +303,10 @@ EOTXT $this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand); } - private function setupArgon2id() + private function setupSodium() { putenv('COLUMNS='.(119 + \strlen(PHP_EOL))); - $kernel = $this->createKernel(['test_case' => 'PasswordEncode', 'root_config' => 'argon2id.yml']); + $kernel = $this->createKernel(['test_case' => 'PasswordEncode', 'root_config' => 'sodium.yml']); $kernel->boot(); $application = new Application($kernel); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/argon2id.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/argon2id.yml deleted file mode 100644 index 481262acb7..0000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/argon2id.yml +++ /dev/null @@ -1,7 +0,0 @@ -imports: - - { resource: config.yml } - -security: - encoders: - Custom\Class\Argon2id\User: - algorithm: argon2id diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/sodium.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/sodium.yml new file mode 100644 index 0000000000..1ccc2a10d5 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/sodium.yml @@ -0,0 +1,7 @@ +imports: + - { resource: config.yml } + +security: + encoders: + Custom\Class\Sodium\User: + algorithm: sodium diff --git a/src/Symfony/Component/Security/CHANGELOG.md b/src/Symfony/Component/Security/CHANGELOG.md index 776774e5b6..42aca94dd0 100644 --- a/src/Symfony/Component/Security/CHANGELOG.md +++ b/src/Symfony/Component/Security/CHANGELOG.md @@ -19,9 +19,7 @@ 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 + * deprecated `Argon2iPasswordEncoder`, use `SodiumPasswordEncoder` instead 4.2.0 ----- diff --git a/src/Symfony/Component/Security/Core/Encoder/Argon2Trait.php b/src/Symfony/Component/Security/Core/Encoder/Argon2Trait.php deleted file mode 100644 index de14becf47..0000000000 --- a/src/Symfony/Component/Security/Core/Encoder/Argon2Trait.php +++ /dev/null @@ -1,52 +0,0 @@ - - * - * 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 b1add53763..d7b53d34b0 100644 --- a/src/Symfony/Component/Security/Core/Encoder/Argon2iPasswordEncoder.php +++ b/src/Symfony/Component/Security/Core/Encoder/Argon2iPasswordEncoder.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Security\Core\Encoder; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" instead.', Argon2iPasswordEncoder::class, SodiumPasswordEncoder::class), E_USER_DEPRECATED); + use Symfony\Component\Security\Core\Exception\BadCredentialsException; /** @@ -18,10 +20,30 @@ use Symfony\Component\Security\Core\Exception\BadCredentialsException; * * @author Zan Baldwin * @author Dominik Müller + * + * @deprecated since Symfony 4.3, use SodiumPasswordEncoder instead */ class Argon2iPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface { - use Argon2Trait; + 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, + ]; + } + } public static function isSupported() { @@ -46,12 +68,9 @@ class Argon2iPasswordEncoder extends BasePasswordEncoder implements SelfSaltingE } if (\PHP_VERSION_ID >= 70200 && \defined('PASSWORD_ARGON2I')) { - return $this->encodePasswordNative($raw, \PASSWORD_ARGON2I); - } elseif (\function_exists('sodium_crypto_pwhash_str')) { - if (Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) { - @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 $this->encodePasswordNative($raw); + } + if (\function_exists('sodium_crypto_pwhash_str')) { return $this->encodePasswordSodiumFunction($raw); } if (\extension_loaded('libsodium')) { @@ -66,20 +85,10 @@ class Argon2iPasswordEncoder extends BasePasswordEncoder implements SelfSaltingE */ public function isPasswordValid($encoded, $raw, $salt) { - if ($this->isPasswordTooLong($raw)) { - return false; - } - - // 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); - } - - if (\PHP_VERSION_ID >= 70200 && \defined('PASSWORD_ARGON2I')) { - // Remove the right part of the OR in 5.0 - if (\defined('PASSWORD_ARGON2I') || $isArgon2id && \defined('PASSWORD_ARGON2ID')) { - return password_verify($raw, $encoded); - } + // 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 (\function_exists('sodium_crypto_pwhash_str_verify')) { $valid = !$this->isPasswordTooLong($raw) && \sodium_crypto_pwhash_str_verify($encoded, $raw); @@ -97,6 +106,23 @@ 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 deleted file mode 100644 index a201bca36b..0000000000 --- a/src/Symfony/Component/Security/Core/Encoder/Argon2idPasswordEncoder.php +++ /dev/null @@ -1,88 +0,0 @@ - - * - * 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 (!self::isDefaultSodiumAlgorithm()) { - throw new LogicException('Algorithm "argon2id" is not supported. Please install the libsodium extension or upgrade to PHP 7.3+.'); - } - - return $this->encodePasswordSodiumFunction($raw); - } - - /** - * {@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+.'); - } - - /** - * @internal - */ - public static function isDefaultSodiumAlgorithm() - { - return \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13') - && \defined('SODIUM_CRYPTO_PWHASH_ALG_DEFAULT') - && \SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13 === \SODIUM_CRYPTO_PWHASH_ALG_DEFAULT; - } -} diff --git a/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php b/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php index c5770b1e58..01dd33f053 100644 --- a/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php +++ b/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php @@ -108,6 +108,13 @@ class EncoderFactory implements EncoderFactoryInterface 'arguments' => [$config['cost']], ]; + case 'sodium': + return [ + 'class' => SodiumPasswordEncoder::class, + 'arguments' => [], + ]; + + /* @deprecated since Symfony 4.3 */ case 'argon2i': return [ 'class' => Argon2iPasswordEncoder::class, @@ -117,15 +124,6 @@ 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/Encoder/SodiumPasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php new file mode 100644 index 0000000000..febca05dd0 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php @@ -0,0 +1,84 @@ + + * + * 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 libsodium. + * + * @author Robin Chalas + * @author Zan Baldwin + * @author Dominik Müller + * + * @final + */ +class SodiumPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface +{ + public static function isSupported(): bool + { + if (\class_exists('ParagonIE_Sodium_Compat') && \method_exists('ParagonIE_Sodium_Compat', 'crypto_pwhash_is_available')) { + return \ParagonIE_Sodium_Compat::crypto_pwhash_is_available(); + } + + return \function_exists('sodium_crypto_pwhash_str') || \extension_loaded('libsodium'); + } + + /** + * {@inheritdoc} + */ + public function encodePassword($raw, $salt) + { + if ($this->isPasswordTooLong($raw)) { + 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 + ); + } + + if (\extension_loaded('libsodium')) { + return \Sodium\crypto_pwhash_str( + $raw, + \Sodium\CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, + \Sodium\CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE + ); + } + + throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.'); + } + + /** + * {@inheritdoc} + */ + public function isPasswordValid($encoded, $raw, $salt) + { + if ($this->isPasswordTooLong($raw)) { + return false; + } + + if (\function_exists('sodium_crypto_pwhash_str_verify')) { + return \sodium_crypto_pwhash_str_verify($encoded, $raw); + } + + if (\extension_loaded('libsodium')) { + return \Sodium\crypto_pwhash_str_verify($encoded, $raw); + } + + throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.'); + } +} diff --git a/src/Symfony/Component/Security/Core/Tests/Encoder/Argon2iPasswordEncoderTest.php b/src/Symfony/Component/Security/Core/Tests/Encoder/Argon2iPasswordEncoderTest.php index 88f10b446d..a9991749f0 100644 --- a/src/Symfony/Component/Security/Core/Tests/Encoder/Argon2iPasswordEncoderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Encoder/Argon2iPasswordEncoderTest.php @@ -12,22 +12,26 @@ namespace Symfony\Component\Security\Core\Tests\Encoder; use PHPUnit\Framework\TestCase; -use Symfony\Component\Security\Core\Encoder\Argon2idPasswordEncoder; use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder; /** * @author Zan Baldwin + * + * @group legacy */ class Argon2iPasswordEncoderTest extends TestCase { const PASSWORD = 'password'; - public function testValidationWithConfig() + protected function setUp() { - if (!Argon2iPasswordEncoder::isSupported() || Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) { + if (!Argon2iPasswordEncoder::isSupported()) { $this->markTestSkipped('Argon2i algorithm is not supported.'); } + } + public function testValidationWithConfig() + { $encoder = new Argon2iPasswordEncoder(8, 4, 1); $result = $encoder->encodePassword(self::PASSWORD, null); $this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, null)); @@ -36,10 +40,6 @@ class Argon2iPasswordEncoderTest extends TestCase public function testValidation() { - if (!Argon2iPasswordEncoder::isSupported() || Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) { - $this->markTestSkipped('Argon2i algorithm is not supported.'); - } - $encoder = new Argon2iPasswordEncoder(); $result = $encoder->encodePassword(self::PASSWORD, null); $this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, null)); @@ -51,20 +51,12 @@ class Argon2iPasswordEncoderTest extends TestCase */ public function testEncodePasswordLength() { - if (!Argon2iPasswordEncoder::isSupported() || Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) { - $this->markTestSkipped('Argon2i algorithm is not supported.'); - } - $encoder = new Argon2iPasswordEncoder(); $encoder->encodePassword(str_repeat('a', 4097), 'salt'); } public function testCheckPasswordLength() { - if (!Argon2iPasswordEncoder::isSupported() || Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) { - $this->markTestSkipped('Argon2i algorithm is not supported.'); - } - $encoder = new Argon2iPasswordEncoder(); $result = $encoder->encodePassword(str_repeat('a', 4096), null); $this->assertFalse($encoder->isPasswordValid($result, str_repeat('a', 4097), null)); @@ -73,29 +65,8 @@ class Argon2iPasswordEncoderTest extends TestCase public function testUserProvidedSaltIsNotUsed() { - if (!Argon2iPasswordEncoder::isSupported() || Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) { - $this->markTestSkipped('Argon2i algorithm is not supported.'); - } - $encoder = new Argon2iPasswordEncoder(); $result = $encoder->encodePassword(self::PASSWORD, 'salt'); $this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, 'anotherSalt')); } - - /** - * @group legacy - * @exectedDeprecation Using "Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder" while only the "argon2id" algorithm is supported is deprecated since Symfony 4.3, use "Symfony\Component\Security\Core\Encoder\Argon2idPasswordEncoder" instead. - * @exectedDeprecation Calling "Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder::isPasswordValid()" with a password hashed using argon2id is deprecated since Symfony 4.3, use "Symfony\Component\Security\Core\Encoder\Argon2idPasswordEncoder" instead. - */ - public function testEncodeWithArgon2idSupportOnly() - { - if (!Argon2iPasswordEncoder::isSupported() || !Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) { - $this->markTestSkipped('Argon2id algorithm not available.'); - } - - $encoder = new Argon2iPasswordEncoder(); - $result = $encoder->encodePassword(self::PASSWORD, null); - $this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, null)); - $this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword', null)); - } } diff --git a/src/Symfony/Component/Security/Core/Tests/Encoder/Argon2idPasswordEncoderTest.php b/src/Symfony/Component/Security/Core/Tests/Encoder/SodiumPasswordEncoderTest.php similarity index 64% rename from src/Symfony/Component/Security/Core/Tests/Encoder/Argon2idPasswordEncoderTest.php rename to src/Symfony/Component/Security/Core/Tests/Encoder/SodiumPasswordEncoderTest.php index 460777c124..fe9e5db0eb 100644 --- a/src/Symfony/Component/Security/Core/Tests/Encoder/Argon2idPasswordEncoderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Encoder/SodiumPasswordEncoderTest.php @@ -12,28 +12,20 @@ namespace Symfony\Component\Security\Core\Tests\Encoder; use PHPUnit\Framework\TestCase; -use Symfony\Component\Security\Core\Encoder\Argon2idPasswordEncoder; +use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; -class Argon2idPasswordEncoderTest extends TestCase +class SodiumPasswordEncoderTest extends TestCase { protected function setUp() { - if (!Argon2idPasswordEncoder::isSupported()) { - $this->markTestSkipped('Argon2i algorithm is not supported.'); + if (!SodiumPasswordEncoder::isSupported()) { + $this->markTestSkipped('Libsodium is not available.'); } } - 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(); + $encoder = new SodiumPasswordEncoder(); $result = $encoder->encodePassword('password', null); $this->assertTrue($encoder->isPasswordValid($result, 'password', null)); $this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword', null)); @@ -44,13 +36,13 @@ class Argon2idPasswordEncoderTest extends TestCase */ public function testEncodePasswordLength() { - $encoder = new Argon2idPasswordEncoder(); + $encoder = new SodiumPasswordEncoder(); $encoder->encodePassword(str_repeat('a', 4097), 'salt'); } public function testCheckPasswordLength() { - $encoder = new Argon2idPasswordEncoder(); + $encoder = new SodiumPasswordEncoder(); $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)); @@ -58,7 +50,7 @@ class Argon2idPasswordEncoderTest extends TestCase public function testUserProvidedSaltIsNotUsed() { - $encoder = new Argon2idPasswordEncoder(); + $encoder = new SodiumPasswordEncoder(); $result = $encoder->encodePassword('password', 'salt'); $this->assertTrue($encoder->isPasswordValid($result, 'password', 'anotherSalt')); }