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

Skip to content

Commit 1c79f90

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

File tree

10 files changed

+243
-31
lines changed

10 files changed

+243
-31
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 no 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: 17 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;
@@ -559,7 +560,7 @@ private function createEncoder($config, ContainerBuilder $container)
559560
if ('bcrypt' === $config['algorithm']) {
560561
return [
561562
'class' => 'Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder',
562-
'arguments' => [$config['cost']],
563+
'arguments' => [$config['cost'] ?? 13],
563564
];
564565
}
565566

@@ -585,14 +586,28 @@ private function createEncoder($config, ContainerBuilder $container)
585586
];
586587
}
587588

589+
if ('native' === $config['algorithm']) {
590+
return [
591+
'class' => NativePasswordEncoder::class,
592+
'arguments' => [
593+
$config['time_cost'],
594+
(($config['memory_cost'] ?? 0) << 10) ?: null,
595+
$config['cost'],
596+
],
597+
];
598+
}
599+
588600
if ('sodium' === $config['algorithm']) {
589601
if (!SodiumPasswordEncoder::isSupported()) {
590602
throw new InvalidConfigurationException('Libsodium is not available. Install the sodium extension or use BCrypt instead.');
591603
}
592604

593605
return [
594606
'class' => SodiumPasswordEncoder::class,
595-
'arguments' => [],
607+
'arguments' => [
608+
$config['time_cost'],
609+
(($config['memory_cost'] ?? 0) << 10) ?: null,
610+
],
596611
];
597612
}
598613

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-
E377 '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: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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 implements PasswordEncoderInterface, SelfSaltingEncoderInterface
24+
{
25+
private const MAX_PASSWORD_LENGTH = 4096;
26+
27+
private $algo;
28+
private $options;
29+
30+
public function __construct(int $opsLimit = null, int $memLimit = null, int $cost = null)
31+
{
32+
$cost = $cost ?? 13;
33+
$opsLimit = $opsLimit ?? max(6, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE : 6);
34+
$memLimit = $memLimit ?? max(64 * 1024 * 1024, \defined('SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE : 64 * 1024 * 1024);
35+
36+
if (2 > $opsLimit) {
37+
throw new \InvalidArgumentException('$opsLimit must be 2 or greater.');
38+
}
39+
40+
if (10 * 1024 > $memLimit) {
41+
throw new \InvalidArgumentException('$memLimit must be 10k or greater.');
42+
}
43+
44+
if ($cost < 4 || 31 < $cost) {
45+
throw new \InvalidArgumentException('$cost must be in the range of 4-31.');
46+
}
47+
48+
$this->algo = \defined('PASSWORD_ARGON2I') ? max(PASSWORD_DEFAULT, \defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_ARGON2I) : PASSWORD_DEFAULT;
49+
$this->options = [
50+
'cost' => $cost,
51+
'time_cost' => $opsLimit,
52+
'memory_cost' => $memLimit >> 10,
53+
'threads' => 1,
54+
];
55+
}
56+
57+
/**
58+
* {@inheritdoc}
59+
*/
60+
public function encodePassword($raw, $salt)
61+
{
62+
if (\strlen($raw) > self::MAX_PASSWORD_LENGTH) {
63+
throw new BadCredentialsException('Invalid password.');
64+
}
65+
66+
if ($salt) {
67+
// Ignore $salt, the auto-generated one is always the best
68+
}
69+
70+
$encoded = password_hash($raw, $this->algo, $this->options);
71+
72+
if (72 < \strlen($raw) && 0 === strpos($encoded, '$2')) {
73+
// BCrypt encodes only the first 72 chars
74+
throw new BadCredentialsException('Invalid password.');
75+
}
76+
77+
return $encoded;
78+
}
79+
80+
/**
81+
* {@inheritdoc}
82+
*/
83+
public function isPasswordValid($encoded, $raw, $salt)
84+
{
85+
if (72 < \strlen($raw) && 0 === strpos($encoded, '$2')) {
86+
// BCrypt encodes only the first 72 chars
87+
return false;
88+
}
89+
90+
return \strlen($raw) <= self::MAX_PASSWORD_LENGTH && password_verify($raw, $encoded);
91+
}
92+
}

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

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,32 @@
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 implements PasswordEncoderInterface, SelfSaltingEncoderInterface
2725
{
26+
private const MAX_PASSWORD_LENGTH = 4096;
27+
28+
private $opsLimit;
29+
private $memLimit;
30+
31+
public function __construct(int $opsLimit = null, int $memLimit = null)
32+
{
33+
if (!self::isSupported()) {
34+
throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.');
35+
}
36+
37+
$this->opsLimit = $opsLimit ?? max(6, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE : 6);
38+
$this->memLimit = $memLimit ?? max(64 * 1024 * 1024, \defined('SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE : 64 * 1024 * 2014);
39+
40+
if (2 > $this->opsLimit) {
41+
throw new \InvalidArgumentException('$opsLimit must be 2 or greater.');
42+
}
43+
44+
if (10 * 1024 > $this->memLimit) {
45+
throw new \InvalidArgumentException('$memLimit must be 10k or greater.');
46+
}
47+
}
48+
2849
public static function isSupported(): bool
2950
{
3051
if (\class_exists('ParagonIE_Sodium_Compat') && \method_exists('ParagonIE_Sodium_Compat', 'crypto_pwhash_is_available')) {
@@ -39,24 +60,16 @@ public static function isSupported(): bool
3960
*/
4061
public function encodePassword($raw, $salt)
4162
{
42-
if ($this->isPasswordTooLong($raw)) {
63+
if (\strlen($raw) > self::MAX_PASSWORD_LENGTH) {
4364
throw new BadCredentialsException('Invalid password.');
4465
}
4566

4667
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-
);
68+
return \sodium_crypto_pwhash_str($raw, $this->opsLimit, $this->memLimit);
5269
}
5370

5471
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-
);
72+
return \Sodium\crypto_pwhash_str($raw, $this->opsLimit, $this->memLimit);
6073
}
6174

6275
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 @@ public function encodePassword($raw, $salt)
6780
*/
6881
public function isPasswordValid($encoded, $raw, $salt)
6982
{
70-
if ($this->isPasswordTooLong($raw)) {
83+
if (\strlen($raw) > self::MAX_PASSWORD_LENGTH) {
7184
return false;
7285
}
7386

0 commit comments

Comments
 (0)
0