Skip to content

Commit d18baac

Browse files
committed
[Security] Allow to use a specific password hashing algorithm
1 parent 6253369 commit d18baac

File tree

11 files changed

+127
-63
lines changed

11 files changed

+127
-63
lines changed

UPGRADE-4.3.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,11 +209,6 @@ Security
209209
* Not implementing the methods `__serialize` and `__unserialize` in classes implementing
210210
the `TokenInterface` is deprecated
211211

212-
SecurityBundle
213-
--------------
214-
215-
* Configuring encoders using `argon2i` or `bcrypt` as algorithm has been deprecated, use `auto` instead.
216-
217212
TwigBridge
218213
----------
219214

UPGRADE-5.0.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,6 @@ SecurityBundle
537537
changed to underscores.
538538
Before: `my-cookie` deleted the `my_cookie` cookie (with an underscore).
539539
After: `my-cookie` deletes the `my-cookie` cookie (with a dash).
540-
* Configuring encoders using `argon2i` or `bcrypt` as algorithm is not supported anymore, use `auto` instead.
541540

542541
Serializer
543542
----------

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

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
use Symfony\Component\DependencyInjection\Reference;
2929
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
3030
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
31-
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
3231
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
3332
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
3433
use Symfony\Component\Security\Core\User\UserProviderInterface;
@@ -538,32 +537,72 @@ private function createEncoder(array $config)
538537

539538
// bcrypt encoder
540539
if ('bcrypt' === $config['algorithm']) {
541-
@trigger_error('Configuring an encoder with "bcrypt" as algorithm is deprecated since Symfony 4.3, use "auto" instead.', E_USER_DEPRECATED);
542-
543540
return [
544-
'class' => 'Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder',
545-
'arguments' => [$config['cost'] ?? 13],
541+
'class' => NativePasswordEncoder::class,
542+
'arguments' => [
543+
$config['time_cost'] ?? null,
544+
(($config['memory_cost'] ?? 0) << 10) ?: null,
545+
$config['cost'] ?? null,
546+
\PASSWORD_BCRYPT,
547+
],
546548
];
547549
}
548550

549551
// Argon2i encoder
550552
if ('argon2i' === $config['algorithm']) {
551-
@trigger_error('Configuring an encoder with "argon2i" as algorithm is deprecated since Symfony 4.3, use "auto" instead.', E_USER_DEPRECATED);
553+
if (SodiumPasswordEncoder::isSupported() && !($hasSodiumArgon2id = \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13'))) {
554+
return [
555+
'class' => SodiumPasswordEncoder::class,
556+
'arguments' => [
557+
$config['time_cost'] ?? null,
558+
(($config['memory_cost'] ?? 0) << 10) ?: null,
559+
],
560+
];
561+
}
552562

553-
if (!Argon2iPasswordEncoder::isSupported()) {
554-
if (\extension_loaded('sodium') && !\defined('SODIUM_CRYPTO_PWHASH_SALTBYTES')) {
555-
throw new InvalidConfigurationException('The installed libsodium version does not have support for Argon2i. Use "auto" instead.');
563+
if (!\defined('PASSWORD_ARGON2I')) {
564+
if ($hasSodiumArgon2id ?? false) {
565+
throw new InvalidConfigurationException('Algorithm "argon2i" is not available. You should either use "argon2id", downgrade your sodium extension or use a different encoder.');
556566
}
567+
throw new InvalidConfigurationException('Algorithm "argon2i" is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.');
568+
}
557569

558-
throw new InvalidConfigurationException('Argon2i algorithm is not supported. Install the libsodium extension or use "auto" instead.');
570+
return [
571+
'class' => NativePasswordEncoder::class,
572+
'arguments' => [
573+
$config['time_cost'] ?? null,
574+
(($config['memory_cost'] ?? 0) << 10) ?: null,
575+
$config['cost'] ?? null,
576+
\PASSWORD_ARGON2I,
577+
],
578+
];
579+
}
580+
581+
if ('argon2id' === $config['algorithm']) {
582+
if (($hasSodium = SodiumPasswordEncoder::isSupported()) && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
583+
return [
584+
'class' => SodiumPasswordEncoder::class,
585+
'arguments' => [
586+
$config['time_cost'] ?? null,
587+
(($config['memory_cost'] ?? 0) << 10) ?: null,
588+
],
589+
];
590+
}
591+
592+
if (!\defined('PASSWORD_ARGON2ID')) {
593+
if (\defined('PASSWORD_ARGON2I')) {
594+
throw new InvalidConfigurationException(sprintf('Algorithm "argon2id" is not available. You can either use "argon2i", upgrade to PHP 7.3+, %s sodium extension or use a different encoder.', $hasSodium ? 'upgrade your' : 'install the'));
595+
}
596+
throw new InvalidConfigurationException(sprintf('Algorithm "argon2id" is not available. You should either %s sodium extension, upgrade to PHP 7.3+ or use a different encoder.', $hasSodium ? 'upgrade your' : 'install the'));
559597
}
560598

561599
return [
562-
'class' => 'Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder',
600+
'class' => NativePasswordEncoder::class,
563601
'arguments' => [
564-
$config['memory_cost'],
565-
$config['time_cost'],
566-
$config['threads'],
602+
$config['time_cost'] ?? null,
603+
(($config['memory_cost'] ?? 0) << 10) ?: null,
604+
$config['cost'] ?? null,
605+
\PASSWORD_ARGON2ID,
567606
],
568607
];
569608
}

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

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
use Symfony\Component\DependencyInjection\ContainerBuilder;
1919
use Symfony\Component\DependencyInjection\Reference;
2020
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
21-
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
21+
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
2222
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
2323

2424
abstract class CompleteConfigurationTest extends TestCase
@@ -377,14 +377,9 @@ public function testEncodersWithLibsodium()
377377
]], $container->getDefinition('security.encoder_factory.generic')->getArguments());
378378
}
379379

380-
/**
381-
* @group legacy
382-
*
383-
* @expectedDeprecation Configuring an encoder with "argon2i" as algorithm is deprecated since Symfony 4.3, use "auto" instead.
384-
*/
385380
public function testEncodersWithArgon2i()
386381
{
387-
if (!Argon2iPasswordEncoder::isSupported()) {
382+
if (!($sodium = (SodiumPasswordEncoder::isSupported()) && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) {
388383
$this->markTestSkipped('Argon2i algorithm is not supported.');
389384
}
390385

@@ -429,15 +424,12 @@ public function testEncodersWithArgon2i()
429424
'arguments' => [8, 102400, 15],
430425
],
431426
'JMS\FooBundle\Entity\User7' => [
432-
'class' => 'Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder',
433-
'arguments' => [256, 1, 2],
427+
'class' => $sodium ? SodiumPasswordEncoder::class : NativePasswordEncoder::class,
428+
'arguments' => $sodium ? [256, 1] : [1, 262144, null, \PASSWORD_ARGON2I],
434429
],
435430
]], $container->getDefinition('security.encoder_factory.generic')->getArguments());
436431
}
437432

438-
/**
439-
* @group legacy
440-
*/
441433
public function testEncodersWithBCrypt()
442434
{
443435
$container = $this->getContainer('bcrypt_encoder');
@@ -481,8 +473,8 @@ public function testEncodersWithBCrypt()
481473
'arguments' => [8, 102400, 15],
482474
],
483475
'JMS\FooBundle\Entity\User7' => [
484-
'class' => 'Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder',
485-
'arguments' => [15],
476+
'class' => NativePasswordEncoder::class,
477+
'arguments' => [null, null, 15, \PASSWORD_BCRYPT],
486478
],
487479
]], $container->getDefinition('security.encoder_factory.generic')->getArguments());
488480
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
'algorithm' => 'argon2i',
99
'memory_cost' => 256,
1010
'time_cost' => 1,
11-
'threads' => 2,
1211
],
1312
],
1413
]);

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2i_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="argon2i" memory_cost="256" time_cost="1" threads="2" />
13+
<sec:encoder class="JMS\FooBundle\Entity\User7" algorithm="argon2i" memory_cost="256" time_cost="1" />
1414
</sec:config>
1515

1616
</container>

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,3 @@ security:
77
algorithm: argon2i
88
memory_cost: 256
99
time_cost: 1
10-
threads: 2

src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
use Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand;
1616
use Symfony\Component\Console\Application as ConsoleApplication;
1717
use Symfony\Component\Console\Tester\CommandTester;
18-
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
19-
use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder;
2018
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
2119
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
2220
use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder;
@@ -55,9 +53,6 @@ public function testEncodeNoPasswordNoInteraction()
5553
$this->assertEquals($statusCode, 1);
5654
}
5755

58-
/**
59-
* @group legacy
60-
*/
6156
public function testEncodePasswordBcrypt()
6257
{
6358
$this->setupBcrypt();
@@ -70,18 +65,15 @@ public function testEncodePasswordBcrypt()
7065
$output = $this->passwordEncoderCommandTester->getDisplay();
7166
$this->assertStringContainsString('Password encoding succeeded', $output);
7267

73-
$encoder = new BCryptPasswordEncoder(17);
68+
$encoder = new NativePasswordEncoder(null, null, 17, \PASSWORD_BCRYPT);
7469
preg_match('# Encoded password\s{1,}([\w+\/$.]+={0,2})\s+#', $output, $matches);
7570
$hash = $matches[1];
7671
$this->assertTrue($encoder->isPasswordValid($hash, 'password', null));
7772
}
7873

79-
/**
80-
* @group legacy
81-
*/
8274
public function testEncodePasswordArgon2i()
8375
{
84-
if (!Argon2iPasswordEncoder::isSupported()) {
76+
if (!($sodium = (SodiumPasswordEncoder::isSupported()) && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) {
8577
$this->markTestSkipped('Argon2i algorithm not available.');
8678
}
8779
$this->setupArgon2i();
@@ -94,8 +86,8 @@ public function testEncodePasswordArgon2i()
9486
$output = $this->passwordEncoderCommandTester->getDisplay();
9587
$this->assertStringContainsString('Password encoding succeeded', $output);
9688

97-
$encoder = new Argon2iPasswordEncoder();
98-
preg_match('# Encoded password\s+(\$argon2id?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches);
89+
$encoder = $sodium ? new SodiumPasswordEncoder() : new NativePasswordEncoder(null, null, null, \PASSWORD_ARGON2I);
90+
preg_match('# Encoded password\s+(\$argon2i?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches);
9991
$hash = $matches[1];
10092
$this->assertTrue($encoder->isPasswordValid($hash, 'password', null));
10193
}
@@ -195,12 +187,9 @@ public function testEncodePasswordNativeOutput()
195187
$this->assertStringNotContainsString(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay());
196188
}
197189

198-
/**
199-
* @group legacy
200-
*/
201190
public function testEncodePasswordArgon2iOutput()
202191
{
203-
if (!Argon2iPasswordEncoder::isSupported()) {
192+
if (!($sodium = (SodiumPasswordEncoder::isSupported()) && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) {
204193
$this->markTestSkipped('Argon2i algorithm not available.');
205194
}
206195

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

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\Security\Core\Encoder;
1313

14+
use Symfony\Component\Security\Core\Exception\LogicException;
15+
1416
/**
1517
* A generic encoder factory implementation.
1618
*
@@ -114,11 +116,15 @@ private function getEncoderConfigFromAlgorithm(array $config): array
114116
],
115117
];
116118

117-
/* @deprecated since Symfony 4.3 */
118119
case 'bcrypt':
119120
return [
120-
'class' => BCryptPasswordEncoder::class,
121-
'arguments' => [$config['cost']],
121+
'class' => NativePasswordEncoder::class,
122+
'arguments' => [
123+
$config['time_cost'] ?? null,
124+
(($config['memory_cost'] ?? 0) << 10) ?: null,
125+
$config['cost'] ?? null,
126+
\PASSWORD_BCRYPT,
127+
],
122128
];
123129

124130
case 'native':
@@ -140,14 +146,52 @@ private function getEncoderConfigFromAlgorithm(array $config): array
140146
],
141147
];
142148

143-
/* @deprecated since Symfony 4.3 */
144149
case 'argon2i':
150+
if (SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
151+
return [
152+
'class' => SodiumPasswordEncoder::class,
153+
'arguments' => [
154+
$config['time_cost'] ?? null,
155+
(($config['memory_cost'] ?? 0) << 10) ?: null,
156+
],
157+
];
158+
}
159+
160+
if (!\defined('PASSWORD_ARGON2I')) {
161+
throw new LogicException('Algorithm "argon2i" is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.');
162+
}
163+
145164
return [
146-
'class' => Argon2iPasswordEncoder::class,
165+
'class' => NativePasswordEncoder::class,
147166
'arguments' => [
148-
$config['memory_cost'],
149-
$config['time_cost'],
150-
$config['threads'],
167+
$config['time_cost'] ?? null,
168+
(($config['memory_cost'] ?? 0) << 10) ?: null,
169+
$config['cost'] ?? null,
170+
\PASSWORD_ARGON2I,
171+
],
172+
];
173+
case 'argon2id':
174+
if (($hasSodium = SodiumPasswordEncoder::isSupported()) && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
175+
return [
176+
'class' => SodiumPasswordEncoder::class,
177+
'arguments' => [
178+
$config['time_cost'] ?? null,
179+
(($config['memory_cost'] ?? 0) << 10) ?: null,
180+
],
181+
];
182+
}
183+
184+
if (!\defined('PASSWORD_ARGON2ID')) {
185+
throw new LogicException(sprintf('Algorithm "argon2id" is not available. You should either %s sodium extension, upgrade to PHP 7.3+ or use a different encoder.', $hasSodium ? 'upgrade your' : 'install the'));
186+
}
187+
188+
return [
189+
'class' => NativePasswordEncoder::class,
190+
'arguments' => [
191+
$config['time_cost'] ?? null,
192+
(($config['memory_cost'] ?? 0) << 10) ?: null,
193+
$config['cost'] ?? null,
194+
\PASSWORD_ARGON2ID,
151195
],
152196
];
153197
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ final class NativePasswordEncoder implements PasswordEncoderInterface, SelfSalti
2727
private $algo;
2828
private $options;
2929

30-
public function __construct(int $opsLimit = null, int $memLimit = null, int $cost = null)
30+
public function __construct(int $opsLimit = null, int $memLimit = null, int $cost = null, int $algo = null)
3131
{
3232
$cost = $cost ?? 13;
3333
$opsLimit = $opsLimit ?? max(4, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE : 4);
@@ -45,7 +45,7 @@ public function __construct(int $opsLimit = null, int $memLimit = null, int $cos
4545
throw new \InvalidArgumentException('$cost must be in the range of 4-31.');
4646
}
4747

48-
$this->algo = \defined('PASSWORD_ARGON2I') ? max(PASSWORD_DEFAULT, \defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_ARGON2I) : PASSWORD_DEFAULT;
48+
$this->algo = $algo ?? (\defined('PASSWORD_ARGON2I') ? max(PASSWORD_DEFAULT, \defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_ARGON2I) : PASSWORD_DEFAULT);
4949
$this->options = [
5050
'cost' => $cost,
5151
'time_cost' => $opsLimit,

0 commit comments

Comments
 (0)