Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[Security] Argon2i Password Encoder #21604

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 29, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions 1 src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
),
),
));
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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')) {
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
imports:
- { resource: config.yml }

security:
encoders:
Custom\Class\Argon2i\User:
algorithm: argon2i
1 change: 1 addition & 0 deletions 1 src/Symfony/Component/Security/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----
Expand Down
104 changes: 104 additions & 0 deletions 104 src/Symfony/Component/Security/Core/Encoder/Argon2iPasswordEncoder.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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'));
}
}
Morty Proxy This is a proxified and sanitized view of the page, visit original site.