[Security] Replace Argon2*PasswordEncoder by SodiumPasswordEncoder

This reverts commit dc95a6fec6.
This commit is contained in:
Robin Chalas 2019-04-08 19:01:45 +02:00
parent 9a7a2767da
commit 529211d7ed
20 changed files with 268 additions and 346 deletions

View File

@ -151,14 +151,12 @@ Security
} }
``` ```
* Using `Argon2iPasswordEncoder` while only the `argon2id` algorithm is supported * The `Argon2iPasswordEncoder` class has been deprecated, use `SodiumPasswordEncoder` instead.
is deprecated, use `Argon2idPasswordEncoder` instead
SecurityBundle SecurityBundle
-------------- --------------
* Configuring encoders using `argon2i` as algorithm while only `argon2id` is * Configuring encoders using `argon2i` as algorithm has been deprecated, use `sodium` instead.
supported is deprecated, use `argon2id` instead
TwigBridge TwigBridge
---------- ----------

View File

@ -326,8 +326,7 @@ Security
} }
``` ```
* Using `Argon2iPasswordEncoder` while only the `argon2id` algorithm is supported * The `Argon2iPasswordEncoder` class has been removed, use `SodiumPasswordEncoder` instead.
now throws a `\LogicException`, use `Argon2idPasswordEncoder` instead
SecurityBundle SecurityBundle
-------------- --------------
@ -348,8 +347,7 @@ 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 * Configuring encoders using `argon2i` as algorithm is not supported anymore, use `sodium` instead.
now throws a `\LogicException`, use `argon2id` instead
Serializer Serializer
---------- ----------

View File

@ -8,9 +8,7 @@ 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, * Deprecated configuring encoders using `argon2i` as algorithm, use `sodium` instead
use `argon2id` instead
4.2.0 4.2.0
----- -----

View File

@ -29,8 +29,8 @@ 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\Encoder\SodiumPasswordEncoder;
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;
use Symfony\Component\Templating\PhpEngine; use Symfony\Component\Templating\PhpEngine;
@ -565,14 +565,14 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
// Argon2i encoder // Argon2i encoder
if ('argon2i' === $config['algorithm']) { if ('argon2i' === $config['algorithm']) {
@trigger_error('Configuring an encoder with "argon2i" as algorithm is deprecated since Symfony 4.3, use "sodium" instead.', E_USER_DEPRECATED);
if (!Argon2iPasswordEncoder::isSupported()) { if (!Argon2iPasswordEncoder::isSupported()) {
if (\extension_loaded('sodium') && !\defined('SODIUM_CRYPTO_PWHASH_SALTBYTES')) { if (\extension_loaded('sodium') && !\defined('SODIUM_CRYPTO_PWHASH_SALTBYTES')) {
throw new InvalidConfigurationException('The installed libsodium version does not have support for Argon2i. Use Bcrypt instead.'); throw new InvalidConfigurationException('The installed libsodium version does not have support for Argon2i. Use Bcrypt instead.');
} }
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('PASSWORD_ARGON2I') && Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) {
@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 [
@ -585,19 +585,14 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
]; ];
} }
// Argon2id encoder if ('sodium' === $config['algorithm']) {
if ('argon2id' === $config['algorithm']) { if (!SodiumPasswordEncoder::isSupported()) {
if (!Argon2idPasswordEncoder::isSupported()) { throw new InvalidConfigurationException('Libsodium is not available. Install the sodium extension or use BCrypt instead.');
throw new InvalidConfigurationException('Argon2i algorithm is not supported. Install the libsodium extension or use BCrypt instead.');
} }
return [ return [
'class' => Argon2idPasswordEncoder::class, 'class' => SodiumPasswordEncoder::class,
'arguments' => [ 'arguments' => [],
$config['memory_cost'],
$config['time_cost'],
$config['threads'],
],
]; ];
} }

View File

@ -18,8 +18,8 @@ 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;
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
abstract class CompleteConfigurationTest extends TestCase abstract class CompleteConfigurationTest extends TestCase
{ {
@ -314,7 +314,65 @@ abstract class CompleteConfigurationTest extends TestCase
public function testEncodersWithLibsodium() public function testEncodersWithLibsodium()
{ {
if (!Argon2iPasswordEncoder::isSupported() || \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) { if (!SodiumPasswordEncoder::isSupported()) {
$this->markTestSkipped('Libsodium is not available.');
}
$container = $this->getContainer('sodium_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\SodiumPasswordEncoder',
'arguments' => [],
],
]], $container->getDefinition('security.encoder_factory.generic')->getArguments());
}
/**
* @group legacy
*
* @expectedDeprecation Configuring an encoder with "argon2i" as algorithm is deprecated since Symfony 4.3, use "sodium" instead.
*/
public function testEncodersWithArgon2i()
{
if (!Argon2iPasswordEncoder::isSupported()) {
$this->markTestSkipped('Argon2i algorithm is not supported.'); $this->markTestSkipped('Argon2i algorithm is not supported.');
} }
@ -365,59 +423,6 @@ 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

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

View File

@ -10,7 +10,7 @@
</imports> </imports>
<sec:config> <sec:config>
<sec:encoder class="JMS\FooBundle\Entity\User7" algorithm="argon2id" memory_cost="256" time_cost="1" threads="2" /> <sec:encoder class="JMS\FooBundle\Entity\User7" algorithm="sodium" />
</sec:config> </sec:config>
</container> </container>

View File

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

View File

@ -0,0 +1,7 @@
imports:
- { resource: container1.yml }
security:
encoders:
JMS\FooBundle\Entity\User7:
algorithm: sodium

View File

@ -15,11 +15,11 @@ 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;
use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder; use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder;
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
/** /**
* Tests UserPasswordEncoderCommand. * Tests UserPasswordEncoderCommand.
@ -71,9 +71,12 @@ class UserPasswordEncoderCommandTest extends WebTestCase
$this->assertTrue($encoder->isPasswordValid($hash, 'password', null)); $this->assertTrue($encoder->isPasswordValid($hash, 'password', null));
} }
/**
* @group legacy
*/
public function testEncodePasswordArgon2i() public function testEncodePasswordArgon2i()
{ {
if (!Argon2iPasswordEncoder::isSupported() || !\defined('PASSWORD_ARGON2I') && Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) { if (!Argon2iPasswordEncoder::isSupported()) {
$this->markTestSkipped('Argon2i algorithm not available.'); $this->markTestSkipped('Argon2i algorithm not available.');
} }
$this->setupArgon2i(); $this->setupArgon2i();
@ -87,30 +90,29 @@ class UserPasswordEncoderCommandTest extends WebTestCase
$this->assertContains('Password encoding succeeded', $output); $this->assertContains('Password encoding succeeded', $output);
$encoder = new Argon2iPasswordEncoder(); $encoder = new Argon2iPasswordEncoder();
preg_match('# Encoded password\s+(\$argon2i?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches); preg_match('# Encoded password\s+(\$argon2id?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches);
$hash = $matches[1]; $hash = $matches[1];
$this->assertTrue($encoder->isPasswordValid($hash, 'password', null)); $this->assertTrue($encoder->isPasswordValid($hash, 'password', null));
} }
public function testEncodePasswordArgon2id() public function testEncodePasswordSodium()
{ {
if (!Argon2idPasswordEncoder::isSupported()) { if (!SodiumPasswordEncoder::isSupported()) {
$this->markTestSkipped('Argon2id algorithm not available.'); $this->markTestSkipped('Libsodium is not available.');
} }
$this->setupArgon2id(); $this->setupSodium();
$this->passwordEncoderCommandTester->execute([ $this->passwordEncoderCommandTester->execute([
'command' => 'security:encode-password', 'command' => 'security:encode-password',
'password' => 'password', 'password' => 'password',
'user-class' => 'Custom\Class\Argon2id\User', 'user-class' => 'Custom\Class\Sodium\User',
], ['interactive' => false]); ], ['interactive' => false]);
$output = $this->passwordEncoderCommandTester->getDisplay(); $output = $this->passwordEncoderCommandTester->getDisplay();
$this->assertContains('Password encoding succeeded', $output); $this->assertContains('Password encoding succeeded', $output);
$encoder = new Argon2idPasswordEncoder(); preg_match('# Encoded password\s+(\$?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches);
preg_match('# Encoded password\s+(\$argon2id?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches);
$hash = $matches[1]; $hash = $matches[1];
$this->assertTrue($encoder->isPasswordValid($hash, 'password', null)); $this->assertTrue((new SodiumPasswordEncoder())->isPasswordValid($hash, 'password', null));
} }
public function testEncodePasswordPbkdf2() public function testEncodePasswordPbkdf2()
@ -173,10 +175,13 @@ class UserPasswordEncoderCommandTest extends WebTestCase
$this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); $this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay());
} }
/**
* @group legacy
*/
public function testEncodePasswordArgon2iOutput() public function testEncodePasswordArgon2iOutput()
{ {
if (!Argon2iPasswordEncoder::isSupported() || !\defined('PASSWORD_ARGON2I') && Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) { if (!Argon2iPasswordEncoder::isSupported()) {
$this->markTestSkipped('Argon2id algorithm not available.'); $this->markTestSkipped('Argon2i algorithm not available.');
} }
$this->setupArgon2i(); $this->setupArgon2i();
@ -189,17 +194,17 @@ class UserPasswordEncoderCommandTest extends WebTestCase
$this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); $this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay());
} }
public function testEncodePasswordArgon2idOutput() public function testEncodePasswordSodiumOutput()
{ {
if (!Argon2idPasswordEncoder::isSupported()) { if (!SodiumPasswordEncoder::isSupported()) {
$this->markTestSkipped('Argon2id algorithm not available.'); $this->markTestSkipped('Libsodium is not available.');
} }
$this->setupArgon2id(); $this->setupSodium();
$this->passwordEncoderCommandTester->execute([ $this->passwordEncoderCommandTester->execute([
'command' => 'security:encode-password', 'command' => 'security:encode-password',
'password' => 'p@ssw0rd', 'password' => 'p@ssw0rd',
'user-class' => 'Custom\Class\Argon2id\User', 'user-class' => 'Custom\Class\Sodium\User',
], ['interactive' => false]); ], ['interactive' => false]);
$this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); $this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay());
@ -298,10 +303,10 @@ EOTXT
$this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand); $this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand);
} }
private function setupArgon2id() private function setupSodium()
{ {
putenv('COLUMNS='.(119 + \strlen(PHP_EOL))); putenv('COLUMNS='.(119 + \strlen(PHP_EOL)));
$kernel = $this->createKernel(['test_case' => 'PasswordEncode', 'root_config' => 'argon2id.yml']); $kernel = $this->createKernel(['test_case' => 'PasswordEncode', 'root_config' => 'sodium.yml']);
$kernel->boot(); $kernel->boot();
$application = new Application($kernel); $application = new Application($kernel);

View File

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

View File

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

View File

@ -19,9 +19,7 @@ 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 `Argon2iPasswordEncoder`, use `SodiumPasswordEncoder` instead
* Deprecated using `Argon2iPasswordEncoder` while only the `argon2id` algorithm
is supported, use `Argon2idPasswordEncoder` instead
4.2.0 4.2.0
----- -----

View File

@ -1,52 +0,0 @@
<?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

@ -11,6 +11,8 @@
namespace Symfony\Component\Security\Core\Encoder; namespace Symfony\Component\Security\Core\Encoder;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" instead.', Argon2iPasswordEncoder::class, SodiumPasswordEncoder::class), E_USER_DEPRECATED);
use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\BadCredentialsException;
/** /**
@ -18,10 +20,30 @@ use Symfony\Component\Security\Core\Exception\BadCredentialsException;
* *
* @author Zan Baldwin <hello@zanbaldwin.com> * @author Zan Baldwin <hello@zanbaldwin.com>
* @author Dominik Müller <dominik.mueller@jkweb.ch> * @author Dominik Müller <dominik.mueller@jkweb.ch>
*
* @deprecated since Symfony 4.3, use SodiumPasswordEncoder instead
*/ */
class Argon2iPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface class Argon2iPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface
{ {
use Argon2Trait; private $config = [];
/**
* 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()
{ {
@ -46,12 +68,9 @@ 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, \PASSWORD_ARGON2I); return $this->encodePasswordNative($raw);
} elseif (\function_exists('sodium_crypto_pwhash_str')) { }
if (Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) { if (\function_exists('sodium_crypto_pwhash_str')) {
@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 $this->encodePasswordSodiumFunction($raw); return $this->encodePasswordSodiumFunction($raw);
} }
if (\extension_loaded('libsodium')) { if (\extension_loaded('libsodium')) {
@ -66,20 +85,10 @@ class Argon2iPasswordEncoder extends BasePasswordEncoder implements SelfSaltingE
*/ */
public function isPasswordValid($encoded, $raw, $salt) public function isPasswordValid($encoded, $raw, $salt)
{ {
if ($this->isPasswordTooLong($raw)) { // If $encoded was created via "sodium_crypto_pwhash_str()", the hashing algorithm may be "argon2id" instead of "argon2i".
return false; // In this case, "password_verify()" cannot be used.
} if (\PHP_VERSION_ID >= 70200 && \defined('PASSWORD_ARGON2I') && (false === strpos($encoded, '$argon2id$'))) {
return !$this->isPasswordTooLong($raw) && password_verify($raw, $encoded);
// 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);
}
if (\PHP_VERSION_ID >= 70200 && \defined('PASSWORD_ARGON2I')) {
// 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);
@ -97,6 +106,23 @@ 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

@ -1,88 +0,0 @@
<?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 (!self::isDefaultSodiumAlgorithm()) {
throw new LogicException('Algorithm "argon2id" is not supported. Please install the libsodium extension or upgrade to PHP 7.3+.');
}
return $this->encodePasswordSodiumFunction($raw);
}
/**
* {@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+.');
}
/**
* @internal
*/
public static function isDefaultSodiumAlgorithm()
{
return \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')
&& \defined('SODIUM_CRYPTO_PWHASH_ALG_DEFAULT')
&& \SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13 === \SODIUM_CRYPTO_PWHASH_ALG_DEFAULT;
}
}

View File

@ -108,6 +108,13 @@ class EncoderFactory implements EncoderFactoryInterface
'arguments' => [$config['cost']], 'arguments' => [$config['cost']],
]; ];
case 'sodium':
return [
'class' => SodiumPasswordEncoder::class,
'arguments' => [],
];
/* @deprecated since Symfony 4.3 */
case 'argon2i': case 'argon2i':
return [ return [
'class' => Argon2iPasswordEncoder::class, 'class' => Argon2iPasswordEncoder::class,
@ -117,15 +124,6 @@ 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

@ -0,0 +1,84 @@
<?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 libsodium.
*
* @author Robin Chalas <robin.chalas@gmail.com>
* @author Zan Baldwin <hello@zanbaldwin.com>
* @author Dominik Müller <dominik.mueller@jkweb.ch>
*
* @final
*/
class SodiumPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface
{
public static function isSupported(): bool
{
if (\class_exists('ParagonIE_Sodium_Compat') && \method_exists('ParagonIE_Sodium_Compat', 'crypto_pwhash_is_available')) {
return \ParagonIE_Sodium_Compat::crypto_pwhash_is_available();
}
return \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 (\function_exists('sodium_crypto_pwhash_str')) {
return \sodium_crypto_pwhash_str(
$raw,
\SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
\SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
);
}
if (\extension_loaded('libsodium')) {
return \Sodium\crypto_pwhash_str(
$raw,
\Sodium\CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
\Sodium\CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
);
}
throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.');
}
/**
* {@inheritdoc}
*/
public function isPasswordValid($encoded, $raw, $salt)
{
if ($this->isPasswordTooLong($raw)) {
return false;
}
if (\function_exists('sodium_crypto_pwhash_str_verify')) {
return \sodium_crypto_pwhash_str_verify($encoded, $raw);
}
if (\extension_loaded('libsodium')) {
return \Sodium\crypto_pwhash_str_verify($encoded, $raw);
}
throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.');
}
}

View File

@ -12,22 +12,26 @@
namespace Symfony\Component\Security\Core\Tests\Encoder; namespace Symfony\Component\Security\Core\Tests\Encoder;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Encoder\Argon2idPasswordEncoder;
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder; use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
/** /**
* @author Zan Baldwin <hello@zanbaldwin.com> * @author Zan Baldwin <hello@zanbaldwin.com>
*
* @group legacy
*/ */
class Argon2iPasswordEncoderTest extends TestCase class Argon2iPasswordEncoderTest extends TestCase
{ {
const PASSWORD = 'password'; const PASSWORD = 'password';
public function testValidationWithConfig() protected function setUp()
{ {
if (!Argon2iPasswordEncoder::isSupported() || Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) { if (!Argon2iPasswordEncoder::isSupported()) {
$this->markTestSkipped('Argon2i algorithm is not supported.'); $this->markTestSkipped('Argon2i algorithm is not supported.');
} }
}
public function testValidationWithConfig()
{
$encoder = new Argon2iPasswordEncoder(8, 4, 1); $encoder = new Argon2iPasswordEncoder(8, 4, 1);
$result = $encoder->encodePassword(self::PASSWORD, null); $result = $encoder->encodePassword(self::PASSWORD, null);
$this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, null)); $this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, null));
@ -36,10 +40,6 @@ class Argon2iPasswordEncoderTest extends TestCase
public function testValidation() public function testValidation()
{ {
if (!Argon2iPasswordEncoder::isSupported() || Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) {
$this->markTestSkipped('Argon2i algorithm is not supported.');
}
$encoder = new Argon2iPasswordEncoder(); $encoder = new Argon2iPasswordEncoder();
$result = $encoder->encodePassword(self::PASSWORD, null); $result = $encoder->encodePassword(self::PASSWORD, null);
$this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, null)); $this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, null));
@ -51,20 +51,12 @@ class Argon2iPasswordEncoderTest extends TestCase
*/ */
public function testEncodePasswordLength() public function testEncodePasswordLength()
{ {
if (!Argon2iPasswordEncoder::isSupported() || Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) {
$this->markTestSkipped('Argon2i algorithm is not supported.');
}
$encoder = new Argon2iPasswordEncoder(); $encoder = new Argon2iPasswordEncoder();
$encoder->encodePassword(str_repeat('a', 4097), 'salt'); $encoder->encodePassword(str_repeat('a', 4097), 'salt');
} }
public function testCheckPasswordLength() public function testCheckPasswordLength()
{ {
if (!Argon2iPasswordEncoder::isSupported() || Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) {
$this->markTestSkipped('Argon2i algorithm is not supported.');
}
$encoder = new Argon2iPasswordEncoder(); $encoder = new Argon2iPasswordEncoder();
$result = $encoder->encodePassword(str_repeat('a', 4096), null); $result = $encoder->encodePassword(str_repeat('a', 4096), null);
$this->assertFalse($encoder->isPasswordValid($result, str_repeat('a', 4097), null)); $this->assertFalse($encoder->isPasswordValid($result, str_repeat('a', 4097), null));
@ -73,29 +65,8 @@ class Argon2iPasswordEncoderTest extends TestCase
public function testUserProvidedSaltIsNotUsed() public function testUserProvidedSaltIsNotUsed()
{ {
if (!Argon2iPasswordEncoder::isSupported() || Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) {
$this->markTestSkipped('Argon2i algorithm is not supported.');
}
$encoder = new Argon2iPasswordEncoder(); $encoder = new Argon2iPasswordEncoder();
$result = $encoder->encodePassword(self::PASSWORD, 'salt'); $result = $encoder->encodePassword(self::PASSWORD, 'salt');
$this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, 'anotherSalt')); $this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, 'anotherSalt'));
} }
/**
* @group legacy
* @exectedDeprecation Using "Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder" while only the "argon2id" algorithm is supported is deprecated since Symfony 4.3, use "Symfony\Component\Security\Core\Encoder\Argon2idPasswordEncoder" instead.
* @exectedDeprecation Calling "Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder::isPasswordValid()" with a password hashed using argon2id is deprecated since Symfony 4.3, use "Symfony\Component\Security\Core\Encoder\Argon2idPasswordEncoder" instead.
*/
public function testEncodeWithArgon2idSupportOnly()
{
if (!Argon2iPasswordEncoder::isSupported() || !Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) {
$this->markTestSkipped('Argon2id algorithm not available.');
}
$encoder = new Argon2iPasswordEncoder();
$result = $encoder->encodePassword(self::PASSWORD, null);
$this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, null));
$this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword', null));
}
} }

View File

@ -12,28 +12,20 @@
namespace Symfony\Component\Security\Core\Tests\Encoder; namespace Symfony\Component\Security\Core\Tests\Encoder;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Encoder\Argon2idPasswordEncoder; use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
class Argon2idPasswordEncoderTest extends TestCase class SodiumPasswordEncoderTest extends TestCase
{ {
protected function setUp() protected function setUp()
{ {
if (!Argon2idPasswordEncoder::isSupported()) { if (!SodiumPasswordEncoder::isSupported()) {
$this->markTestSkipped('Argon2i algorithm is not supported.'); $this->markTestSkipped('Libsodium is not available.');
} }
} }
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() public function testValidation()
{ {
$encoder = new Argon2idPasswordEncoder(); $encoder = new SodiumPasswordEncoder();
$result = $encoder->encodePassword('password', null); $result = $encoder->encodePassword('password', null);
$this->assertTrue($encoder->isPasswordValid($result, 'password', null)); $this->assertTrue($encoder->isPasswordValid($result, 'password', null));
$this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword', null)); $this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword', null));
@ -44,13 +36,13 @@ class Argon2idPasswordEncoderTest extends TestCase
*/ */
public function testEncodePasswordLength() public function testEncodePasswordLength()
{ {
$encoder = new Argon2idPasswordEncoder(); $encoder = new SodiumPasswordEncoder();
$encoder->encodePassword(str_repeat('a', 4097), 'salt'); $encoder->encodePassword(str_repeat('a', 4097), 'salt');
} }
public function testCheckPasswordLength() public function testCheckPasswordLength()
{ {
$encoder = new Argon2idPasswordEncoder(); $encoder = new SodiumPasswordEncoder();
$result = $encoder->encodePassword(str_repeat('a', 4096), null); $result = $encoder->encodePassword(str_repeat('a', 4096), null);
$this->assertFalse($encoder->isPasswordValid($result, str_repeat('a', 4097), null)); $this->assertFalse($encoder->isPasswordValid($result, str_repeat('a', 4097), null));
$this->assertTrue($encoder->isPasswordValid($result, str_repeat('a', 4096), null)); $this->assertTrue($encoder->isPasswordValid($result, str_repeat('a', 4096), null));
@ -58,7 +50,7 @@ class Argon2idPasswordEncoderTest extends TestCase
public function testUserProvidedSaltIsNotUsed() public function testUserProvidedSaltIsNotUsed()
{ {
$encoder = new Argon2idPasswordEncoder(); $encoder = new SodiumPasswordEncoder();
$result = $encoder->encodePassword('password', 'salt'); $result = $encoder->encodePassword('password', 'salt');
$this->assertTrue($encoder->isPasswordValid($result, 'password', 'anotherSalt')); $this->assertTrue($encoder->isPasswordValid($result, 'password', 'anotherSalt'));
} }