Argon2i Password Encoder
Add the Argon2i hashing algorithm provided by libsodium as a core encoder in the Security component, and enable it in the SecurityBundle. Credit to @chalasr for help with unit tests.
This commit is contained in:
parent
250d56b8d7
commit
be093dd79a
@ -14,6 +14,7 @@ CHANGELOG
|
|||||||
* deprecated HTTP digest authentication
|
* deprecated HTTP digest authentication
|
||||||
* deprecated command `acl:set` along with `SetAclCommand` class
|
* deprecated command `acl:set` along with `SetAclCommand` class
|
||||||
* deprecated command `init:acl` along with `InitAclCommand` class
|
* deprecated command `init:acl` along with `InitAclCommand` class
|
||||||
|
* Added support for the new Argon2i password encoder
|
||||||
|
|
||||||
3.3.0
|
3.3.0
|
||||||
-----
|
-----
|
||||||
|
@ -29,6 +29,7 @@ use Symfony\Component\DependencyInjection\Reference;
|
|||||||
use Symfony\Component\Config\FileLocator;
|
use Symfony\Component\Config\FileLocator;
|
||||||
use Symfony\Component\Security\Core\Authorization\ExpressionLanguage;
|
use Symfony\Component\Security\Core\Authorization\ExpressionLanguage;
|
||||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||||
|
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SecurityExtension.
|
* SecurityExtension.
|
||||||
@ -607,6 +608,18 @@ class SecurityExtension extends Extension
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// run-time configured encoder
|
||||||
return $config;
|
return $config;
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ use Symfony\Bundle\SecurityBundle\SecurityBundle;
|
|||||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
|
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
|
||||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||||
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
|
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
|
||||||
|
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
|
||||||
|
|
||||||
abstract class CompleteConfigurationTest extends TestCase
|
abstract class CompleteConfigurationTest extends TestCase
|
||||||
{
|
{
|
||||||
@ -451,6 +452,18 @@ abstract class CompleteConfigurationTest extends TestCase
|
|||||||
)), $container->getDefinition('security.encoder_factory.generic')->getArguments());
|
)), $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
|
* @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.
|
* @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.
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$container->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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<srv:container xmlns="http://symfony.com/schema/dic/security"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:srv="http://symfony.com/schema/dic/services"
|
||||||
|
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
|
||||||
|
|
||||||
|
<config>
|
||||||
|
<encoder class="JMS\FooBundle\Entity\User7" algorithm="argon2i" />
|
||||||
|
|
||||||
|
<provider name="default" id="foo" />
|
||||||
|
|
||||||
|
<firewall name="main" logout-on-user-change="true">
|
||||||
|
<form-login login-path="/login" />
|
||||||
|
</firewall>
|
||||||
|
</config>
|
||||||
|
|
||||||
|
</srv:container>
|
@ -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
|
@ -15,6 +15,7 @@ use Symfony\Bundle\FrameworkBundle\Console\Application;
|
|||||||
use Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand;
|
use Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand;
|
||||||
use Symfony\Component\Console\Application as ConsoleApplication;
|
use Symfony\Component\Console\Application as ConsoleApplication;
|
||||||
use Symfony\Component\Console\Tester\CommandTester;
|
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\BCryptPasswordEncoder;
|
||||||
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
||||||
use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder;
|
use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder;
|
||||||
@ -69,6 +70,27 @@ class UserPasswordEncoderCommandTest extends WebTestCase
|
|||||||
$this->assertTrue($encoder->isPasswordValid($hash, 'password', null));
|
$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()
|
public function testEncodePasswordPbkdf2()
|
||||||
{
|
{
|
||||||
$this->passwordEncoderCommandTester->execute(array(
|
$this->passwordEncoderCommandTester->execute(array(
|
||||||
@ -129,6 +151,22 @@ class UserPasswordEncoderCommandTest extends WebTestCase
|
|||||||
$this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay());
|
$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()
|
public function testEncodePasswordNoConfigForGivenUserClass()
|
||||||
{
|
{
|
||||||
if (method_exists($this, 'expectException')) {
|
if (method_exists($this, 'expectException')) {
|
||||||
@ -230,4 +268,17 @@ EOTXT
|
|||||||
{
|
{
|
||||||
$this->passwordEncoderCommandTester = null;
|
$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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
imports:
|
||||||
|
- { resource: config.yml }
|
||||||
|
|
||||||
|
security:
|
||||||
|
encoders:
|
||||||
|
Custom\Class\Argon2i\User:
|
||||||
|
algorithm: argon2i
|
@ -13,6 +13,7 @@ CHANGELOG
|
|||||||
the user will always be logged out when the user has changed between
|
the user will always be logged out when the user has changed between
|
||||||
requests.
|
requests.
|
||||||
* deprecated HTTP digest authentication
|
* deprecated HTTP digest authentication
|
||||||
|
* Added a new password encoder for the Argon2i hashing algorithm
|
||||||
|
|
||||||
3.3.0
|
3.3.0
|
||||||
-----
|
-----
|
||||||
|
@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* 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 <hello@zanbaldwin.com>
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -109,6 +109,12 @@ class EncoderFactory implements EncoderFactoryInterface
|
|||||||
'class' => BCryptPasswordEncoder::class,
|
'class' => BCryptPasswordEncoder::class,
|
||||||
'arguments' => array($config['cost']),
|
'arguments' => array($config['cost']),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case 'argon2i':
|
||||||
|
return array(
|
||||||
|
'class' => Argon2iPasswordEncoder::class,
|
||||||
|
'arguments' => array(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* 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 <hello@zanbaldwin.com>
|
||||||
|
*/
|
||||||
|
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'));
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user