Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit be093dd

Browse files
committed
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.
1 parent 250d56b commit be093dd

File tree

12 files changed

+308
-0
lines changed

12 files changed

+308
-0
lines changed

src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ CHANGELOG
1414
* deprecated HTTP digest authentication
1515
* deprecated command `acl:set` along with `SetAclCommand` class
1616
* deprecated command `init:acl` along with `InitAclCommand` class
17+
* Added support for the new Argon2i password encoder
1718

1819
3.3.0
1920
-----

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use Symfony\Component\Config\FileLocator;
3030
use Symfony\Component\Security\Core\Authorization\ExpressionLanguage;
3131
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
32+
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
3233

3334
/**
3435
* SecurityExtension.
@@ -607,6 +608,18 @@ private function createEncoder($config, ContainerBuilder $container)
607608
);
608609
}
609610

611+
// Argon2i encoder
612+
if ('argon2i' === $config['algorithm']) {
613+
if (!Argon2iPasswordEncoder::isSupported()) {
614+
throw new InvalidConfigurationException('Argon2i algorithm is not supported. Please install the libsodium extension or upgrade to PHP 7.2+.');
615+
}
616+
617+
return array(
618+
'class' => 'Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder',
619+
'arguments' => array(),
620+
);
621+
}
622+
610623
// run-time configured encoder
611624
return $config;
612625
}

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
2020
use Symfony\Component\DependencyInjection\ContainerBuilder;
2121
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
22+
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
2223

2324
abstract class CompleteConfigurationTest extends TestCase
2425
{
@@ -451,6 +452,18 @@ public function testEncoders()
451452
)), $container->getDefinition('security.encoder_factory.generic')->getArguments());
452453
}
453454

455+
public function testArgon2iEncoder()
456+
{
457+
if (!Argon2iPasswordEncoder::isSupported()) {
458+
$this->markTestSkipped('Argon2i algorithm is not supported.');
459+
}
460+
461+
$this->assertSame(array(array('JMS\FooBundle\Entity\User7' => array(
462+
'class' => 'Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder',
463+
'arguments' => array(),
464+
))), $this->getContainer('argon2i_encoder')->getDefinition('security.encoder_factory.generic')->getArguments());
465+
}
466+
454467
/**
455468
* @group legacy
456469
* @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.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
$container->loadFromExtension('security', array(
4+
'encoders' => array(
5+
'JMS\FooBundle\Entity\User7' => array(
6+
'algorithm' => 'argon2i',
7+
),
8+
),
9+
'providers' => array(
10+
'default' => array('id' => 'foo'),
11+
),
12+
'firewalls' => array(
13+
'main' => array(
14+
'form_login' => false,
15+
'http_basic' => null,
16+
'logout_on_user_change' => true,
17+
),
18+
),
19+
));
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
3+
<srv:container xmlns="http://symfony.com/schema/dic/security"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xmlns:srv="http://symfony.com/schema/dic/services"
6+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
7+
8+
<config>
9+
<encoder class="JMS\FooBundle\Entity\User7" algorithm="argon2i" />
10+
11+
<provider name="default" id="foo" />
12+
13+
<firewall name="main" logout-on-user-change="true">
14+
<form-login login-path="/login" />
15+
</firewall>
16+
</config>
17+
18+
</srv:container>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
security:
2+
encoders:
3+
JMS\FooBundle\Entity\User6:
4+
algorithm: argon2i
5+
6+
providers:
7+
default: { id: foo }
8+
9+
firewalls:
10+
main:
11+
form_login: false
12+
http_basic: ~
13+
logout_on_user_change: true

src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand;
1616
use Symfony\Component\Console\Application as ConsoleApplication;
1717
use Symfony\Component\Console\Tester\CommandTester;
18+
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
1819
use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder;
1920
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
2021
use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder;
@@ -69,6 +70,27 @@ public function testEncodePasswordBcrypt()
6970
$this->assertTrue($encoder->isPasswordValid($hash, 'password', null));
7071
}
7172

73+
public function testEncodePasswordArgon2i()
74+
{
75+
if (!Argon2iPasswordEncoder::isSupported()) {
76+
$this->markTestSkipped('Argon2i algorithm not available.');
77+
}
78+
$this->setupArgon2i();
79+
$this->passwordEncoderCommandTester->execute(array(
80+
'command' => 'security:encode-password',
81+
'password' => 'password',
82+
'user-class' => 'Custom\Class\Argon2i\User',
83+
), array('interactive' => false));
84+
85+
$output = $this->passwordEncoderCommandTester->getDisplay();
86+
$this->assertContains('Password encoding succeeded', $output);
87+
88+
$encoder = new Argon2iPasswordEncoder();
89+
preg_match('# Encoded password\s+(\$argon2i\$[\w\d,=\$+\/]+={0,2})\s+#', $output, $matches);
90+
$hash = $matches[1];
91+
$this->assertTrue($encoder->isPasswordValid($hash, 'password', null));
92+
}
93+
7294
public function testEncodePasswordPbkdf2()
7395
{
7496
$this->passwordEncoderCommandTester->execute(array(
@@ -129,6 +151,22 @@ public function testEncodePasswordBcryptOutput()
129151
$this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay());
130152
}
131153

154+
public function testEncodePasswordArgon2iOutput()
155+
{
156+
if (!Argon2iPasswordEncoder::isSupported()) {
157+
$this->markTestSkipped('Argon2i algorithm not available.');
158+
}
159+
160+
$this->setupArgon2i();
161+
$this->passwordEncoderCommandTester->execute(array(
162+
'command' => 'security:encode-password',
163+
'password' => 'p@ssw0rd',
164+
'user-class' => 'Custom\Class\Argon2i\User',
165+
), array('interactive' => false));
166+
167+
$this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay());
168+
}
169+
132170
public function testEncodePasswordNoConfigForGivenUserClass()
133171
{
134172
if (method_exists($this, 'expectException')) {
@@ -230,4 +268,17 @@ protected function tearDown()
230268
{
231269
$this->passwordEncoderCommandTester = null;
232270
}
271+
272+
private function setupArgon2i()
273+
{
274+
putenv('COLUMNS='.(119 + strlen(PHP_EOL)));
275+
$kernel = $this->createKernel(array('test_case' => 'PasswordEncode', 'root_config' => 'argon2i'));
276+
$kernel->boot();
277+
278+
$application = new Application($kernel);
279+
280+
$passwordEncoderCommand = $application->get('security:encode-password');
281+
282+
$this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand);
283+
}
233284
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
imports:
2+
- { resource: config.yml }
3+
4+
security:
5+
encoders:
6+
Custom\Class\Argon2i\User:
7+
algorithm: argon2i

src/Symfony/Component/Security/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ CHANGELOG
1313
the user will always be logged out when the user has changed between
1414
requests.
1515
* deprecated HTTP digest authentication
16+
* Added a new password encoder for the Argon2i hashing algorithm
1617

1718
3.3.0
1819
-----
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Encoder;
13+
14+
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
15+
16+
/**
17+
* Argon2iPasswordEncoder uses the Argon2i hashing algorithm.
18+
*
19+
* @author Zan Baldwin <[email protected]>
20+
*/
21+
class Argon2iPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface
22+
{
23+
public static function isSupported()
24+
{
25+
return (\PHP_VERSION_ID >= 70200 && \defined('PASSWORD_ARGON2I'))
26+
|| \function_exists('sodium_crypto_pwhash_str')
27+
|| \extension_loaded('libsodium');
28+
}
29+
30+
/**
31+
* {@inheritdoc}
32+
*/
33+
public function encodePassword($raw, $salt)
34+
{
35+
if ($this->isPasswordTooLong($raw)) {
36+
throw new BadCredentialsException('Invalid password.');
37+
}
38+
39+
if (\PHP_VERSION_ID >= 70200 && \defined('PASSWORD_ARGON2I')) {
40+
return $this->encodePasswordNative($raw);
41+
}
42+
if (\function_exists('sodium_crypto_pwhash_str')) {
43+
return $this->encodePasswordSodiumFunction($raw);
44+
}
45+
if (\extension_loaded('libsodium')) {
46+
return $this->encodePasswordSodiumExtension($raw);
47+
}
48+
49+
throw new \LogicException('Argon2i algorithm is not supported. Please install the libsodium extension or upgrade to PHP 7.2+.');
50+
}
51+
52+
/**
53+
* {@inheritdoc}
54+
*/
55+
public function isPasswordValid($encoded, $raw, $salt)
56+
{
57+
if (\PHP_VERSION_ID >= 70200 && \defined('PASSWORD_ARGON2I')) {
58+
return !$this->isPasswordTooLong($raw) && password_verify($raw, $encoded);
59+
}
60+
if (\function_exists('sodium_crypto_pwhash_str_verify')) {
61+
$valid = !$this->isPasswordTooLong($raw) && \sodium_crypto_pwhash_str_verify($encoded, $raw);
62+
\sodium_memzero($raw);
63+
64+
return $valid;
65+
}
66+
if (\extension_loaded('libsodium')) {
67+
$valid = !$this->isPasswordTooLong($raw) && \Sodium\crypto_pwhash_str_verify($encoded, $raw);
68+
\Sodium\memzero($raw);
69+
70+
return $valid;
71+
}
72+
73+
throw new \LogicException('Argon2i algorithm is not supported. Please install the libsodium extension or upgrade to PHP 7.2+.');
74+
}
75+
76+
private function encodePasswordNative($raw)
77+
{
78+
return password_hash($raw, \PASSWORD_ARGON2I);
79+
}
80+
81+
private function encodePasswordSodiumFunction($raw)
82+
{
83+
$hash = \sodium_crypto_pwhash_str(
84+
$raw,
85+
\SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
86+
\SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
87+
);
88+
\sodium_memzero($raw);
89+
90+
return $hash;
91+
}
92+
93+
private function encodePasswordSodiumExtension($raw)
94+
{
95+
$hash = \Sodium\crypto_pwhash_str(
96+
$raw,
97+
\Sodium\CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
98+
\Sodium\CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
99+
);
100+
\Sodium\memzero($raw);
101+
102+
return $hash;
103+
}
104+
}

src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ private function getEncoderConfigFromAlgorithm($config)
109109
'class' => BCryptPasswordEncoder::class,
110110
'arguments' => array($config['cost']),
111111
);
112+
113+
case 'argon2i':
114+
return array(
115+
'class' => Argon2iPasswordEncoder::class,
116+
'arguments' => array(),
117+
);
112118
}
113119

114120
return array(
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Tests\Encoder;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
16+
17+
/**
18+
* @author Zan Baldwin <[email protected]>
19+
*/
20+
class Argon2iPasswordEncoderTest extends TestCase
21+
{
22+
const PASSWORD = 'password';
23+
24+
protected function setUp()
25+
{
26+
if (!Argon2iPasswordEncoder::isSupported()) {
27+
$this->markTestSkipped('Argon2i algorithm is not supported.');
28+
}
29+
}
30+
31+
public function testValidation()
32+
{
33+
$encoder = new Argon2iPasswordEncoder();
34+
$result = $encoder->encodePassword(self::PASSWORD, null);
35+
$this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, null));
36+
$this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword', null));
37+
}
38+
39+
/**
40+
* @expectedException \Symfony\Component\Security\Core\Exception\BadCredentialsException
41+
*/
42+
public function testEncodePasswordLength()
43+
{
44+
$encoder = new Argon2iPasswordEncoder();
45+
$encoder->encodePassword(str_repeat('a', 4097), 'salt');
46+
}
47+
48+
public function testCheckPasswordLength()
49+
{
50+
$encoder = new Argon2iPasswordEncoder();
51+
$result = $encoder->encodePassword(str_repeat('a', 4096), null);
52+
$this->assertFalse($encoder->isPasswordValid($result, str_repeat('a', 4097), null));
53+
$this->assertTrue($encoder->isPasswordValid($result, str_repeat('a', 4096), null));
54+
}
55+
56+
public function testUserProvidedSaltIsNotUsed()
57+
{
58+
$encoder = new Argon2iPasswordEncoder();
59+
$result = $encoder->encodePassword(self::PASSWORD, 'salt');
60+
$this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, 'anotherSalt'));
61+
}
62+
}

0 commit comments

Comments
 (0)