feature #31019 [Security] Replace Argon2*PasswordEncoder by SodiumPasswordEncoder (chalasr)
This PR was merged into the 4.3-dev branch.
Discussion
----------
[Security] Replace Argon2*PasswordEncoder by SodiumPasswordEncoder
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | yes
| Tests pass? | yes
| Fixed tickets | #31016
| License | MIT
| Doc PR | todo https://github.com/symfony/symfony-docs/issues/11368
See fixed ticket, much simpler/secure/maintainable.
Commits
-------
529211d7ed
[Security] Replace Argon2*PasswordEncoder by SodiumPasswordEncoder
This commit is contained in:
commit
ace49c152b
@ -151,14 +151,12 @@ Security
|
||||
}
|
||||
```
|
||||
|
||||
* Using `Argon2iPasswordEncoder` while only the `argon2id` algorithm is supported
|
||||
is deprecated, use `Argon2idPasswordEncoder` instead
|
||||
* The `Argon2iPasswordEncoder` class has been deprecated, use `SodiumPasswordEncoder` instead.
|
||||
|
||||
SecurityBundle
|
||||
--------------
|
||||
|
||||
* Configuring encoders using `argon2i` as algorithm while only `argon2id` is
|
||||
supported is deprecated, use `argon2id` instead
|
||||
* Configuring encoders using `argon2i` as algorithm has been deprecated, use `sodium` instead.
|
||||
|
||||
TwigBridge
|
||||
----------
|
||||
|
@ -326,8 +326,7 @@ Security
|
||||
}
|
||||
```
|
||||
|
||||
* Using `Argon2iPasswordEncoder` while only the `argon2id` algorithm is supported
|
||||
now throws a `\LogicException`, use `Argon2idPasswordEncoder` instead
|
||||
* The `Argon2iPasswordEncoder` class has been removed, use `SodiumPasswordEncoder` instead.
|
||||
|
||||
SecurityBundle
|
||||
--------------
|
||||
@ -348,8 +347,7 @@ SecurityBundle
|
||||
changed to underscores.
|
||||
Before: `my-cookie` deleted the `my_cookie` cookie (with an underscore).
|
||||
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
|
||||
* Configuring encoders using `argon2i` as algorithm is not supported anymore, use `sodium` instead.
|
||||
|
||||
Serializer
|
||||
----------
|
||||
|
@ -8,9 +8,7 @@ CHANGELOG
|
||||
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`
|
||||
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
|
||||
|
||||
* Deprecated configuring encoders using `argon2i` as algorithm, use `sodium` instead
|
||||
|
||||
4.2.0
|
||||
-----
|
||||
|
@ -29,8 +29,8 @@ use Symfony\Component\DependencyInjection\Parameter;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
||||
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\SodiumPasswordEncoder;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Controller\UserValueResolver;
|
||||
use Symfony\Component\Templating\PhpEngine;
|
||||
@ -565,14 +565,14 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
|
||||
|
||||
// Argon2i encoder
|
||||
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 (\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('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 [
|
||||
@ -585,19 +585,14 @@ 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.');
|
||||
if ('sodium' === $config['algorithm']) {
|
||||
if (!SodiumPasswordEncoder::isSupported()) {
|
||||
throw new InvalidConfigurationException('Libsodium is not available. Install the sodium extension or use BCrypt instead.');
|
||||
}
|
||||
|
||||
return [
|
||||
'class' => Argon2idPasswordEncoder::class,
|
||||
'arguments' => [
|
||||
$config['memory_cost'],
|
||||
$config['time_cost'],
|
||||
$config['threads'],
|
||||
],
|
||||
'class' => SodiumPasswordEncoder::class,
|
||||
'arguments' => [],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -18,8 +18,8 @@ use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
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\SodiumPasswordEncoder;
|
||||
|
||||
abstract class CompleteConfigurationTest extends TestCase
|
||||
{
|
||||
@ -314,7 +314,65 @@ abstract class CompleteConfigurationTest extends TestCase
|
||||
|
||||
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.');
|
||||
}
|
||||
|
||||
@ -365,59 +423,6 @@ abstract class CompleteConfigurationTest extends TestCase
|
||||
]], $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()
|
||||
{
|
||||
$container = $this->getContainer('container1');
|
||||
|
@ -5,10 +5,7 @@ $this->load('container1.php', $container);
|
||||
$container->loadFromExtension('security', [
|
||||
'encoders' => [
|
||||
'JMS\FooBundle\Entity\User7' => [
|
||||
'algorithm' => 'argon2id',
|
||||
'memory_cost' => 256,
|
||||
'time_cost' => 1,
|
||||
'threads' => 2,
|
||||
'algorithm' => 'sodium',
|
||||
],
|
||||
],
|
||||
]);
|
@ -10,7 +10,7 @@
|
||||
</imports>
|
||||
|
||||
<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>
|
||||
|
||||
</container>
|
@ -1,10 +0,0 @@
|
||||
imports:
|
||||
- { resource: container1.yml }
|
||||
|
||||
security:
|
||||
encoders:
|
||||
JMS\FooBundle\Entity\User7:
|
||||
algorithm: argon2id
|
||||
memory_cost: 256
|
||||
time_cost: 1
|
||||
threads: 2
|
@ -0,0 +1,7 @@
|
||||
imports:
|
||||
- { resource: container1.yml }
|
||||
|
||||
security:
|
||||
encoders:
|
||||
JMS\FooBundle\Entity\User7:
|
||||
algorithm: sodium
|
@ -15,11 +15,11 @@ use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
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\Argon2idPasswordEncoder;
|
||||
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;
|
||||
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
|
||||
|
||||
/**
|
||||
* Tests UserPasswordEncoderCommand.
|
||||
@ -71,9 +71,12 @@ class UserPasswordEncoderCommandTest extends WebTestCase
|
||||
$this->assertTrue($encoder->isPasswordValid($hash, 'password', null));
|
||||
}
|
||||
|
||||
/**
|
||||
* @group legacy
|
||||
*/
|
||||
public function testEncodePasswordArgon2i()
|
||||
{
|
||||
if (!Argon2iPasswordEncoder::isSupported() || !\defined('PASSWORD_ARGON2I') && Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) {
|
||||
if (!Argon2iPasswordEncoder::isSupported()) {
|
||||
$this->markTestSkipped('Argon2i algorithm not available.');
|
||||
}
|
||||
$this->setupArgon2i();
|
||||
@ -87,30 +90,29 @@ class UserPasswordEncoderCommandTest extends WebTestCase
|
||||
$this->assertContains('Password encoding succeeded', $output);
|
||||
|
||||
$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];
|
||||
$this->assertTrue($encoder->isPasswordValid($hash, 'password', null));
|
||||
}
|
||||
|
||||
public function testEncodePasswordArgon2id()
|
||||
public function testEncodePasswordSodium()
|
||||
{
|
||||
if (!Argon2idPasswordEncoder::isSupported()) {
|
||||
$this->markTestSkipped('Argon2id algorithm not available.');
|
||||
if (!SodiumPasswordEncoder::isSupported()) {
|
||||
$this->markTestSkipped('Libsodium is not available.');
|
||||
}
|
||||
$this->setupArgon2id();
|
||||
$this->setupSodium();
|
||||
$this->passwordEncoderCommandTester->execute([
|
||||
'command' => 'security:encode-password',
|
||||
'password' => 'password',
|
||||
'user-class' => 'Custom\Class\Argon2id\User',
|
||||
'user-class' => 'Custom\Class\Sodium\User',
|
||||
], ['interactive' => false]);
|
||||
|
||||
$output = $this->passwordEncoderCommandTester->getDisplay();
|
||||
$this->assertContains('Password encoding succeeded', $output);
|
||||
|
||||
$encoder = new Argon2idPasswordEncoder();
|
||||
preg_match('# Encoded password\s+(\$argon2id?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches);
|
||||
preg_match('# Encoded password\s+(\$?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches);
|
||||
$hash = $matches[1];
|
||||
$this->assertTrue($encoder->isPasswordValid($hash, 'password', null));
|
||||
$this->assertTrue((new SodiumPasswordEncoder())->isPasswordValid($hash, 'password', null));
|
||||
}
|
||||
|
||||
public function testEncodePasswordPbkdf2()
|
||||
@ -173,10 +175,13 @@ class UserPasswordEncoderCommandTest extends WebTestCase
|
||||
$this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group legacy
|
||||
*/
|
||||
public function testEncodePasswordArgon2iOutput()
|
||||
{
|
||||
if (!Argon2iPasswordEncoder::isSupported() || !\defined('PASSWORD_ARGON2I') && Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) {
|
||||
$this->markTestSkipped('Argon2id algorithm not available.');
|
||||
if (!Argon2iPasswordEncoder::isSupported()) {
|
||||
$this->markTestSkipped('Argon2i algorithm not available.');
|
||||
}
|
||||
|
||||
$this->setupArgon2i();
|
||||
@ -189,17 +194,17 @@ class UserPasswordEncoderCommandTest extends WebTestCase
|
||||
$this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay());
|
||||
}
|
||||
|
||||
public function testEncodePasswordArgon2idOutput()
|
||||
public function testEncodePasswordSodiumOutput()
|
||||
{
|
||||
if (!Argon2idPasswordEncoder::isSupported()) {
|
||||
$this->markTestSkipped('Argon2id algorithm not available.');
|
||||
if (!SodiumPasswordEncoder::isSupported()) {
|
||||
$this->markTestSkipped('Libsodium is not available.');
|
||||
}
|
||||
|
||||
$this->setupArgon2id();
|
||||
$this->setupSodium();
|
||||
$this->passwordEncoderCommandTester->execute([
|
||||
'command' => 'security:encode-password',
|
||||
'password' => 'p@ssw0rd',
|
||||
'user-class' => 'Custom\Class\Argon2id\User',
|
||||
'user-class' => 'Custom\Class\Sodium\User',
|
||||
], ['interactive' => false]);
|
||||
|
||||
$this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay());
|
||||
@ -298,10 +303,10 @@ EOTXT
|
||||
$this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand);
|
||||
}
|
||||
|
||||
private function setupArgon2id()
|
||||
private function setupSodium()
|
||||
{
|
||||
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();
|
||||
|
||||
$application = new Application($kernel);
|
||||
|
@ -1,7 +0,0 @@
|
||||
imports:
|
||||
- { resource: config.yml }
|
||||
|
||||
security:
|
||||
encoders:
|
||||
Custom\Class\Argon2id\User:
|
||||
algorithm: argon2id
|
@ -0,0 +1,7 @@
|
||||
imports:
|
||||
- { resource: config.yml }
|
||||
|
||||
security:
|
||||
encoders:
|
||||
Custom\Class\Sodium\User:
|
||||
algorithm: sodium
|
@ -19,9 +19,7 @@ CHANGELOG
|
||||
* Dispatch `AuthenticationFailureEvent` on `security.authentication.failure`
|
||||
* Dispatch `InteractiveLoginEvent` on `security.interactive_login`
|
||||
* Dispatch `SwitchUserEvent` on `security.switch_user`
|
||||
* Added `Argon2idPasswordEncoder`
|
||||
* Deprecated using `Argon2iPasswordEncoder` while only the `argon2id` algorithm
|
||||
is supported, use `Argon2idPasswordEncoder` instead
|
||||
* deprecated `Argon2iPasswordEncoder`, use `SodiumPasswordEncoder` instead
|
||||
|
||||
4.2.0
|
||||
-----
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -11,6 +11,8 @@
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
@ -18,10 +20,30 @@ use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
*
|
||||
* @author Zan Baldwin <hello@zanbaldwin.com>
|
||||
* @author Dominik Müller <dominik.mueller@jkweb.ch>
|
||||
*
|
||||
* @deprecated since Symfony 4.3, use SodiumPasswordEncoder instead
|
||||
*/
|
||||
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()
|
||||
{
|
||||
@ -46,12 +68,9 @@ class Argon2iPasswordEncoder extends BasePasswordEncoder implements SelfSaltingE
|
||||
}
|
||||
|
||||
if (\PHP_VERSION_ID >= 70200 && \defined('PASSWORD_ARGON2I')) {
|
||||
return $this->encodePasswordNative($raw, \PASSWORD_ARGON2I);
|
||||
} elseif (\function_exists('sodium_crypto_pwhash_str')) {
|
||||
if (Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) {
|
||||
@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->encodePasswordNative($raw);
|
||||
}
|
||||
|
||||
if (\function_exists('sodium_crypto_pwhash_str')) {
|
||||
return $this->encodePasswordSodiumFunction($raw);
|
||||
}
|
||||
if (\extension_loaded('libsodium')) {
|
||||
@ -66,20 +85,10 @@ class Argon2iPasswordEncoder extends BasePasswordEncoder implements SelfSaltingE
|
||||
*/
|
||||
public function isPasswordValid($encoded, $raw, $salt)
|
||||
{
|
||||
if ($this->isPasswordTooLong($raw)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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 $encoded was created via "sodium_crypto_pwhash_str()", the hashing algorithm may be "argon2id" instead of "argon2i".
|
||||
// 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 (\function_exists('sodium_crypto_pwhash_str_verify')) {
|
||||
$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+.');
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
$hash = \Sodium\crypto_pwhash_str(
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -108,6 +108,13 @@ class EncoderFactory implements EncoderFactoryInterface
|
||||
'arguments' => [$config['cost']],
|
||||
];
|
||||
|
||||
case 'sodium':
|
||||
return [
|
||||
'class' => SodiumPasswordEncoder::class,
|
||||
'arguments' => [],
|
||||
];
|
||||
|
||||
/* @deprecated since Symfony 4.3 */
|
||||
case 'argon2i':
|
||||
return [
|
||||
'class' => Argon2iPasswordEncoder::class,
|
||||
@ -117,15 +124,6 @@ class EncoderFactory implements EncoderFactoryInterface
|
||||
$config['threads'],
|
||||
],
|
||||
];
|
||||
case 'argon2id':
|
||||
return [
|
||||
'class' => Argon2idPasswordEncoder::class,
|
||||
'arguments' => [
|
||||
$config['memory_cost'],
|
||||
$config['time_cost'],
|
||||
$config['threads'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
|
@ -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.');
|
||||
}
|
||||
}
|
@ -12,22 +12,26 @@
|
||||
namespace Symfony\Component\Security\Core\Tests\Encoder;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\Encoder\Argon2idPasswordEncoder;
|
||||
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
|
||||
|
||||
/**
|
||||
* @author Zan Baldwin <hello@zanbaldwin.com>
|
||||
*
|
||||
* @group legacy
|
||||
*/
|
||||
class Argon2iPasswordEncoderTest extends TestCase
|
||||
{
|
||||
const PASSWORD = 'password';
|
||||
|
||||
public function testValidationWithConfig()
|
||||
protected function setUp()
|
||||
{
|
||||
if (!Argon2iPasswordEncoder::isSupported() || Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) {
|
||||
if (!Argon2iPasswordEncoder::isSupported()) {
|
||||
$this->markTestSkipped('Argon2i algorithm is not supported.');
|
||||
}
|
||||
}
|
||||
|
||||
public function testValidationWithConfig()
|
||||
{
|
||||
$encoder = new Argon2iPasswordEncoder(8, 4, 1);
|
||||
$result = $encoder->encodePassword(self::PASSWORD, null);
|
||||
$this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, null));
|
||||
@ -36,10 +40,6 @@ class Argon2iPasswordEncoderTest extends TestCase
|
||||
|
||||
public function testValidation()
|
||||
{
|
||||
if (!Argon2iPasswordEncoder::isSupported() || Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) {
|
||||
$this->markTestSkipped('Argon2i algorithm is not supported.');
|
||||
}
|
||||
|
||||
$encoder = new Argon2iPasswordEncoder();
|
||||
$result = $encoder->encodePassword(self::PASSWORD, null);
|
||||
$this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, null));
|
||||
@ -51,20 +51,12 @@ class Argon2iPasswordEncoderTest extends TestCase
|
||||
*/
|
||||
public function testEncodePasswordLength()
|
||||
{
|
||||
if (!Argon2iPasswordEncoder::isSupported() || Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) {
|
||||
$this->markTestSkipped('Argon2i algorithm is not supported.');
|
||||
}
|
||||
|
||||
$encoder = new Argon2iPasswordEncoder();
|
||||
$encoder->encodePassword(str_repeat('a', 4097), 'salt');
|
||||
}
|
||||
|
||||
public function testCheckPasswordLength()
|
||||
{
|
||||
if (!Argon2iPasswordEncoder::isSupported() || Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) {
|
||||
$this->markTestSkipped('Argon2i algorithm is not supported.');
|
||||
}
|
||||
|
||||
$encoder = new Argon2iPasswordEncoder();
|
||||
$result = $encoder->encodePassword(str_repeat('a', 4096), null);
|
||||
$this->assertFalse($encoder->isPasswordValid($result, str_repeat('a', 4097), null));
|
||||
@ -73,29 +65,8 @@ class Argon2iPasswordEncoderTest extends TestCase
|
||||
|
||||
public function testUserProvidedSaltIsNotUsed()
|
||||
{
|
||||
if (!Argon2iPasswordEncoder::isSupported() || Argon2idPasswordEncoder::isDefaultSodiumAlgorithm()) {
|
||||
$this->markTestSkipped('Argon2i algorithm is not supported.');
|
||||
}
|
||||
|
||||
$encoder = new Argon2iPasswordEncoder();
|
||||
$result = $encoder->encodePassword(self::PASSWORD, 'salt');
|
||||
$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));
|
||||
}
|
||||
}
|
||||
|
@ -12,28 +12,20 @@
|
||||
namespace Symfony\Component\Security\Core\Tests\Encoder;
|
||||
|
||||
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()
|
||||
{
|
||||
if (!Argon2idPasswordEncoder::isSupported()) {
|
||||
$this->markTestSkipped('Argon2i algorithm is not supported.');
|
||||
if (!SodiumPasswordEncoder::isSupported()) {
|
||||
$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()
|
||||
{
|
||||
$encoder = new Argon2idPasswordEncoder();
|
||||
$encoder = new SodiumPasswordEncoder();
|
||||
$result = $encoder->encodePassword('password', null);
|
||||
$this->assertTrue($encoder->isPasswordValid($result, 'password', null));
|
||||
$this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword', null));
|
||||
@ -44,13 +36,13 @@ class Argon2idPasswordEncoderTest extends TestCase
|
||||
*/
|
||||
public function testEncodePasswordLength()
|
||||
{
|
||||
$encoder = new Argon2idPasswordEncoder();
|
||||
$encoder = new SodiumPasswordEncoder();
|
||||
$encoder->encodePassword(str_repeat('a', 4097), 'salt');
|
||||
}
|
||||
|
||||
public function testCheckPasswordLength()
|
||||
{
|
||||
$encoder = new Argon2idPasswordEncoder();
|
||||
$encoder = new SodiumPasswordEncoder();
|
||||
$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));
|
||||
@ -58,7 +50,7 @@ class Argon2idPasswordEncoderTest extends TestCase
|
||||
|
||||
public function testUserProvidedSaltIsNotUsed()
|
||||
{
|
||||
$encoder = new Argon2idPasswordEncoder();
|
||||
$encoder = new SodiumPasswordEncoder();
|
||||
$result = $encoder->encodePassword('password', 'salt');
|
||||
$this->assertTrue($encoder->isPasswordValid($result, 'password', 'anotherSalt'));
|
||||
}
|
Reference in New Issue
Block a user