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

Skip to content

Commit 9c1b4ce

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

File tree

13 files changed

+256
-39
lines changed

13 files changed

+256
-39
lines changed

UPGRADE-4.3.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ Security
174174
SecurityBundle
175175
--------------
176176

177-
* Configuring encoders using `argon2i` as algorithm has been deprecated, use `sodium` instead.
177+
* Configuring encoders using `argon2i` as algorithm has been deprecated, use `auto` instead.
178178

179179
TwigBridge
180180
----------

src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ CHANGELOG
44
4.3.0
55
-----
66

7+
* Added new encoder types: `auto` (recommended), `native` and `sodium`
78
* The normalization of the cookie names configured in the `logout.delete_cookies`
89
option is deprecated and will be disabled in Symfony 5.0. This affects to cookies
910
with dashes in their names. For example, starting from Symfony 5.0, the `my-cookie`
1011
name will delete `my-cookie` (with a dash) instead of `my_cookie` (with an underscore).
11-
* Deprecated configuring encoders using `argon2i` as algorithm, use `sodium` instead
12+
* Deprecated configuring encoders using `argon2i` as algorithm, use `auto` instead
1213

1314
4.2.0
1415
-----

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -394,9 +394,10 @@ private function addEncodersSection(ArrayNodeDefinition $rootNode)
394394
->children()
395395
->arrayNode('encoders')
396396
->example([
397-
'App\Entity\User1' => 'bcrypt',
397+
'App\Entity\User1' => 'native',
398398
'App\Entity\User2' => [
399-
'algorithm' => 'bcrypt',
399+
'algorithm' => 'native',
400+
'time_cost' => 8,
400401
'cost' => 13,
401402
],
402403
])
@@ -416,11 +417,14 @@ private function addEncodersSection(ArrayNodeDefinition $rootNode)
416417
->integerNode('cost')
417418
->min(4)
418419
->max(31)
419-
->defaultValue(13)
420+
->defaultNull()
420421
->end()
421422
->scalarNode('memory_cost')->defaultNull()->end()
422423
->scalarNode('time_cost')->defaultNull()->end()
423-
->scalarNode('threads')->defaultNull()->end()
424+
->scalarNode('threads')
425+
->defaultNull()
426+
->setDeprecated('The "%path%.%node%" configuration key has no effect since Symfony 4.3 and will be removed in 5.0.')
427+
->end()
424428
->scalarNode('id')->end()
425429
->end()
426430
->end()

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

Lines changed: 21 additions & 6 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,20 +560,20 @@ 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

566567
// Argon2i encoder
567568
if ('argon2i' === $config['algorithm']) {
568-
@trigger_error('Configuring an encoder with "argon2i" as algorithm is deprecated since Symfony 4.3, use "sodium" instead.', E_USER_DEPRECATED);
569+
@trigger_error('Configuring an encoder with "argon2i" as algorithm is deprecated since Symfony 4.3, use "auto" instead.', E_USER_DEPRECATED);
569570

570571
if (!Argon2iPasswordEncoder::isSupported()) {
571572
if (\extension_loaded('sodium') && !\defined('SODIUM_CRYPTO_PWHASH_SALTBYTES')) {
572-
throw new InvalidConfigurationException('The installed libsodium version does not have support for Argon2i. Use Bcrypt instead.');
573+
throw new InvalidConfigurationException('The installed libsodium version does not have support for Argon2i. Use "auto" instead.');
573574
}
574575

575-
throw new InvalidConfigurationException('Argon2i algorithm is not supported. Install the libsodium extension or use BCrypt instead.');
576+
throw new InvalidConfigurationException('Argon2i algorithm is not supported. Install the libsodium extension or use "auto" instead.');
576577
}
577578

578579
return [
@@ -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()) {
590-
throw new InvalidConfigurationException('Libsodium is not available. Install the sodium extension or use BCrypt instead.');
602+
throw new InvalidConfigurationException('Libsodium is not available. Install the sodium extension or use "auto" 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: 8 additions & 8 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,15 +360,15 @@ 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
}
367367

368368
/**
369369
* @group legacy
370370
*
371-
* @expectedDeprecation Configuring an encoder with "argon2i" as algorithm is deprecated since Symfony 4.3, use "sodium" instead.
371+
* @expectedDeprecation Configuring an encoder with "argon2i" as algorithm is deprecated since Symfony 4.3, use "auto" instead.
372372
*/
373373
public function testEncodersWithArgon2i()
374374
{
@@ -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/CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ CHANGELOG
44
4.3.0
55
-----
66

7+
* Added methods `__serialize` and `__unserialize` to the `TokenInterface`
8+
* Added `SodiumPasswordEncoder` and `NativePasswordEncoder`
79
* The `Role` and `SwitchUserRole` classes are deprecated and will be removed in 5.0. Use strings for roles
810
instead.
911
* The `getReachableRoles()` method of the `RoleHierarchyInterface` is deprecated and will be removed in 5.0.
@@ -19,8 +21,7 @@ CHANGELOG
1921
* Dispatch `AuthenticationFailureEvent` on `security.authentication.failure`
2022
* Dispatch `InteractiveLoginEvent` on `security.interactive_login`
2123
* Dispatch `SwitchUserEvent` on `security.switch_user`
22-
* deprecated `Argon2iPasswordEncoder`, use `SodiumPasswordEncoder` instead
23-
* Added methods `__serialize` and `__unserialize` to the `TokenInterface`
24+
* Deprecated `Argon2iPasswordEncoder`, use `SodiumPasswordEncoder`
2425

2526
4.2.0
2627
-----

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ private function createEncoder(array $config)
8484

8585
private function getEncoderConfigFromAlgorithm($config)
8686
{
87+
if ('auto' === $config['algorithm']) {
88+
$config['algorithm'] = SodiumPasswordEncoder::isSupported() ? 'sodium' : 'native';
89+
}
90+
8791
switch ($config['algorithm']) {
8892
case 'plaintext':
8993
return [
@@ -108,10 +112,23 @@ private function getEncoderConfigFromAlgorithm($config)
108112
'arguments' => [$config['cost']],
109113
];
110114

115+
case 'native':
116+
return [
117+
'class' => NativePasswordEncoder::class,
118+
'arguments' => [
119+
$config['time_cost'] ?? null,
120+
(($config['memory_cost'] ?? 0) << 10) ?: null,
121+
$config['cost'] ?? null,
122+
],
123+
];
124+
111125
case 'sodium':
112126
return [
113127
'class' => SodiumPasswordEncoder::class,
114-
'arguments' => [],
128+
'arguments' => [
129+
$config['time_cost'] ?? null,
130+
(($config['memory_cost'] ?? 0) << 10) ?: null,
131+
],
115132
];
116133

117134
/* @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+
}

0 commit comments

Comments
 (0)
0