8000 [Security] Add NativePasswordEncoder · symfony/symfony@40f1daf · GitHub
[go: up one dir, main page]

Skip to content

Commit 40f1daf

Browse files
[Security] Add NativePasswordEncoder
1 parent 278a7ec commit 40f1daf

File tree

10 files changed

+241
-29
lines changed

10 files changed

+241
-29
lines changed

src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -416,11 +416,14 @@ private function addEncodersSection(ArrayNodeDefinition $rootNode)
416416
->integerNode('cost')
417417
->min(4)
418418
->max(31)
419-
->defaultValue(13)
419+
->defaultNull()
420420
->end()
421421
->scalarNode('memory_cost')->defaultNull()->end()
422422
->scalarNode('time_cost')->defaultNull()->end()
423-
->scalarNode('threads')->defaultNull()->end()
423+
->scalarNode('threads')
424+
->defaultNull()
425+
->setDeprecated('The "%path%.%node%" configuration key has not effect since Symfony 4.3 and will be removed in 5.0.')
426+
->end()
424427
->scalarNode('id')->end()
425428
->end()
426429
->end()

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
3131
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
3232
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
33+
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
3334
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
3435
use Symfony\Component\Security\Core\User\UserProviderInterface;
3536
use Symfony\Component\Security\Http\Controller\UserValueResolver;
@@ -532,6 +533,10 @@ private function createEncoder($config, ContainerBuilder $container)
532533
return new Reference($config['id']);
533534
}
534535

536+
if ('auto' === $config['algorithm']) {
537+
$config['algorithm'] = SodiumPasswordEncoder::isSupported() ? 'sodium' : 'native';
538+
}
539+
535540
// plaintext encoder
536541
if ('plaintext' === $config['algorithm']) {
537542
$arguments = [$config['ignore_case']];
@@ -559,7 +564,7 @@ private function createEncoder($config, ContainerBuilder $container)
559564
if ('bcrypt' === $config['algorithm']) {
560565
return [
561566
'class' => 'Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder',
562-
'arguments' => [$config['cost']],
567+
'arguments' => [$config['cost'] ?? 13],
563568
];
564569
}
565570

@@ -585,14 +590,28 @@ private function createEncoder($config, ContainerBuilder $container)
585590
];
586591
}
587592

593+
if ('native' === $config['algorithm']) {
594+
return [
595+
'class' => NativePasswordEncoder::class,
596+
'arguments' => [
597+
$config['time_cost'],
598+
(($config['memory_cost'] ?? 0) << 10) ?: null,
599+
$config['cost'],
600+
],
601+
];
602+
}
603+
588604
if ('sodium' === $config['algorithm']) {
589605
if (!SodiumPasswordEncoder::isSupported()) {
590606
throw new InvalidConfigurationException('Libsodium is not available. Install the sodium extension or use BCrypt instead.');
591607
}
592608

593609
return [
594610
'class' => SodiumPasswordEncoder::class,
595-
'arguments' => [],
611+
'arguments' => [
612+
$config['time_cost'],
613+
(($config['memory_cost'] ?? 0) << 10) ?: null,
614+
],
596615
];
597616
}
598617

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ public function testEncoders()
283283
'hash_algorithm' => 'sha512',
284284
'key_length' => 40,
285285
'ignore_case' => false,
286-
'cost' => 13,
286+
'cost' => null,
287287
'memory_cost' => null,
288288
'time_cost' => null,
289289
'threads' => null,
@@ -295,7 +295,7 @@ public function testEncoders()
295295
'ignore_case' => false,
296296
'encode_as_base64' => true,
297297
'iterations' => 5000,
298-
'cost' => 13,
298+
'cost' => null,
299299
'memory_cost' => null,
300300
'time_cost' => null,
301301
'threads' => null,
@@ -332,7 +332,7 @@ public function testEncodersWithLibsodium()
332332
'hash_algorithm' => 'sha512',
333333
'key_length' => 40,
334334
'ignore_case' => false,
335-
'cost' => 13,
335+
'cost' => null,
336336
'memory_cost' => null,
337337
'time_cost' => null,
338338
'threads' => null,
@@ -344,7 +344,7 @@ public function testEncodersWithLibsodium()
344344
'ignore_case' => false,
345345
'encode_as_base64' => true,
346346
'iterations' => 5000,
347-
'cost' => 13,
347+
'cost' => null,
348348
'memory_cost' => null,
349349
'time_cost' => null,
350350
'threads' => null,
@@ -360,7 +360,7 @@ public function testEncodersWithLibsodium()
360360
],
361361
'JMS\FooBundle\Entity\User7' => [
362362
'class' => 'Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder',
363-
'arguments' => [],
363+
'arguments' => [8, 128 * 1024 * 1024],
364364
],
365365
]], $container->getDefinition('security.encoder_factory.generic')->getArguments());
366366
}
@@ -390,7 +390,7 @@ public function testEncodersWithArgon2i()
390390
'hash_algorithm' => 'sha512',
391391
'key_length' => 40,
392392
'ignore_case' => false,
393-
'cost' => 13,
393+
'cost' => null,
394394
'memory_cost' => null,
395395
'time_cost' => null,
396396
'threads' => null,
@@ -402,7 +402,7 @@ public function testEncodersWithArgon2i()
402402
'ignore_case' => false,
403403
'encode_as_base64' => true,
404404
'iterations' => 5000,
405-
'cost' => 13,
405+
'cost' => null,
406406
'memory_cost' => null,
407407
'time_cost' => null,
408408
'threads' => null,

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/sodium_encoder.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
'encoders' => [
77
'JMS\FooBundle\Entity\User7' => [
88
'algorithm' => 'sodium',
9+
'time_cost' => 8,
10+
'memory_cost' => 128 * 1024,
911
],
1012
],
1113
]);

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/sodium_encoder.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
</imports>
1111

1212
<sec:config>
13-
<sec:encoder class="JMS\FooBundle\Entity\User7" algorithm="sodium" />
13+
<sec:encoder class="JMS\FooBundle\Entity\User7" algorithm="sodium" time-cost="8" memory-cost="131072" />
1414
</sec:config>
1515

1616
</container>

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/sodium_encoder.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ security:
55
encoders:
66
JMS\FooBundle\Entity\User7:
77
algorithm: sodium
8+
time_cost: 8
9+
memory_cost: 131072

src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,15 @@ private function createEncoder(array $config)
7777
throw new \InvalidArgumentException(sprintf('"arguments" must be set in %s.', json_encode($config)));
7878
}
7979

80-
$reflection = new \ReflectionClass($config['class']);
81-
82-
return $reflection->newInstanceArgs($config['arguments']);
80+
return new $config['class'](...$config['arguments']);
8381
}
8482

8583
private function getEncoderConfigFromAlgorithm($config)
8684
{
85+
if ('auto' === $config['algorithm']) {
86+
$config['algorithm'] = SodiumPasswordEncoder::isSupported() ? 'sodium' : 'native';
87+
}
88+
8789
switch ($config['algorithm']) {
8890
case 'plaintext':
8991
return [
@@ -108,10 +110,23 @@ private function getEncoderConfigFromAlgorithm($config)
108110
'arguments' => [$config['cost']],
109111
];
110112

113+
case 'native':
114+
return [
115+
'class' => NativePasswordEncoder::class,
116+
'arguments' => [
117+
$config['time_cost'] ?? null,
118+
(($config['memory_cost'] ?? 0) << 10) ?: null,
119+
$config['cost'] ?? null,
120+
],
121+
];
122+
111123
case 'sodium':
112124
return [
113125
'class' => SodiumPasswordEncoder::class,
114-
'arguments' => [],
126+
'arguments' => [
127+
$config['time_cost'] ?? null,
128+
(($config['memory_cost'] ?? 0) << 10) ?: null,
129+
],
115130
];
116131

117132
/* @deprecated since Symfony 4.3 */
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Encoder;
13+
14+
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
15+
16+
/**
17+
* Hashes passwords using password_hash().
18+
*
19+
* @author Elnur Abdurrakhimov <elnur@elnur.pro>
20+
* @author Terje Bråten <terje@braten.be>
21+
* @author Nicolas Grekas <p@tchwork.com>
22+
*/
23+
final class NativePasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface
24+
{
25+
private $algo;
26+
private $options;
27+
28+
public function __construct(int $opsLimit = null, int $memLimit = null, int $cost = null)
29+
{
30+
$cost = $cost ?? 13;
31+
$opsLimit = $opsLimit ?? max(6, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE : 6);
32+
$memLimit = $memLimit ?? max(64 * 1024 * 1024, \defined('SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE : 64 * 1024 * 1024);
33+
34+
if (2 > $opsLimit) {
35+
throw new \InvalidArgumentException('$opsLimit must be 2 or greater.');
36+
}
37+
38+
if (10 * 1024 > $memLimit) {
39+
throw new \InvalidArgumentException('$memLimit must be 10k or greater.');
40+
}
41+
42+
if ($cost < 4 || 31 < $cost) {
43+
throw new \InvalidArgumentException('$cost must be in the range of 4-31.');
44+
}
45+
46+
$this->algo = \defined('PASSWORD_ARGON2I') ? max(PASSWORD_DEFAULT, \defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_ARGON2I) : PASSWORD_DEFAULT;
47+
$this->options = [
48+
'cost' => $cost,
49+
'time_cost' => $opsLimit,
50+
'memory_cost' => $memLimit >> 10,
51+
'threads' => 1,
52+
];
53+
}
54+
55+
/**
56+
* {@inheritdoc}
57+
*/
58+
public function encodePassword($raw, $salt)
59+
{
60+
if ($this->isPasswordTooLong($raw)) {
61+
throw new BadCredentialsException('Invalid password.');
62+
}
63+
64+
if ($salt) {
65+
// Ignore $salt, the auto-generated one is always the best
66+
}
67+
68+
$encoded = password_hash($raw, $this->algo, $this->options);
69+
70+
if (72 < \strlen($raw) && '2' === $encoded[1]) {
71+
// BCrypt encodes only the first 72 chars
72+
throw new BadCredentialsException('Invalid password.');
73+
}
74+
75+
return $encoded;
76+
}
77+
78+
/**
79+
* {@inheritdoc}
80+
*/
81+
public function isPasswordValid($encoded, $raw, $salt)
82+
{
83+
if (72 < \strlen($raw) && 0 === strpos($encoded, '$2')) {
84+
// BCrypt encodes only the first 72 chars
85+
return false;
86+
}
87+
88+
return !$this->isPasswordTooLong($raw) && password_verify($raw, $encoded);
89+
}
90+
}

src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,30 @@
2020
* @author Robin Chalas <robin.chalas@gmail.com>
2121
* @author Zan Baldwin <hello@zanbaldwin.com>
2222
* @author Dominik Müller <dominik.mueller@jkweb.ch>
23-
*
24-
* @final
2523
*/
26-
class SodiumPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface
24+
final class SodiumPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface
2725
{
26+
private $opsLimit;
27+
private $memLimit;
28+
29+
public function __construct(int $opsLimit = null, int $memLimit = null)
30+
{
31+
if (!self::isSupported()) {
32+
throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.');
33+
}
34+
35+
$this->opsLimit = $opsLimit ?? max(6, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE : 6);
36+
$this->memLimit = $memLimit ?? max(64 * 1024 * 1024, \defined('SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE : 64 * 1024 * 2014);
37+
38+
if (2 > $this->opsLimit) {
39+
throw new \InvalidArgumentException('$opsLimit must be 2 or greater.');
40+
}
41+
42+
if (10 * 1024 > $this->memLimit) {
43+
throw new \InvalidArgumentException('$memLimit must be 10k or greater.');
44+
}
45+
}
46+
2847
public static function isSupported(): bool
2948
{
3049
if (\class_exists('ParagonIE_Sodium_Compat') && \method_exists('ParagonIE_Sodium_Compat', 'crypto_pwhash_is_available')) {
@@ -44,19 +63,11 @@ public function encodePassword($raw, $salt)
4463
}
4564

4665
if (\function_exists('sodium_crypto_pwhash_str')) {
47-
return \sodium_crypto_pwhash_str(
48-
$raw,
49-
\SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
50-
\SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
51-
);
66+
return \sodium_crypto_pwhash_str($raw, $this->opsLimit, $this->memLimit);
5267
}
5368

5469
if (\extension_loaded('libsodium')) {
55-
return \Sodium\crypto_pwhash_str(
56-
$raw,
57-
\Sodium\CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
58-
\Sodium\CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
59-
);
70+
return \Sodium\crypto_pwhash_str($raw, $this->opsLimit, $this->memLimit);
6071
}
6172

6273
throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.');

0 commit comments

Comments
 (0)
0