[Security] Add NativePasswordEncoder
This commit is contained in:
parent
278a7ece35
commit
28f7961c55
|
@ -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
|
||||||
----------
|
----------
|
||||||
|
|
|
@ -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
|
||||||
-----
|
-----
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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' => [
|
||||||
|
|
|
@ -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,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
-----
|
-----
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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'));
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue