Skip to content

[Security] Allow to stick to a specific password hashing algorithm #34020

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions UPGRADE-4.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,6 @@ Security
* Not implementing the methods `__serialize` and `__unserialize` in classes implementing
the `TokenInterface` is deprecated

SecurityBundle
--------------

* Configuring encoders using `argon2i` or `bcrypt` as algorithm has been deprecated, use `auto` instead.

TwigBridge
----------

Expand Down
1 change: 0 additions & 1 deletion UPGRADE-5.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,6 @@ SecurityBundle
changed to underscores.
Before: `my-cookie` deleted the `my_cookie` cookie (with an underscore).
After: `my-cookie` deletes the `my-cookie` cookie (with a dash).
* Configuring encoders using `argon2i` or `bcrypt` as algorithm is not supported anymore, use `auto` instead.

Serializer
----------
Expand Down
5 changes: 3 additions & 2 deletions src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ CHANGELOG
=========

4.4.0
-----

* Deprecated the usage of "query_string" without a "search_dn" and a "search_password" config key in Ldap factories.
* Added new `argon2id` encoder, undeprecated the `bcrypt` and `argon2i` ones (using `auto` is still recommended by default.)
* Deprecated the usage of "query_string" without a "search_dn" and a "search_password" config key in Ldap factories.

4.3.0
-----
Expand All @@ -14,7 +16,6 @@ CHANGELOG
option is deprecated and will be disabled in Symfony 5.0. This affects to cookies
with dashes in their names. For example, starting from Symfony 5.0, the `my-cookie`
name will delete `my-cookie` (with a dash) instead of `my_cookie` (with an underscore).
* Deprecated configuring encoders using `argon2i` as algorithm, use `auto` instead

4.2.0
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
use Symfony\Component\Security\Core\User\UserProviderInterface;
Expand Down Expand Up @@ -538,34 +537,37 @@ private function createEncoder(array $config)

// bcrypt encoder
if ('bcrypt' === $config['algorithm']) {
@trigger_error('Configuring an encoder with "bcrypt" as algorithm is deprecated since Symfony 4.3, use "auto" instead.', E_USER_DEPRECATED);
$config['algorithm'] = 'native';
$config['native_algorithm'] = PASSWORD_BCRYPT;

return [
'class' => 'Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder',
'arguments' => [$config['cost'] ?? 13],
];
return $this->createEncoder($config);
}

// Argon2i encoder
if ('argon2i' === $config['algorithm']) {
@trigger_error('Configuring an encoder with "argon2i" as algorithm is deprecated since Symfony 4.3, use "auto" instead.', E_USER_DEPRECATED);
if (SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
$config['algorithm'] = 'sodium';
} elseif (\defined('PASSWORD_ARGON2I')) {
$config['algorithm'] = 'native';
$config['native_algorithm'] = PASSWORD_ARGON2I;
} else {
throw new InvalidConfigurationException(sprintf('Algorithm "argon2i" is not available. Either use %s"auto" or upgrade to PHP 7.2+ instead.', \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13') ? '"argon2id", ' : ''));
}

if (!Argon2iPasswordEncoder::isSupported()) {
if (\extension_loaded('sodium') && !\defined('SODIUM_CRYPTO_PWHASH_SALTBYTES')) {
throw new InvalidConfigurationException('The installed libsodium version does not have support for Argon2i. Use "auto" instead.');
}
return $this->createEncoder($config);
}

throw new InvalidConfigurationException('Argon2i algorithm is not supported. Install the libsodium extension or use "auto" instead.');
if ('argon2id' === $config['algorithm']) {
if (($hasSodium = SodiumPasswordEncoder::isSupported()) && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
$config['algorithm'] = 'sodium';
} elseif (\defined('PASSWORD_ARGON2ID')) {
$config['algorithm'] = 'native';
$config['native_algorithm'] = PASSWORD_ARGON2ID;
} else {
throw new InvalidConfigurationException(sprintf('Algorithm "argon2id" is not available. Either use %s"auto", upgrade to PHP 7.3+ or use libsodium 1.0.15+ instead.', \defined('PASSWORD_ARGON2I') || $hasSodium ? '"argon2i", ' : ''));
}

return [
'class' => 'Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder',
'arguments' => [
$config['memory_cost'],
$config['time_cost'],
$config['threads'],
],
];
return $this->createEncoder($config);
}

if ('native' === $config['algorithm']) {
Expand All @@ -574,8 +576,8 @@ private function createEncoder(array $config)
'arguments' => [
$config['time_cost'],
(($config['memory_cost'] ?? 0) << 10) ?: null,
$config['cost'],
],
$config['cost']
] + (isset($config['native_algorithm']) ? [3 => $config['native_algorithm']] : []),
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;

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

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

Expand Down Expand Up @@ -429,19 +424,15 @@ public function testEncodersWithArgon2i()
'arguments' => [8, 102400, 15],
],
'JMS\FooBundle\Entity\User7' => [
'class' => 'Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder',
'arguments' => [256, 1, 2],
'class' => $sodium ? SodiumPasswordEncoder::class : NativePasswordEncoder::class,
'arguments' => $sodium ? [256, 1] : [1, 262144, null, \PASSWORD_ARGON2I],
],
]], $container->getDefinition('security.encoder_factory.generic')->getArguments());
}

/**
* @group legacy
*/
public function testEncodersWithBCrypt()
{
$container = $this->getContainer('bcrypt_encoder');

$this->assertEquals([[
'JMS\FooBundle\Entity\User1' => [
'class' => 'Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder',
Expand Down Expand Up @@ -481,8 +472,8 @@ public function testEncodersWithBCrypt()
'arguments' => [8, 102400, 15],
],
'JMS\FooBundle\Entity\User7' => [
'class' => 'Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder',
'arguments' => [15],
'class' => NativePasswordEncoder::class,
'arguments' => [null, null, 15, \PASSWORD_BCRYPT],
],
]], $container->getDefinition('security.encoder_factory.generic')->getArguments());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
'algorithm' => 'argon2i',
'memory_cost' => 256,
'time_cost' => 1,
'threads' => 2,
],
],
]);
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</imports>

<sec:config>
<sec:encoder class="JMS\FooBundle\Entity\User7" algorithm="argon2i" memory_cost="256" time_cost="1" threads="2" />
<sec:encoder class="JMS\FooBundle\Entity\User7" algorithm="argon2i" memory_cost="256" time_cost="1" />
</sec:config>

</container>
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@ security:
algorithm: argon2i
memory_cost: 256
time_cost: 1
threads: 2
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
use Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand;
use Symfony\Component\Console\Application as ConsoleApplication;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder;
Expand Down Expand Up @@ -55,9 +53,6 @@ public function testEncodeNoPasswordNoInteraction()
$this->assertEquals($statusCode, 1);
}

/**
* @group legacy
*/
public function testEncodePasswordBcrypt()
{
$this->setupBcrypt();
Expand All @@ -70,18 +65,15 @@ public function testEncodePasswordBcrypt()
$output = $this->passwordEncoderCommandTester->getDisplay();
$this->assertStringContainsString('Password encoding succeeded', $output);

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

/**
* @group legacy
*/
public function testEncodePasswordArgon2i()
{
if (!Argon2iPasswordEncoder::isSupported()) {
if (!($sodium = SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) {
$this->markTestSkipped('Argon2i algorithm not available.');
}
$this->setupArgon2i();
Expand All @@ -94,7 +86,28 @@ public function testEncodePasswordArgon2i()
$output = $this->passwordEncoderCommandTester->getDisplay();
$this->assertStringContainsString('Password encoding succeeded', $output);

$encoder = new Argon2iPasswordEncoder();
$encoder = $sodium ? new SodiumPasswordEncoder() : new NativePasswordEncoder(null, null, null, PASSWORD_ARGON2I);
preg_match('# Encoded password\s+(\$argon2i?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches);
$hash = $matches[1];
$this->assertTrue($encoder->isPasswordValid($hash, 'password', null));
}

public function testEncodePasswordArgon2id()
{
if (!($sodium = (SodiumPasswordEncoder::isSupported() && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13'))) && !\defined('PASSWORD_ARGON2ID')) {
$this->markTestSkipped('Argon2id algorithm not available.');
}
$this->setupArgon2id();
$this->passwordEncoderCommandTester->execute([
'command' => 'security:encode-password',
'password' => 'password',
'user-class' => 'Custom\Class\Argon2id\User',
], ['interactive' => false]);

$output = $this->passwordEncoderCommandTester->getDisplay();
$this->assertStringContainsString('Password encoding succeeded', $output);

$encoder = $sodium ? new SodiumPasswordEncoder() : new NativePasswordEncoder(null, null, null, PASSWORD_ARGON2ID);
preg_match('# Encoded password\s+(\$argon2id?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches);
$hash = $matches[1];
$this->assertTrue($encoder->isPasswordValid($hash, 'password', null));
Expand Down Expand Up @@ -195,12 +208,9 @@ public function testEncodePasswordNativeOutput()
$this->assertStringNotContainsString(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay());
}

/**
* @group legacy
*/
public function testEncodePasswordArgon2iOutput()
{
if (!Argon2iPasswordEncoder::isSupported()) {
if (!(SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) {
$this->markTestSkipped('Argon2i algorithm not available.');
}

Expand All @@ -214,6 +224,22 @@ public function testEncodePasswordArgon2iOutput()
$this->assertStringNotContainsString(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay());
}

public function testEncodePasswordArgon2idOutput()
{
if (!(SodiumPasswordEncoder::isSupported() && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2ID')) {
$this->markTestSkipped('Argon2id algorithm not available.');
}

$this->setupArgon2id();
$this->passwordEncoderCommandTester->execute([
'command' => 'security:encode-password',
'password' => 'p@ssw0rd',
'user-class' => 'Custom\Class\Argon2id\User',
], ['interactive' => false]);

$this->assertStringNotContainsString(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay());
}

public function testEncodePasswordSodiumOutput()
{
if (!SodiumPasswordEncoder::isSupported()) {
Expand Down Expand Up @@ -317,6 +343,19 @@ private function setupArgon2i()
$this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand);
}

private function setupArgon2id()
{
putenv('COLUMNS='.(119 + \strlen(PHP_EOL)));
$kernel = $this->createKernel(['test_case' => 'PasswordEncode', 'root_config' => 'argon2id.yml']);
$kernel->boot();

$application = new Application($kernel);

$passwordEncoderCommand = $application->get('security:encode-password');

$this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand);
}

private function setupBcrypt()
{
putenv('COLUMNS='.(119 + \strlen(PHP_EOL)));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
imports:
- { resource: config.yml }

security:
encoders:
Custom\Class\Argon2id\User:
algorithm: argon2id
1 change: 1 addition & 0 deletions src/Symfony/Component/Security/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ CHANGELOG
* Marked all dispatched event classes as `@final`
* Deprecated returning a non-boolean value when implementing `Guard\AuthenticatorInterface::checkCredentials()`.
* Deprecated passing more than one attribute to `AccessDecisionManager::decide()` and `AuthorizationChecker::isGranted()`
* Added new `argon2id` encoder, undeprecated the `bcrypt` and `argon2i` ones (using `auto` is still recommended by default.)

4.3.0
-----
Expand Down
Loading