feature #30968 [Security] Add Argon2idPasswordEncoder (chalasr)

This PR was merged into the 4.3-dev branch.

Discussion
----------

[Security] Add Argon2idPasswordEncoder

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | #28093
| License       | MIT
| Doc PR        | TODO

Currently we have a `Argon2iPasswordEncoder` that may hash passwords using `argon2id` instead of `argon2i` (platform-dependent) which is not good.
This deprecates producing/validating `argon2id` hashed passwords using the `Argon2iPasswordEncoder`, and adds a `Argon2idPasswordEncoder` able to produce/validate `argon2id` hashed passwords only.

#EUFOSSA

Commits
-------

0c82173b24 [Security] Add Argon2idPasswordEncoder
This commit is contained in:
Fabien Potencier 2019-04-08 08:14:55 +02:00
commit fa7df09dbb
17 changed files with 430 additions and 50 deletions

View File

@ -151,8 +151,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 TwigBridge
========== ----------
* deprecated the `$requestStack` and `$requestContext` arguments of the * deprecated the `$requestStack` and `$requestContext` arguments of the
`HttpFoundationExtension`, pass a `Symfony\Component\HttpFoundation\UrlHelper` `HttpFoundationExtension`, pass a `Symfony\Component\HttpFoundation\UrlHelper`

View File

@ -326,6 +326,9 @@ Security
} }
``` ```
* Using `Argon2iPasswordEncoder` while only the `argon2id` algorithm is supported
now throws a \LogicException`, use `Argon2idPasswordEncoder` instead
SecurityBundle SecurityBundle
-------------- --------------
@ -345,6 +348,8 @@ SecurityBundle
changed to underscores. changed to underscores.
Before: `my-cookie` deleted the `my_cookie` cookie (with an underscore). Before: `my-cookie` deleted the `my_cookie` cookie (with an underscore).
After: `my-cookie` deletes the `my-cookie` cookie (with a dash). 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 Serializer
---------- ----------

View File

@ -8,6 +8,9 @@ CHANGELOG
option is deprecated and will be disabled in Symfony 5.0. This affects to cookies 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` 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). 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 4.2.0
----- -----

View File

@ -29,6 +29,7 @@ use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; 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\Encoder\Argon2iPasswordEncoder;
use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Controller\UserValueResolver; use Symfony\Component\Security\Http\Controller\UserValueResolver;
@ -570,6 +571,8 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
} }
throw new InvalidConfigurationException('Argon2i algorithm is not supported. Install the libsodium extension or use BCrypt instead.'); 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 [ return [
@ -582,6 +585,22 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
]; ];
} }
// 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 // run-time configured encoder
return $config; return $config;
} }

View File

@ -18,6 +18,7 @@ use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
use Symfony\Component\Security\Core\Encoder\Argon2idPasswordEncoder;
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder; use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
abstract class CompleteConfigurationTest extends TestCase abstract class CompleteConfigurationTest extends TestCase
@ -313,7 +314,7 @@ abstract class CompleteConfigurationTest extends TestCase
public function testEncodersWithLibsodium() public function testEncodersWithLibsodium()
{ {
if (!Argon2iPasswordEncoder::isSupported()) { if (!Argon2iPasswordEncoder::isSupported() || \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
$this->markTestSkipped('Argon2i algorithm is not supported.'); $this->markTestSkipped('Argon2i algorithm is not supported.');
} }
@ -364,6 +365,59 @@ abstract class CompleteConfigurationTest extends TestCase
]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); ]], $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() public function testRememberMeThrowExceptionsDefault()
{ {
$container = $this->getContainer('container1'); $container = $this->getContainer('container1');

View File

@ -0,0 +1,14 @@
<?php
$this->load('container1.php', $container);
$container->loadFromExtension('security', [
'encoders' => [
'JMS\FooBundle\Entity\User7' => [
'algorithm' => 'argon2id',
'memory_cost' => 256,
'time_cost' => 1,
'threads' => 2,
],
],
]);

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:sec="http://symfony.com/schema/dic/security"
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
<imports>
<import resource="container1.xml"/>
</imports>
<sec:config>
<sec:encoder class="JMS\FooBundle\Entity\User7" algorithm="argon2id" memory_cost="256" time_cost="1" threads="2" />
</sec:config>
</container>

View File

@ -0,0 +1,10 @@
imports:
- { resource: container1.yml }
security:
encoders:
JMS\FooBundle\Entity\User7:
algorithm: argon2id
memory_cost: 256
time_cost: 1
threads: 2

View File

@ -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\Argon2idPasswordEncoder;
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder; 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;
@ -72,7 +73,7 @@ class UserPasswordEncoderCommandTest extends WebTestCase
public function testEncodePasswordArgon2i() public function testEncodePasswordArgon2i()
{ {
if (!Argon2iPasswordEncoder::isSupported()) { if (!Argon2iPasswordEncoder::isSupported() || \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
$this->markTestSkipped('Argon2i algorithm not available.'); $this->markTestSkipped('Argon2i algorithm not available.');
} }
$this->setupArgon2i(); $this->setupArgon2i();
@ -85,6 +86,27 @@ class UserPasswordEncoderCommandTest extends WebTestCase
$output = $this->passwordEncoderCommandTester->getDisplay(); $output = $this->passwordEncoderCommandTester->getDisplay();
$this->assertContains('Password encoding succeeded', $output); $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(); $encoder = new Argon2iPasswordEncoder();
preg_match('# Encoded password\s+(\$argon2id?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches); preg_match('# Encoded password\s+(\$argon2id?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches);
$hash = $matches[1]; $hash = $matches[1];
@ -153,8 +175,8 @@ class UserPasswordEncoderCommandTest extends WebTestCase
public function testEncodePasswordArgon2iOutput() public function testEncodePasswordArgon2iOutput()
{ {
if (!Argon2iPasswordEncoder::isSupported()) { if (!Argon2iPasswordEncoder::isSupported() || \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
$this->markTestSkipped('Argon2i algorithm not available.'); $this->markTestSkipped('Argon2id algorithm not available.');
} }
$this->setupArgon2i(); $this->setupArgon2i();
@ -167,6 +189,22 @@ class UserPasswordEncoderCommandTest extends WebTestCase
$this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); $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() public function testEncodePasswordNoConfigForGivenUserClass()
{ {
if (method_exists($this, 'expectException')) { if (method_exists($this, 'expectException')) {
@ -259,4 +297,17 @@ EOTXT
$this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand); $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);
}
} }

View File

@ -0,0 +1,7 @@
imports:
- { resource: config.yml }
security:
encoders:
Custom\Class\Argon2id\User:
algorithm: argon2id

View File

@ -19,6 +19,9 @@ CHANGELOG
* Dispatch `AuthenticationFailureEvent` on `security.authentication.failure` * Dispatch `AuthenticationFailureEvent` on `security.authentication.failure`
* Dispatch `InteractiveLoginEvent` on `security.interactive_login` * Dispatch `InteractiveLoginEvent` on `security.interactive_login`
* Dispatch `SwitchUserEvent` on `security.switch_user` * Dispatch `SwitchUserEvent` on `security.switch_user`
* Added `Argon2idPasswordEncoder`
* Deprecated using `Argon2iPasswordEncoder` while only the `argon2id` algorithm
is supported, use `Argon2idPasswordEncoder` instead
4.2.0 4.2.0
----- -----

View File

@ -0,0 +1,52 @@
<?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;
/**
* @internal
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
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;
}
}

View File

@ -21,25 +21,7 @@ use Symfony\Component\Security\Core\Exception\BadCredentialsException;
*/ */
class Argon2iPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface class Argon2iPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface
{ {
private $config = []; use Argon2Trait;
/**
* 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,
];
}
}
public static function isSupported() public static function isSupported()
{ {
@ -64,10 +46,13 @@ class Argon2iPasswordEncoder extends BasePasswordEncoder implements SelfSaltingE
} }
if (\PHP_VERSION_ID >= 70200 && \defined('PASSWORD_ARGON2I')) { if (\PHP_VERSION_ID >= 70200 && \defined('PASSWORD_ARGON2I')) {
return $this->encodePasswordNative($raw); return $this->encodePasswordNative($raw, \PASSWORD_ARGON2I);
} } elseif (\function_exists('sodium_crypto_pwhash_str')) {
if (\function_exists('sodium_crypto_pwhash_str')) { if (0 === strpos($hash = $this->encodePasswordSodiumFunction($raw), Argon2idPasswordEncoder::HASH_PREFIX)) {
return $this->encodePasswordSodiumFunction($raw); @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')) { if (\extension_loaded('libsodium')) {
return $this->encodePasswordSodiumExtension($raw); return $this->encodePasswordSodiumExtension($raw);
@ -81,10 +66,20 @@ class Argon2iPasswordEncoder extends BasePasswordEncoder implements SelfSaltingE
*/ */
public function isPasswordValid($encoded, $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". if ($this->isPasswordTooLong($raw)) {
// In this case, "password_verify()" cannot be used. return false;
if (\PHP_VERSION_ID >= 70200 && \defined('PASSWORD_ARGON2I') && (false === strpos($encoded, '$argon2id$'))) { }
return !$this->isPasswordTooLong($raw) && password_verify($raw, $encoded);
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')) { if (\function_exists('sodium_crypto_pwhash_str_verify')) {
$valid = !$this->isPasswordTooLong($raw) && \sodium_crypto_pwhash_str_verify($encoded, $raw); $valid = !$this->isPasswordTooLong($raw) && \sodium_crypto_pwhash_str_verify($encoded, $raw);
@ -102,23 +97,6 @@ class Argon2iPasswordEncoder extends BasePasswordEncoder implements SelfSaltingE
throw new \LogicException('Argon2i algorithm is not supported. Please install the libsodium extension or upgrade to PHP 7.2+.'); 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) private function encodePasswordSodiumExtension($raw)
{ {
$hash = \Sodium\crypto_pwhash_str( $hash = \Sodium\crypto_pwhash_str(

View File

@ -0,0 +1,85 @@
<?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;
use Symfony\Component\Security\Core\Exception\LogicException;
/**
* Hashes passwords using the Argon2id algorithm.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*
* @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+.');
}
}

View File

@ -117,6 +117,15 @@ class EncoderFactory implements EncoderFactoryInterface
$config['threads'], $config['threads'],
], ],
]; ];
case 'argon2id':
return [
'class' => Argon2idPasswordEncoder::class,
'arguments' => [
$config['memory_cost'],
$config['time_cost'],
$config['threads'],
],
];
} }
return [ return [

View File

@ -23,7 +23,7 @@ class Argon2iPasswordEncoderTest extends TestCase
protected function setUp() protected function setUp()
{ {
if (!Argon2iPasswordEncoder::isSupported()) { if (!Argon2iPasswordEncoder::isSupported() || \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
$this->markTestSkipped('Argon2i algorithm is not supported.'); $this->markTestSkipped('Argon2i algorithm is not supported.');
} }
} }

View File

@ -0,0 +1,65 @@
<?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\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'));
}
}