diff --git a/UPGRADE-4.3.md b/UPGRADE-4.3.md
index 4cbf2216238ba..9967dda85425e 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 aa448846237f7..50eb4861530fd 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 84219a99f08d1..fd0edcb7dd8e2 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 4d74daa4d48be..b8b3358c020e9 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\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 @@ private function createEncoder($config, ContainerBuilder $container)
}
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 @@ private function createEncoder($config, ContainerBuilder $container)
];
}
+ // 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 f9102edfb07bf..5c95500dc3dd3 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\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 @@ public function testEncoders()
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 @@ public function testEncodersWithLibsodium()
]], $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 0000000000000..df63deb92eb24
--- /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 0000000000000..8bb8fa91c9d51
--- /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 0000000000000..f13de5ff63874
--- /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 faec77550b2b2..80d3348124125 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\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 @@ public function testEncodePasswordBcrypt()
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 @@ public function testEncodePasswordArgon2i()
$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 @@ public function testEncodePasswordBcryptOutput()
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 @@ public function testEncodePasswordArgon2iOutput()
$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 @@ private function setupArgon2i()
$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 0000000000000..481262acb7e6c
--- /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 fc6b60a3a1309..c8063c270d3fc 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 0000000000000..de14becf47378
--- /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 333d3ddb1165a..1694e8fd65c84 100644
--- a/src/Symfony/Component/Security/Core/Encoder/Argon2iPasswordEncoder.php
+++ b/src/Symfony/Component/Security/Core/Encoder/Argon2iPasswordEncoder.php
@@ -21,25 +21,7 @@
*/
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 @@ public function encodePassword($raw, $salt)
}
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 @@ public function encodePassword($raw, $salt)
*/
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 @@ public function isPasswordValid($encoded, $raw, $salt)
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 0000000000000..14fa7b4cec791
--- /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 8695ba3401d72..c5770b1e58226 100644
--- a/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php
+++ b/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php
@@ -117,6 +117,15 @@ private function getEncoderConfigFromAlgorithm($config)
$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 1b033cfacc685..93917c5b59773 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 0000000000000..460777c124f5f
--- /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'));
+ }
+}