diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
index b759b693cd273..dd27ace596055 100644
--- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
+++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
@@ -14,6 +14,7 @@ CHANGELOG
* deprecated HTTP digest authentication
* deprecated command `acl:set` along with `SetAclCommand` class
* deprecated command `init:acl` along with `InitAclCommand` class
+ * Added support for the new Argon2i password encoder
3.3.0
-----
diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
index b19f6b1b8d58c..45ab00ac47871 100644
--- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
+++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
@@ -29,6 +29,7 @@
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Security\Core\Authorization\ExpressionLanguage;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
+use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
/**
* SecurityExtension.
@@ -607,6 +608,18 @@ private function createEncoder($config, ContainerBuilder $container)
);
}
+ // Argon2i encoder
+ if ('argon2i' === $config['algorithm']) {
+ if (!Argon2iPasswordEncoder::isSupported()) {
+ throw new InvalidConfigurationException('Argon2i algorithm is not supported. Please install the libsodium extension or upgrade to PHP 7.2+.');
+ }
+
+ return array(
+ 'class' => 'Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder',
+ 'arguments' => array(),
+ );
+ }
+
// 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 5a2c731b2ac7d..cb581048448fd 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php
@@ -19,6 +19,7 @@
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
+use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
abstract class CompleteConfigurationTest extends TestCase
{
@@ -451,6 +452,18 @@ public function testEncoders()
)), $container->getDefinition('security.encoder_factory.generic')->getArguments());
}
+ public function testArgon2iEncoder()
+ {
+ if (!Argon2iPasswordEncoder::isSupported()) {
+ $this->markTestSkipped('Argon2i algorithm is not supported.');
+ }
+
+ $this->assertSame(array(array('JMS\FooBundle\Entity\User7' => array(
+ 'class' => 'Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder',
+ 'arguments' => array(),
+ ))), $this->getContainer('argon2i_encoder')->getDefinition('security.encoder_factory.generic')->getArguments());
+ }
+
/**
* @group legacy
* @expectedDeprecation The "security.acl" configuration key is deprecated since version 3.4 and will be removed in 4.0. Install symfony/acl-bundle and use the "acl" key instead.
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2i_encoder.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2i_encoder.php
new file mode 100644
index 0000000000000..23ff1799c8300
--- /dev/null
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2i_encoder.php
@@ -0,0 +1,19 @@
+loadFromExtension('security', array(
+ 'encoders' => array(
+ 'JMS\FooBundle\Entity\User7' => array(
+ 'algorithm' => 'argon2i',
+ ),
+ ),
+ 'providers' => array(
+ 'default' => array('id' => 'foo'),
+ ),
+ 'firewalls' => array(
+ 'main' => array(
+ 'form_login' => false,
+ 'http_basic' => null,
+ 'logout_on_user_change' => true,
+ ),
+ ),
+));
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2i_encoder.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2i_encoder.xml
new file mode 100644
index 0000000000000..dda4d8ec888c8
--- /dev/null
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2i_encoder.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2i_encoder.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2i_encoder.yml
new file mode 100644
index 0000000000000..a51e766005456
--- /dev/null
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2i_encoder.yml
@@ -0,0 +1,13 @@
+security:
+ encoders:
+ JMS\FooBundle\Entity\User6:
+ algorithm: argon2i
+
+ providers:
+ default: { id: foo }
+
+ firewalls:
+ main:
+ form_login: false
+ http_basic: ~
+ logout_on_user_change: true
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php
index afd3da09ce0d2..ab9275aeed24d 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\Argon2iPasswordEncoder;
use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder;
@@ -69,6 +70,27 @@ public function testEncodePasswordBcrypt()
$this->assertTrue($encoder->isPasswordValid($hash, 'password', null));
}
+ public function testEncodePasswordArgon2i()
+ {
+ if (!Argon2iPasswordEncoder::isSupported()) {
+ $this->markTestSkipped('Argon2i algorithm not available.');
+ }
+ $this->setupArgon2i();
+ $this->passwordEncoderCommandTester->execute(array(
+ 'command' => 'security:encode-password',
+ 'password' => 'password',
+ 'user-class' => 'Custom\Class\Argon2i\User',
+ ), array('interactive' => false));
+
+ $output = $this->passwordEncoderCommandTester->getDisplay();
+ $this->assertContains('Password encoding succeeded', $output);
+
+ $encoder = new Argon2iPasswordEncoder();
+ preg_match('# Encoded password\s+(\$argon2i\$[\w\d,=\$+\/]+={0,2})\s+#', $output, $matches);
+ $hash = $matches[1];
+ $this->assertTrue($encoder->isPasswordValid($hash, 'password', null));
+ }
+
public function testEncodePasswordPbkdf2()
{
$this->passwordEncoderCommandTester->execute(array(
@@ -129,6 +151,22 @@ public function testEncodePasswordBcryptOutput()
$this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay());
}
+ public function testEncodePasswordArgon2iOutput()
+ {
+ if (!Argon2iPasswordEncoder::isSupported()) {
+ $this->markTestSkipped('Argon2i algorithm not available.');
+ }
+
+ $this->setupArgon2i();
+ $this->passwordEncoderCommandTester->execute(array(
+ 'command' => 'security:encode-password',
+ 'password' => 'p@ssw0rd',
+ 'user-class' => 'Custom\Class\Argon2i\User',
+ ), array('interactive' => false));
+
+ $this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay());
+ }
+
public function testEncodePasswordNoConfigForGivenUserClass()
{
if (method_exists($this, 'expectException')) {
@@ -230,4 +268,17 @@ protected function tearDown()
{
$this->passwordEncoderCommandTester = null;
}
+
+ private function setupArgon2i()
+ {
+ putenv('COLUMNS='.(119 + strlen(PHP_EOL)));
+ $kernel = $this->createKernel(array('test_case' => 'PasswordEncode', 'root_config' => 'argon2i'));
+ $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/argon2i.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/argon2i.yml
new file mode 100644
index 0000000000000..2ca4f3461a6e9
--- /dev/null
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/argon2i.yml
@@ -0,0 +1,7 @@
+imports:
+ - { resource: config.yml }
+
+security:
+ encoders:
+ Custom\Class\Argon2i\User:
+ algorithm: argon2i
diff --git a/src/Symfony/Component/Security/CHANGELOG.md b/src/Symfony/Component/Security/CHANGELOG.md
index 5ac97ca8b20bb..176393c173ec6 100644
--- a/src/Symfony/Component/Security/CHANGELOG.md
+++ b/src/Symfony/Component/Security/CHANGELOG.md
@@ -13,6 +13,7 @@ CHANGELOG
the user will always be logged out when the user has changed between
requests.
* deprecated HTTP digest authentication
+ * Added a new password encoder for the Argon2i hashing algorithm
3.3.0
-----
diff --git a/src/Symfony/Component/Security/Core/Encoder/Argon2iPasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/Argon2iPasswordEncoder.php
new file mode 100644
index 0000000000000..c88bce0081941
--- /dev/null
+++ b/src/Symfony/Component/Security/Core/Encoder/Argon2iPasswordEncoder.php
@@ -0,0 +1,104 @@
+
+ *
+ * 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;
+
+/**
+ * Argon2iPasswordEncoder uses the Argon2i hashing algorithm.
+ *
+ * @author Zan Baldwin
+ */
+class Argon2iPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface
+{
+ public static function isSupported()
+ {
+ return (\PHP_VERSION_ID >= 70200 && \defined('PASSWORD_ARGON2I'))
+ || \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 (\PHP_VERSION_ID >= 70200 && \defined('PASSWORD_ARGON2I')) {
+ return $this->encodePasswordNative($raw);
+ }
+ if (\function_exists('sodium_crypto_pwhash_str')) {
+ return $this->encodePasswordSodiumFunction($raw);
+ }
+ if (\extension_loaded('libsodium')) {
+ return $this->encodePasswordSodiumExtension($raw);
+ }
+
+ throw new \LogicException('Argon2i algorithm is not supported. Please install the libsodium extension or upgrade to PHP 7.2+.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isPasswordValid($encoded, $raw, $salt)
+ {
+ if (\PHP_VERSION_ID >= 70200 && \defined('PASSWORD_ARGON2I')) {
+ 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;
+ }
+ if (\extension_loaded('libsodium')) {
+ $valid = !$this->isPasswordTooLong($raw) && \Sodium\crypto_pwhash_str_verify($encoded, $raw);
+ \Sodium\memzero($raw);
+
+ return $valid;
+ }
+
+ 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);
+ }
+
+ 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(
+ $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/EncoderFactory.php b/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php
index 7794b2f4dbcc1..8e1dbc852e746 100644
--- a/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php
+++ b/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php
@@ -109,6 +109,12 @@ private function getEncoderConfigFromAlgorithm($config)
'class' => BCryptPasswordEncoder::class,
'arguments' => array($config['cost']),
);
+
+ case 'argon2i':
+ return array(
+ 'class' => Argon2iPasswordEncoder::class,
+ 'arguments' => array(),
+ );
}
return array(
diff --git a/src/Symfony/Component/Security/Core/Tests/Encoder/Argon2iPasswordEncoderTest.php b/src/Symfony/Component/Security/Core/Tests/Encoder/Argon2iPasswordEncoderTest.php
new file mode 100644
index 0000000000000..70f2142ec39df
--- /dev/null
+++ b/src/Symfony/Component/Security/Core/Tests/Encoder/Argon2iPasswordEncoderTest.php
@@ -0,0 +1,62 @@
+
+ *
+ * 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\Argon2iPasswordEncoder;
+
+/**
+ * @author Zan Baldwin
+ */
+class Argon2iPasswordEncoderTest extends TestCase
+{
+ const PASSWORD = 'password';
+
+ protected function setUp()
+ {
+ if (!Argon2iPasswordEncoder::isSupported()) {
+ $this->markTestSkipped('Argon2i algorithm is not supported.');
+ }
+ }
+
+ public function testValidation()
+ {
+ $encoder = new Argon2iPasswordEncoder();
+ $result = $encoder->encodePassword(self::PASSWORD, null);
+ $this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, null));
+ $this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword', null));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Security\Core\Exception\BadCredentialsException
+ */
+ public function testEncodePasswordLength()
+ {
+ $encoder = new Argon2iPasswordEncoder();
+ $encoder->encodePassword(str_repeat('a', 4097), 'salt');
+ }
+
+ public function testCheckPasswordLength()
+ {
+ $encoder = new Argon2iPasswordEncoder();
+ $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 Argon2iPasswordEncoder();
+ $result = $encoder->encodePassword(self::PASSWORD, 'salt');
+ $this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, 'anotherSalt'));
+ }
+}