Skip to content

Commit 6991f27

Browse files
Merge branch '4.4'
* 4.4: [OptionsResolve] Revert change in tests for a not-merged change in code [HttpClient] fix handling of 3xx with no Location header - ignore Content-Length when no body is expected [Workflow] Made the configuration more robust for the 'property' key [Security/Core] make NativePasswordEncoder use sodium to validate passwords when possible [FrameworkBundle] make SodiumVault report bad decryption key accurately cs fix [Security] Allow to set a fixed algorithm [Security/Core] make encodedLength computation more generic [Security/Core] add fast path when encoded password cannot match anything #30432 fix an error message fix paths to detect code owners [HttpClient] ignore the body of responses to HEAD requests [Validator] Ensure numeric subpaths do not cause errors on PHP 7.4 [SecurityBundle] Fix wrong assertion Remove unused local variables in tests [Yaml][Parser] Remove the getLastLineNumberBeforeDeprecation() internal unused method Make sure to collect child forms created on *_SET_DATA events [WebProfilerBundle] Improve display in Email panel for dark theme do not render errors for checkboxes twice
2 parents ff6f789 + 3e4ef8c commit 6991f27

7 files changed

+92
-20
lines changed

Encoder/EncoderFactory.php

Lines changed: 33 additions & 1 deletion
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,14 +116,20 @@ private function getEncoderConfigFromAlgorithm(array $config): array
114116
],
115117
];
116118

119+
case 'bcrypt':
120+
$config['algorithm'] = 'native';
121+
$config['native_algorithm'] = PASSWORD_BCRYPT;
122+
123+
return $this->getEncoderConfigFromAlgorithm($config);
124+
117125
case 'native':
118126
return [
119127
'class' => NativePasswordEncoder::class,
120128
'arguments' => [
121129
$config['time_cost'] ?? null,
122130
(($config['memory_cost'] ?? 0) << 10) ?: null,
123131
$config['cost'] ?? null,
124-
],
132+
] + (isset($config['native_algorithm']) ? [3 => $config['native_algorithm']] : []),
125133
];
126134

127135
case 'sodium':
@@ -132,6 +140,30 @@ private function getEncoderConfigFromAlgorithm(array $config): array
132140
(($config['memory_cost'] ?? 0) << 10) ?: null,
133141
],
134142
];
143+
144+
case 'argon2i':
145+
if (SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
146+
$config['algorithm'] = 'sodium';
147+
} elseif (\defined('PASSWORD_ARGON2I')) {
148+
$config['algorithm'] = 'native';
149+
$config['native_algorithm'] = PASSWORD_ARGON2I;
150+
} else {
151+
throw new LogicException(sprintf('Algorithm "argon2i" is not available. Either use %s"auto" or upgrade to PHP 7.2+ instead.', \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13') ? '"argon2id", ' : ''));
152+
}
153+
154+
return $this->getEncoderConfigFromAlgorithm($config);
155+
156+
case 'argon2id':
157+
if (($hasSodium = SodiumPasswordEncoder::isSupported()) && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
158+
$config['algorithm'] = 'sodium';
159+
} elseif (\defined('PASSWORD_ARGON2ID')) {
160+
$config['algorithm'] = 'native';
161+
$config['native_algorithm'] = PASSWORD_ARGON2ID;
162+
} else {
163+
throw new LogicException(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", ' : ''));
164+
}
165+
166+
return $this->getEncoderConfigFromAlgorithm($config);
135167
}
136168

137169
return [

Encoder/MessageDigestPasswordEncoder.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ class MessageDigestPasswordEncoder extends BasePasswordEncoder
2222
{
2323
private $algorithm;
2424
private $encodeHashAsBase64;
25-
private $iterations;
25+
private $iterations = 1;
26+
private $encodedLength = -1;
2627

2728
/**
2829
* @param string $algorithm The digest algorithm to use
@@ -33,6 +34,13 @@ public function __construct(string $algorithm = 'sha512', bool $encodeHashAsBase
3334
{
3435
$this->algorithm = $algorithm;
3536
$this->encodeHashAsBase64 = $encodeHashAsBase64;
37+
38+
try {
39+
$this->encodedLength = \strlen($this->encodePassword('', 'salt'));
40+
} catch (\LogicException $e) {
41+
// ignore algorithm not supported
42+
}
43+
3644
$this->iterations = $iterations;
3745
}
3846

@@ -65,6 +73,10 @@ public function encodePassword(string $raw, ?string $salt)
6573
*/
6674
public function isPasswordValid(string $encoded, string $raw, ?string $salt)
6775
{
76+
if (\strlen($encoded) !== $this->encodedLength || false !== strpos($encoded, '$')) {
77+
return false;
78+
}
79+
6880
return !$this->isPasswordTooLong($raw) && $this->comparePasswords($encoded, $this->encodePassword($raw, $salt));
6981
}
7082
}

Encoder/NativePasswordEncoder.php

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ 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+
/**
31+
* @param string|null $algo An algorithm supported by password_hash() or null to use the stronger available algorithm
32+
*/
33+
public function __construct(int $opsLimit = null, int $memLimit = null, int $cost = null, string $algo = null)
3134
{
3235
$cost = $cost ?? 13;
3336
$opsLimit = $opsLimit ?? max(4, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE : 4);
@@ -45,7 +48,7 @@ public function __construct(int $opsLimit = null, int $memLimit = null, int $cos
4548
throw new \InvalidArgumentException('$cost must be in the range of 4-31.');
4649
}
4750

48-
$this->algo = \defined('PASSWORD_ARGON2I') ? max(PASSWORD_DEFAULT, \defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_ARGON2I) : PASSWORD_DEFAULT;
51+
$this->algo = (string) ($algo ?? \defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : (\defined('PASSWORD_ARGON2I') ? PASSWORD_ARGON2I : PASSWORD_BCRYPT));
4952
$this->options = [
5053
'cost' => $cost,
5154
'time_cost' => $opsLimit,
@@ -59,33 +62,38 @@ public function __construct(int $opsLimit = null, int $memLimit = null, int $cos
5962
*/
6063
public function encodePassword(string $raw, ?string $salt): string
6164
{
62-
if (\strlen($raw) > self::MAX_PASSWORD_LENGTH) {
65+
if (\strlen($raw) > self::MAX_PASSWORD_LENGTH || ((string) PASSWORD_BCRYPT === $this->algo && 72 < \strlen($raw))) {
6366
throw new BadCredentialsException('Invalid password.');
6467
}
6568

6669
// Ignore $salt, the auto-generated one is always the best
6770

68-
$encoded = password_hash($raw, $this->algo, $this->options);
69-
70-
if (72 < \strlen($raw) && 0 === strpos($encoded, '$2')) {
71-
// BCrypt encodes only the first 72 chars
72-
throw new BadCredentialsException('Invalid password.');
73-
}
74-
75-
return $encoded;
71+
return password_hash($raw, $this->algo, $this->options);
7672
}
7773

7874
/**
7975
* {@inheritdoc}
8076
*/
8177
public function isPasswordValid(string $encoded, string $raw, ?string $salt): bool
8278
{
83-
if (72 < \strlen($raw) && 0 === strpos($encoded, '$2')) {
84-
// BCrypt encodes only the first 72 chars
79+
if (\strlen($raw) > self::MAX_PASSWORD_LENGTH) {
8580
return false;
8681
}
8782

88-
return \strlen($raw) <= self::MAX_PASSWORD_LENGTH && password_verify($raw, $encoded);
83+
if (0 === strpos($encoded, '$2')) {
84+
// BCrypt encodes only the first 72 chars
85+
return 72 >= \strlen($raw) && password_verify($raw, $encoded);
86+
}
87+
88+
if (\extension_loaded('sodium') && version_compare(\SODIUM_LIBRARY_VERSION, '1.0.14', '>=')) {
89+
return sodium_crypto_pwhash_str_verify($encoded, $raw);
90+
}
91+
92+
if (\extension_loaded('libsodium') && version_compare(phpversion('libsodium'), '1.0.14', '>=')) {
93+
return \Sodium\crypto_pwhash_str_verify($encoded, $raw);
94+
}
95+
96+
return password_verify($raw, $encoded);
8997
}
9098

9199
/**

Encoder/Pbkdf2PasswordEncoder.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ class Pbkdf2PasswordEncoder extends BasePasswordEncoder
3030
{
3131
private $algorithm;
3232
private $encodeHashAsBase64;
33-
private $iterations;
33+
private $iterations = 1;
3434
private $length;
35+
private $encodedLength = -1;
3536

3637
/**
3738
* @param string $algorithm The digest algorithm to use
@@ -43,8 +44,15 @@ public function __construct(string $algorithm = 'sha512', bool $encodeHashAsBase
4344
{
4445
$this->algorithm = $algorithm;
4546
$this->encodeHashAsBase64 = $encodeHashAsBase64;
46-
$this->iterations = $iterations;
4747
$this->length = $length;
48+
49+
try {
50+
$this->encodedLength = \strlen($this->encodePassword('', 'salt'));
51+
} catch (\LogicException $e) {
52+
// ignore algorithm not supported
53+
}
54+
55+
$this->iterations = $iterations;
4856
}
4957

5058
/**
@@ -72,6 +80,10 @@ public function encodePassword(string $raw, ?string $salt)
7280
*/
7381
public function isPasswordValid(string $encoded, string $raw, ?string $salt)
7482
{
83+
if (\strlen($encoded) !== $this->encodedLength || false !== strpos($encoded, '$')) {
84+
return false;
85+
}
86+
7587
return !$this->isPasswordTooLong($raw) && $this->comparePasswords($encoded, $this->encodePassword($raw, $salt));
7688
}
7789
}

Encoder/SodiumPasswordEncoder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public function isPasswordValid(string $encoded, string $raw, ?string $salt): bo
9393
return \Sodium\crypto_pwhash_str_verify($encoded, $raw);
9494
}
9595

96-
throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.');
96+
return false;
9797
}
9898

9999
/**

Tests/Encoder/EncoderFactoryTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ public function testGetInvalidNamedEncoderForEncoderAware()
117117

118118
$user = new EncAwareUser('user', 'pass');
119119
$user->encoderName = 'invalid_encoder_name';
120-
$encoder = $factory->getEncoder($user);
120+
$factory->getEncoder($user);
121121
}
122122

123123
public function testGetEncoderForEncoderAwareWithClassName()

Tests/Encoder/NativePasswordEncoderTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ public function testValidation()
5555
$this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword', null));
5656
}
5757

58+
public function testConfiguredAlgorithm()
59+
{
60+
$encoder = new NativePasswordEncoder(null, null, null, PASSWORD_BCRYPT);
61+
$result = $encoder->encodePassword('password', null);
62+
$this->assertTrue($encoder->isPasswordValid($result, 'password', null));
63+
$this->assertStringStartsWith('$2', $result);
64+
}
65+
5866
public function testCheckPasswordLength()
5967
{
6068
$encoder = new NativePasswordEncoder(null, null, 4);

0 commit comments

Comments
 (0)