[Security] Add NativePasswordEncoder

This commit is contained in:
Nicolas Grekas 2019-04-17 11:28:15 +02:00
parent 278a7ece35
commit 28f7961c55
16 changed files with 314 additions and 39 deletions

View File

@ -174,7 +174,7 @@ Security
SecurityBundle SecurityBundle
-------------- --------------
* Configuring encoders using `argon2i` as algorithm has been deprecated, use `sodium` instead. * Configuring encoders using `argon2i` as algorithm has been deprecated, use `auto` instead.
TwigBridge TwigBridge
---------- ----------

View File

@ -4,11 +4,12 @@ CHANGELOG
4.3.0 4.3.0
----- -----
* Added new encoder types: `auto` (recommended), `native` and `sodium`
* The normalization of the cookie names configured in the `logout.delete_cookies` * The normalization of the cookie names configured in the `logout.delete_cookies`
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, use `sodium` instead * Deprecated configuring encoders using `argon2i` as algorithm, use `auto` instead
4.2.0 4.2.0
----- -----

View File

@ -394,9 +394,10 @@ class MainConfiguration implements ConfigurationInterface
->children() ->children()
->arrayNode('encoders') ->arrayNode('encoders')
->example([ ->example([
'App\Entity\User1' => 'bcrypt', 'App\Entity\User1' => 'auto',
'App\Entity\User2' => [ 'App\Entity\User2' => [
'algorithm' => 'bcrypt', 'algorithm' => 'auto',
'time_cost' => 8,
'cost' => 13, 'cost' => 13,
], ],
]) ])
@ -416,11 +417,14 @@ class MainConfiguration implements ConfigurationInterface
->integerNode('cost') ->integerNode('cost')
->min(4) ->min(4)
->max(31) ->max(31)
->defaultValue(13) ->defaultNull()
->end() ->end()
->scalarNode('memory_cost')->defaultNull()->end() ->scalarNode('memory_cost')->defaultNull()->end()
->scalarNode('time_cost')->defaultNull()->end() ->scalarNode('time_cost')->defaultNull()->end()
->scalarNode('threads')->defaultNull()->end() ->scalarNode('threads')
->defaultNull()
->setDeprecated('The "%path%.%node%" configuration key has no effect since Symfony 4.3 and will be removed in 5.0.')
->end()
->scalarNode('id')->end() ->scalarNode('id')->end()
->end() ->end()
->end() ->end()

View File

@ -30,6 +30,7 @@ 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\Argon2iPasswordEncoder; use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; 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;
@ -559,20 +560,20 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
if ('bcrypt' === $config['algorithm']) { if ('bcrypt' === $config['algorithm']) {
return [ return [
'class' => 'Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder', 'class' => 'Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder',
'arguments' => [$config['cost']], 'arguments' => [$config['cost'] ?? 13],
]; ];
} }
// 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); @trigger_error('Configuring an encoder with "argon2i" as algorithm is deprecated since Symfony 4.3, use "auto" 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 "auto" 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 "auto" instead.');
} }
return [ return [
@ -585,14 +586,28 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
]; ];
} }
if ('native' === $config['algorithm']) {
return [
'class' => NativePasswordEncoder::class,
'arguments' => [
$config['time_cost'],
(($config['memory_cost'] ?? 0) << 10) ?: null,
$config['cost'],
],
];
}
if ('sodium' === $config['algorithm']) { if ('sodium' === $config['algorithm']) {
if (!SodiumPasswordEncoder::isSupported()) { if (!SodiumPasswordEncoder::isSupported()) {
throw new InvalidConfigurationException('Libsodium is not available. Install the sodium extension or use BCrypt instead.'); throw new InvalidConfigurationException('Libsodium is not available. Install the sodium extension or use "auto" instead.');
} }
return [ return [
'class' => SodiumPasswordEncoder::class, 'class' => SodiumPasswordEncoder::class,
'arguments' => [], 'arguments' => [
$config['time_cost'],
(($config['memory_cost'] ?? 0) << 10) ?: null,
],
]; ];
} }

View File

@ -283,7 +283,7 @@ abstract class CompleteConfigurationTest extends TestCase
'hash_algorithm' => 'sha512', 'hash_algorithm' => 'sha512',
'key_length' => 40, 'key_length' => 40,
'ignore_case' => false, 'ignore_case' => false,
'cost' => 13, 'cost' => null,
'memory_cost' => null, 'memory_cost' => null,
'time_cost' => null, 'time_cost' => null,
'threads' => null, 'threads' => null,
@ -295,7 +295,7 @@ abstract class CompleteConfigurationTest extends TestCase
'ignore_case' => false, 'ignore_case' => false,
'encode_as_base64' => true, 'encode_as_base64' => true,
'iterations' => 5000, 'iterations' => 5000,
'cost' => 13, 'cost' => null,
'memory_cost' => null, 'memory_cost' => null,
'time_cost' => null, 'time_cost' => null,
'threads' => null, 'threads' => null,
@ -309,6 +309,22 @@ abstract class CompleteConfigurationTest extends TestCase
'class' => 'Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder', 'class' => 'Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder',
'arguments' => [15], 'arguments' => [15],
], ],
'JMS\FooBundle\Entity\User7' => [
'class' => 'Symfony\Component\Security\Core\Encoder\NativePasswordEncoder',
'arguments' => [8, 102400, 15],
],
'JMS\FooBundle\Entity\User8' => [
'algorithm' => 'auto',
'hash_algorithm' => 'sha512',
'key_length' => 40,
'ignore_case' => false,
'encode_as_base64' => true,
'iterations' => 5000,
'cost' => null,
'memory_cost' => null,
'time_cost' => null,
'threads' => null,
],
]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); ]], $container->getDefinition('security.encoder_factory.generic')->getArguments());
} }
@ -332,7 +348,7 @@ abstract class CompleteConfigurationTest extends TestCase
'hash_algorithm' => 'sha512', 'hash_algorithm' => 'sha512',
'key_length' => 40, 'key_length' => 40,
'ignore_case' => false, 'ignore_case' => false,
'cost' => 13, 'cost' => null,
'memory_cost' => null, 'memory_cost' => null,
'time_cost' => null, 'time_cost' => null,
'threads' => null, 'threads' => null,
@ -344,7 +360,7 @@ abstract class CompleteConfigurationTest extends TestCase
'ignore_case' => false, 'ignore_case' => false,
'encode_as_base64' => true, 'encode_as_base64' => true,
'iterations' => 5000, 'iterations' => 5000,
'cost' => 13, 'cost' => null,
'memory_cost' => null, 'memory_cost' => null,
'time_cost' => null, 'time_cost' => null,
'threads' => null, 'threads' => null,
@ -360,7 +376,19 @@ abstract class CompleteConfigurationTest extends TestCase
], ],
'JMS\FooBundle\Entity\User7' => [ 'JMS\FooBundle\Entity\User7' => [
'class' => 'Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder', 'class' => 'Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder',
'arguments' => [], 'arguments' => [8, 128 * 1024 * 1024],
],
'JMS\FooBundle\Entity\User8' => [
'algorithm' => 'auto',
'hash_algorithm' => 'sha512',
'key_length' => 40,
'ignore_case' => false,
'encode_as_base64' => true,
'iterations' => 5000,
'cost' => null,
'memory_cost' => null,
'time_cost' => null,
'threads' => null,
], ],
]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); ]], $container->getDefinition('security.encoder_factory.generic')->getArguments());
} }
@ -368,7 +396,7 @@ abstract class CompleteConfigurationTest extends TestCase
/** /**
* @group legacy * @group legacy
* *
* @expectedDeprecation Configuring an encoder with "argon2i" as algorithm is deprecated since Symfony 4.3, use "sodium" instead. * @expectedDeprecation Configuring an encoder with "argon2i" as algorithm is deprecated since Symfony 4.3, use "auto" instead.
*/ */
public function testEncodersWithArgon2i() public function testEncodersWithArgon2i()
{ {
@ -390,7 +418,7 @@ abstract class CompleteConfigurationTest extends TestCase
'hash_algorithm' => 'sha512', 'hash_algorithm' => 'sha512',
'key_length' => 40, 'key_length' => 40,
'ignore_case' => false, 'ignore_case' => false,
'cost' => 13, 'cost' => null,
'memory_cost' => null, 'memory_cost' => null,
'time_cost' => null, 'time_cost' => null,
'threads' => null, 'threads' => null,
@ -402,7 +430,7 @@ abstract class CompleteConfigurationTest extends TestCase
'ignore_case' => false, 'ignore_case' => false,
'encode_as_base64' => true, 'encode_as_base64' => true,
'iterations' => 5000, 'iterations' => 5000,
'cost' => 13, 'cost' => null,
'memory_cost' => null, 'memory_cost' => null,
'time_cost' => null, 'time_cost' => null,
'threads' => null, 'threads' => null,
@ -420,6 +448,18 @@ abstract class CompleteConfigurationTest extends TestCase
'class' => 'Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder', 'class' => 'Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder',
'arguments' => [256, 1, 2], 'arguments' => [256, 1, 2],
], ],
'JMS\FooBundle\Entity\User8' => [
'algorithm' => 'auto',
'hash_algorithm' => 'sha512',
'key_length' => 40,
'ignore_case' => false,
'encode_as_base64' => true,
'iterations' => 5000,
'cost' => null,
'memory_cost' => null,
'time_cost' => null,
'threads' => null,
],
]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); ]], $container->getDefinition('security.encoder_factory.generic')->getArguments());
} }

View File

@ -25,6 +25,15 @@ $container->loadFromExtension('security', [
'algorithm' => 'bcrypt', 'algorithm' => 'bcrypt',
'cost' => 15, 'cost' => 15,
], ],
'JMS\FooBundle\Entity\User7' => [
'algorithm' => 'native',
'time_cost' => 8,
'memory_cost' => 100,
'cost' => 15,
],
'JMS\FooBundle\Entity\User8' => [
'algorithm' => 'auto',
],
], ],
'providers' => [ 'providers' => [
'default' => [ 'default' => [

View File

@ -6,6 +6,8 @@ $container->loadFromExtension('security', [
'encoders' => [ 'encoders' => [
'JMS\FooBundle\Entity\User7' => [ 'JMS\FooBundle\Entity\User7' => [
'algorithm' => 'sodium', 'algorithm' => 'sodium',
'time_cost' => 8,
'memory_cost' => 128 * 1024,
], ],
], ],
]); ]);

View File

@ -18,6 +18,10 @@
<encoder class="JMS\FooBundle\Entity\User6" algorithm="bcrypt" cost="15" /> <encoder class="JMS\FooBundle\Entity\User6" algorithm="bcrypt" cost="15" />
<encoder class="JMS\FooBundle\Entity\User7" algorithm="native" time-cost="8" memory-cost="100" cost="15" />
<encoder class="JMS\FooBundle\Entity\User8" algorithm="auto" />
<provider name="default"> <provider name="default">
<memory> <memory>
<user name="foo" password="foo" roles="ROLE_USER" /> <user name="foo" password="foo" roles="ROLE_USER" />

View File

@ -10,7 +10,7 @@
</imports> </imports>
<sec:config> <sec:config>
<sec:encoder class="JMS\FooBundle\Entity\User7" algorithm="sodium" /> <sec:encoder class="JMS\FooBundle\Entity\User7" algorithm="sodium" time-cost="8" memory-cost="131072" />
</sec:config> </sec:config>
</container> </container>

View File

@ -18,6 +18,13 @@ security:
JMS\FooBundle\Entity\User6: JMS\FooBundle\Entity\User6:
algorithm: bcrypt algorithm: bcrypt
cost: 15 cost: 15
JMS\FooBundle\Entity\User7:
algorithm: native
time_cost: 8
memory_cost: 100
cost: 15
JMS\FooBundle\Entity\User8:
algorithm: auto
providers: providers:
default: default:

View File

@ -5,3 +5,5 @@ security:
encoders: encoders:
JMS\FooBundle\Entity\User7: JMS\FooBundle\Entity\User7:
algorithm: sodium algorithm: sodium
time_cost: 8
memory_cost: 131072

View File

@ -4,6 +4,8 @@ CHANGELOG
4.3.0 4.3.0
----- -----
* Added methods `__serialize` and `__unserialize` to the `TokenInterface`
* Added `SodiumPasswordEncoder` and `NativePasswordEncoder`
* The `Role` and `SwitchUserRole` classes are deprecated and will be removed in 5.0. Use strings for roles * The `Role` and `SwitchUserRole` classes are deprecated and will be removed in 5.0. Use strings for roles
instead. instead.
* The `getReachableRoles()` method of the `RoleHierarchyInterface` is deprecated and will be removed in 5.0. * The `getReachableRoles()` method of the `RoleHierarchyInterface` is deprecated and will be removed in 5.0.
@ -19,8 +21,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`
* deprecated `Argon2iPasswordEncoder`, use `SodiumPasswordEncoder` instead * Deprecated `Argon2iPasswordEncoder`, use `SodiumPasswordEncoder`
* Added methods `__serialize` and `__unserialize` to the `TokenInterface`
4.2.0 4.2.0
----- -----

View File

@ -84,6 +84,10 @@ class EncoderFactory implements EncoderFactoryInterface
private function getEncoderConfigFromAlgorithm($config) private function getEncoderConfigFromAlgorithm($config)
{ {
if ('auto' === $config['algorithm']) {
$config['algorithm'] = SodiumPasswordEncoder::isSupported() ? 'sodium' : 'native';
}
switch ($config['algorithm']) { switch ($config['algorithm']) {
case 'plaintext': case 'plaintext':
return [ return [
@ -108,10 +112,23 @@ class EncoderFactory implements EncoderFactoryInterface
'arguments' => [$config['cost']], 'arguments' => [$config['cost']],
]; ];
case 'native':
return [
'class' => NativePasswordEncoder::class,
'arguments' => [
$config['time_cost'] ?? null,
(($config['memory_cost'] ?? 0) << 10) ?: null,
$config['cost'] ?? null,
],
];
case 'sodium': case 'sodium':
return [ return [
'class' => SodiumPasswordEncoder::class, 'class' => SodiumPasswordEncoder::class,
'arguments' => [], 'arguments' => [
$config['time_cost'] ?? null,
(($config['memory_cost'] ?? 0) << 10) ?: null,
],
]; ];
/* @deprecated since Symfony 4.3 */ /* @deprecated since Symfony 4.3 */

View File

@ -0,0 +1,90 @@
<?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;
/**
* Hashes passwords using password_hash().
*
* @author Elnur Abdurrakhimov <elnur@elnur.pro>
* @author Terje Bråten <terje@braten.be>
* @author Nicolas Grekas <p@tchwork.com>
*/
final class NativePasswordEncoder implements PasswordEncoderInterface, SelfSaltingEncoderInterface
{
private const MAX_PASSWORD_LENGTH = 4096;
private $algo;
private $options;
public function __construct(int $opsLimit = null, int $memLimit = null, int $cost = null)
{
$cost = $cost ?? 13;
$opsLimit = $opsLimit ?? max(6, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE : 6);
$memLimit = $memLimit ?? max(64 * 1024 * 1024, \defined('SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE : 64 * 1024 * 1024);
if (2 > $opsLimit) {
throw new \InvalidArgumentException('$opsLimit must be 2 or greater.');
}
if (10 * 1024 > $memLimit) {
throw new \InvalidArgumentException('$memLimit must be 10k or greater.');
}
if ($cost < 4 || 31 < $cost) {
throw new \InvalidArgumentException('$cost must be in the range of 4-31.');
}
$this->algo = \defined('PASSWORD_ARGON2I') ? max(PASSWORD_DEFAULT, \defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_ARGON2I) : PASSWORD_DEFAULT;
$this->options = [
'cost' => $cost,
'time_cost' => $opsLimit,
'memory_cost' => $memLimit >> 10,
'threads' => 1,
];
}
/**
* {@inheritdoc}
*/
public function encodePassword($raw, $salt)
{
if (\strlen($raw) > self::MAX_PASSWORD_LENGTH) {
throw new BadCredentialsException('Invalid password.');
}
// Ignore $salt, the auto-generated one is always the best
$encoded = password_hash($raw, $this->algo, $this->options);
if (72 < \strlen($raw) && 0 === strpos($encoded, '$2')) {
// BCrypt encodes only the first 72 chars
throw new BadCredentialsException('Invalid password.');
}
return $encoded;
}
/**
* {@inheritdoc}
*/
public function isPasswordValid($encoded, $raw, $salt)
{
if (72 < \strlen($raw) && 0 === strpos($encoded, '$2')) {
// BCrypt encodes only the first 72 chars
return false;
}
return \strlen($raw) <= self::MAX_PASSWORD_LENGTH && password_verify($raw, $encoded);
}
}

View File

@ -20,11 +20,32 @@ use Symfony\Component\Security\Core\Exception\LogicException;
* @author Robin Chalas <robin.chalas@gmail.com> * @author Robin Chalas <robin.chalas@gmail.com>
* @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>
*
* @final
*/ */
class SodiumPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface final class SodiumPasswordEncoder implements PasswordEncoderInterface, SelfSaltingEncoderInterface
{ {
private const MAX_PASSWORD_LENGTH = 4096;
private $opsLimit;
private $memLimit;
public function __construct(int $opsLimit = null, int $memLimit = null)
{
if (!self::isSupported()) {
throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.');
}
$this->opsLimit = $opsLimit ?? max(6, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE : 6);
$this->memLimit = $memLimit ?? max(64 * 1024 * 1024, \defined('SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE : 64 * 1024 * 2014);
if (2 > $this->opsLimit) {
throw new \InvalidArgumentException('$opsLimit must be 2 or greater.');
}
if (10 * 1024 > $this->memLimit) {
throw new \InvalidArgumentException('$memLimit must be 10k or greater.');
}
}
public static function isSupported(): bool public static function isSupported(): bool
{ {
if (\class_exists('ParagonIE_Sodium_Compat') && \method_exists('ParagonIE_Sodium_Compat', 'crypto_pwhash_is_available')) { if (\class_exists('ParagonIE_Sodium_Compat') && \method_exists('ParagonIE_Sodium_Compat', 'crypto_pwhash_is_available')) {
@ -39,24 +60,16 @@ class SodiumPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEn
*/ */
public function encodePassword($raw, $salt) public function encodePassword($raw, $salt)
{ {
if ($this->isPasswordTooLong($raw)) { if (\strlen($raw) > self::MAX_PASSWORD_LENGTH) {
throw new BadCredentialsException('Invalid password.'); throw new BadCredentialsException('Invalid password.');
} }
if (\function_exists('sodium_crypto_pwhash_str')) { if (\function_exists('sodium_crypto_pwhash_str')) {
return \sodium_crypto_pwhash_str( return \sodium_crypto_pwhash_str($raw, $this->opsLimit, $this->memLimit);
$raw,
\SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
\SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
);
} }
if (\extension_loaded('libsodium')) { if (\extension_loaded('libsodium')) {
return \Sodium\crypto_pwhash_str( return \Sodium\crypto_pwhash_str($raw, $this->opsLimit, $this->memLimit);
$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.'); throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.');
@ -67,7 +80,7 @@ class SodiumPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEn
*/ */
public function isPasswordValid($encoded, $raw, $salt) public function isPasswordValid($encoded, $raw, $salt)
{ {
if ($this->isPasswordTooLong($raw)) { if (\strlen($raw) > self::MAX_PASSWORD_LENGTH) {
return false; return false;
} }

View File

@ -0,0 +1,70 @@
<?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\NativePasswordEncoder;
/**
* @author Elnur Abdurrakhimov <elnur@elnur.pro>
*/
class NativePasswordEncoderTest extends TestCase
{
/**
* @expectedException \InvalidArgumentException
*/
public function testCostBelowRange()
{
new NativePasswordEncoder(null, null, 3);
}
/**
* @expectedException \InvalidArgumentException
*/
public function testCostAboveRange()
{
new NativePasswordEncoder(null, null, 32);
}
/**
* @dataProvider validRangeData
*/
public function testCostInRange($cost)
{
$this->assertInstanceOf(NativePasswordEncoder::class, new NativePasswordEncoder(null, null, $cost));
}
public function validRangeData()
{
$costs = range(4, 31);
array_walk($costs, function (&$cost) { $cost = [$cost]; });
return $costs;
}
public function testValidation()
{
$encoder = new NativePasswordEncoder();
$result = $encoder->encodePassword('password', null);
$this->assertTrue($encoder->isPasswordValid($result, 'password', null));
$this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword', null));
}
public function testCheckPasswordLength()
{
$encoder = new NativePasswordEncoder(null, null, 4);
$result = password_hash(str_repeat('a', 72), PASSWORD_BCRYPT, ['cost' => 4]);
$this->assertFalse($encoder->isPasswordValid($result, str_repeat('a', 73), 'salt'));
$this->assertTrue($encoder->isPasswordValid($result, str_repeat('a', 72), 'salt'));
}
}