Skip to content

Commit 7e82a64

Browse files
author
Robin Chalas
committed
[Security] Add Argon2idPasswordEncoder
1 parent ca29039 commit 7e82a64

17 files changed

+430
-50
lines changed

UPGRADE-4.3.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,17 @@ Security
145145
}
146146
```
147147

148+
* Using `Argon2iPasswordEncoder` while only the `argon2id` algorithm is supported
149+
is deprecated, use `Argon2idPasswordEncoder` instead
150+
151+
SecurityBundle
152+
--------------
153+
154+
* Configuring encoders using `argon2i` as algorithm while only `argon2id` is
155+
supported is deprecated, use `argon2id` instead
156+
148157
TwigBridge
149-
==========
158+
----------
150159

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

UPGRADE-5.0.md

+5
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,9 @@ Security
323323
}
324324
```
325325

326+
* Using `Argon2iPasswordEncoder` while only the `argon2id` algorithm is supported
327+
now throws a \LogicException`, use `Argon2idPasswordEncoder` instead
328+
326329
SecurityBundle
327330
--------------
328331

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

346351
Serializer
347352
----------

src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

+3
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

+19
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' => 'Symfony\Component\Security\Core\Encoder\Argon2idPasswordEncoder',
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

+55-1
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');
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+
]);
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>
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

+54-3
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
}
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

+3
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
-----
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.
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)