8000 feature #30968 [Security] Add Argon2idPasswordEncoder (chalasr) · symfony/symfony@fa7df09 · GitHub
[go: up one dir, main page]

Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit fa7df09

Browse files
committed
feature #30968 [Security] Add Argon2idPasswordEncoder (chalasr)
This PR was merged into the 4.3-dev branch. Discussion ---------- [Security] Add Argon2idPasswordEncoder | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | yes | Tests pass? | yes | Fixed tickets | #28093 | License | MIT | Doc PR | TODO Currently we have a `Argon2iPasswordEncoder` that may hash passwords using `argon2id` instead of `argon2i` (platform-dependent) which is not good. This deprecates producing/validating `argon2id` hashed passwords using the `Argon2iPasswordEncoder`, and adds a `Argon2idPasswordEncoder` able to produce/validate `argon2id` hashed passwords only. #EUFOSSA Commits ------- 0c82173 [Security] Add Argon2idPasswordEncoder
2 parents 5638d6a + 0c82173 commit fa7df09

File tree

17 files changed

+430
-50
lines changed

17 files changed

+430
-50
lines changed

UPGRADE-4.3.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,17 @@ Security
151151
}
152152
```
153153

154+
* Using `Argon2iPasswordEncoder` while only the `argon2id` algorithm is supported
155+
is deprecated, use `Argon2idPasswordEncoder` instead
156+
157+
SecurityBundle
158+
--------------
159+
160+
* Configuring encoders using `argon2i` as algorithm while only `argon2id` is
161+
supported is deprecated, use `argon2id` instead
162+
154163
TwigBridge
155-
==========
164+
----------
156165

157166
* deprecated the `$requestStack` and `$requestContext` arguments of the
158167
`HttpFoundationExtension`, pass a `Symfony\Component\HttpFoundation\UrlHelper`

UPGRADE-5.0.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,9 @@ Security
326326
}
327327
```
328328

329+
* Using `Argon2iPasswordEncoder` while only the `argon2id` algorithm is supported
330+
now throws a \LogicException`, use `Argon2idPasswordEncoder` instead
331+
329332
SecurityBundle
330333
--------------
331334

@@ -345,6 +348,8 @@ SecurityBundle
345348
changed to underscores.
346349
Before: `my-cookie` deleted the `my_cookie` cookie (with an underscore).
347350
After: `my-cookie` deletes the `my-cookie` cookie (with a dash).
351+
* Configuring encoders using `argon2i` as algorithm while only `argon2id` is supported
352+
now throws a `\LogicException`, use `argon2id` instead
348353

349354
Serializer
350355
----------

src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ CHANGELOG
88
option is deprecated and will be disabled in Symfony 5.0. This affects to cookies
99
with dashes in their names. For example, starting from Symfony 5.0, the `my-cookie`
1010
name will delete `my-cookie` (with a dash) instead of `my_cookie` (with an underscore).
11+
* Deprecated configuring encoders using `argon2i` as algorithm while only `argon2id` is supported,
12+
use `argon2id` instead
13+
1114

1215
4.2.0
1316
-----

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use Symfony\Component\DependencyInjection\Reference;
3030
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
3131
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
32+
use Symfony\Component\Security\Core\Encoder\Argon2idPasswordEncoder;
3233
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
3334
use Symfony\Component\Security\Core\User\UserProviderInterface;
3435
use Symfony\Component\Security\Http\Controller\UserValueResolver;
@@ -570,6 +571,8 @@ private function createEncoder($config, ContainerBuilder $container)
570571
}
571572

572573
throw new InvalidConfigurationException('Argon2i algorithm is not supported. Install the libsodium extension or use BCrypt instead.');
574+
} elseif (\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
575+
@trigger_error('Configuring an encoder based on the "argon2i" algorithm while only "argon2id" is supported is deprecated since Symfony 4.3, use "argon2id" instead.', E_USER_DEPRECATED);
573576
}
574577

575578
return [
@@ -582,6 +585,22 @@ private function createEncoder($config, ContainerBuilder $container)
582585
];
583586
}
584587

588+
// Argon2id encoder
589+
if ('argon2id' === $config['algorithm']) {
590+
if (!Argon2idPasswordEncoder::isSupported()) {
591+
throw new InvalidConfigurationException('Argon2i algorithm is not supported. Install the libsodium extension or use BCrypt instead.');
592+
}
593+
594+
return [
595+
'class' => Argon2idPasswordEncoder::class,
596+
'arguments' => [
597+
$config['memory_cost'],
598+
$config['time_cost'],
599+
$config['threads'],
600+
],
601+
];
602+
}
603+
585604
// run-time configured encoder
586605
return $config;
587606
}

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

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +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\Argon2idPasswordEncoder;
2122
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
2223

2324
abstract class CompleteConfigurationTest extends TestCase
@@ -313,7 +314,7 @@ public function testEncoders()
313314

314315
public function testEncodersWithLibsodium()
315316
{
316-
if (!Argon2iPasswordEncoder::isSupported()) {
317+
if (!Argon2iPasswordEncoder::isSupported() || \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
317318
$this->markTestSkipped('Argon2i algorithm is not supported.');
318319
}
319320

@@ -364,6 +365,59 @@ public function testEncodersWithLibsodium()
364365
]], $container->getDefinition('security.encoder_factory.generic')->getArguments());
365366
}
366367

368+
public function testEncodersWithArgon2id()
369+
{
370+
if (!Argon2idPasswordEncoder::isSupported()) {
371+
$this->markTestSkipped('Argon2i algorithm is not supported.');
372+
}
373+
374+
$container = $this->getContainer('argon2id_encoder');
375+
376+
$this->assertEquals([[
377+
'JMS\FooBundle\Entity\User1' => [
378+
'class' => 'Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder',
379+
'arguments' => [false],
380+
],
381+
'JMS\FooBundle\Entity\User2' => [
382+
'algorithm' => 'sha1',
383+
'encode_as_base64' => false,
384+
'iterations' => 5,
385+
'hash_algorithm' => 'sha512',
386+
'key_length' => 40,
387+
'ignore_case' => false,
388+
'cost' => 13,
389+
'memory_cost' => null,
390+
'time_cost' => null,
391+
'threads' => null,
392+
],
393+
'JMS\FooBundle\Entity\User3' => [
394+
'algorithm' => 'md5',
395+
'hash_algorithm' => 'sha512',
396+
'key_length' => 40,
397+
'ignore_case' => false,
398+
'encode_as_base64' => true,
399+
'iterations' => 5000,
400+
'cost' => 13,
401+
'memory_cost' => null,
402+
'time_cost' => null,
403+
'threads' => null,
404+
],
405+
'JMS\FooBundle\Entity\User4' => new Reference('security.encoder.foo'),
406+
'JMS\FooBundle\Entity\User5' => [
407+
'class' => 'Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder',
408+
'arguments' => ['sha1', false, 5, 30],
409+
],
410+
'JMS\FooBundle\Entity\User6' => [
411+
'class' => 'Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder',
412+
'arguments' => [15],
413+
],
414+
'JMS\FooBundle\Entity\User7' => [
415+
'class' => 'Symfony\Component\Security\Core\Encoder\Argon2idPasswordEncoder',
416+
'arguments' => [256, 1, 2],
417+
],
418+
]], $container->getDefinition('security.encoder_factory.generic')->getArguments());
419+
}
420+
367421
public function testRememberMeThrowExceptionsDefault()
368422
{
369423
$container = $this->getContainer('container1');
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
$this->load('container1.php', $container);
4+
5+
$container->loadFromExtension('security', [
6+
'encoders' => [
7+
'JMS\FooBundle\Entity\User7' => [
8+
'algorithm' => 'argon2id',
9+
'memory_cost' => 256,
10+
'time_cost' => 1,
11+
'threads' => 2,
12+
],
13+
],
14+
]);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xmlns:sec="http://symfony.com/schema/dic/security"
6+
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
7+
8+
<imports>
9+
<import resource="container1.xml"/>
10+
</imports>
11+
12+
<sec:config>
13+
<sec:encoder class="JMS\FooBundle\Entity\User7" algorithm="argon2id" memory_cost="256" time_cost="1" threads="2" />
14+
</sec:config>
15+
16+
</container>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
imports:
2+
- { resource: container1.yml }
3+
4+
security:
5+
encoders:
6+
JMS\FooBundle\Entity\User7:
7+
algorithm: argon2id
8+
memory_cost: 256
9+
time_cost: 1
10+
threads: 2

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

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
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\Argon2idPasswordEncoder;
1819
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
1920
use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder;
2021
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
@@ -72,7 +73,7 @@ public function testEncodePasswordBcrypt()
7273

7374
public function testEncodePasswordArgon2i()
7475
{
75-
if (!Argon2iPasswordEncoder::isSupported()) {
76+
if (!Argon2iPasswordEncoder::isSupported() || \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
7677
$this->markTestSkipped('Argon2i algorithm not available.');
7778
}
7879
$this->setupArgon2i();
@@ -85,6 +86,27 @@ public function testEncodePasswordArgon2i()
8586
$output = $this->passwordEncoderCommandTester->getDisplay();
8687
$this->assertContains('Password encoding succeeded', $output);
8788

89+
$encoder = new Argon2iPasswordEncoder();
90+
preg_match('# Encoded password\s+(\$argon2i?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches);
91+
$hash = $matches[1];
92+
$this->assertTrue($encoder->isPasswordValid($hash, 'password', null));
93+
}
94+
95+
public function testEncodePasswordArgon2id()
96+
{
97+
if (!Argon2idPasswordEncoder::isSupported()) {
98+
$this->markTestSkipped('Argon2i algorithm not available.');
99+
}
100+
$this->setupArgon2id();
101+
$this->passwordEncoderCommandTester->execute([
102+
'command' => 'security:encode-password',
103+
'password' => 'password',
104+
'user-class' => 'Custom\Class\Argon2id\User',
105+
], ['interactive' => false]);
106+
107+
$output = $this->passwordEncoderCommandTester->getDisplay();
108+
$this->assertContains('Password encoding succeeded', $output);
109+
88110
$encoder = new Argon2iPasswordEncoder();
89111
preg_match('# Encoded password\s+(\$argon2id?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches);
90112
$hash = $matches[1];
@@ -153,8 +175,8 @@ public function testEncodePasswordBcryptOutput()
153175

154176
public function testEncodePasswordArgon2iOutput()
155177
{
156-
if (!Argon2iPasswordEncoder::isSupported()) {
157-
$this->markTestSkipped('Argon2i algorithm not available.');
178+
if (!Argon2iPasswordEncoder::isSupported() || \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
179+
$this->markTestSkipped('Argon2id algorithm not available.');
158180
}
159181

160182
$this->setupArgon2i();
@@ -167,6 +189,22 @@ public function testEncodePasswordArgon2iOutput()
167189
$this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay());
168190
}
169191

192+
public function testEncodePasswordArgon2idOutput()
193+
{
194+
if (!Argon2idPasswordEncoder::isSupported()) {
195+
$this->markTestSkipped('Argon2id algorithm not available.');
196+
}
197+
198+
$this->setupArgon2id();
199+
$this->passwordEncoderCommandTester->execute([
200+
'command' => 'security:encode-password',
201+
'password' => 'p@ssw0rd',
202+
'user-class' => 'Custom\Class\Argon2id\User',
203+
], ['interactive' => false]);
204+
205+
$this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay());
206+
}
207+
170208
public function testEncodePasswordNoConfigForGivenUserClass()
171209
{
172210
if (method_exists($this, 'expectException')) {
@@ -259,4 +297,17 @@ private function setupArgon2i()
259297

260298
$this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand);
261299
}
300+
301+
private function setupArgon2id()
302+
{
303+
putenv('COLUMNS='.(119 + \strlen(PHP_EOL)));
304+
$kernel = $this->createKernel(['test_case' => 'PasswordEncode', 'root_config' => 'argon2id.yml']);
305+
$kernel->boot();
306+
307+
$application = new Application($kernel);
308+
309+
$passwordEncoderCommand = $application->get('security:encode-password');
310+
311+
$this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand);
312+
}
262313
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
imports:
2+
- { resource: config.yml }
3+
4+
security:
5+
encoders:
6+
Custom\Class\Argon2id\User:
7+
algorithm: argon2id

src/Symfony/Component/Security/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ CHANGELOG
1919
* Dispatch `AuthenticationFailureEvent` on `security.authentication.failure`
2020
* Dispatch `InteractiveLoginEvent` on `security.interactive_login`
2121
* Dispatch `SwitchUserEvent` on `security.switch_user`
22+
* Added `Argon2idPasswordEncoder`
23+
* Deprecated using `Argon2iPasswordEncoder` while only the `argon2id` algorithm
24+
is supported, use `Argon2idPasswordEncoder` instead
2225

2326
4.2.0
2427
-----
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
4E34
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+
/**
15+
* @internal
16+
*
17+
* @author Robin Chalas <robin.chalas@gmail.com>
18+
*/
19+
trait Argon2Trait
20+
{
21+
private $memoryCost;
22+
private $timeCost;
23+
private $threads;
24+
25+
public function __construct(int $memoryCost = null, int $timeCost = null, int $threads = null)
26+
{
27+
$this->memoryCost = $memoryCost;
28+
$this->timeCost = $timeCost;
29+
$this->threads = $threads;
30+
}
31+
32+
private function encodePasswordNative(string $raw, int $algorithm)
33+
{
34+
return password_hash($raw, $algorithm, [
35+
'memory_cost' => $this->memoryCost ?? \PASSWORD_ARGON2_DEFAULT_MEMORY_COST,
36+
'time_cost' => $this->timeCost ?? \PASSWORD_ARGON2_DEFAULT_TIME_COST,
37+
'threads' => $this->threads ?? \PASSWORD_ARGON2_DEFAULT_THREADS,
38+
]);
39+
}
40+
41+
private function encodePasswordSodiumFunction(string $raw)
42+
{
43+
$hash = \sodium_crypto_pwhash_str(
44+
$raw,
45+
\SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
46+
\SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
47+
);
48+
\sodium_memzero($raw);
49+
50+
return $hash;
51+
}
52+
}

0 commit comments

Comments
 (0)
0