+ * @author Nicolas Grekas
+ */
+final class NativePasswordEncoder implements PasswordEncoderInterface, SelfSaltingEncoderInterface
+{
+ private const MAX_PASSWORD_LENGTH = 4096;
+
+ private $algo;
+ private $options;
+
+ public function __construct(int $opsLimit = null, int $memLimit = null, int $cost = null)
+ {
+ $cost = $cost ?? 13;
+ $opsLimit = $opsLimit ?? max(6, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE : 6);
+ $memLimit = $memLimit ?? max(64 * 1024 * 1024, \defined('SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE : 64 * 1024 * 1024);
+
+ if (2 > $opsLimit) {
+ throw new \InvalidArgumentException('$opsLimit must be 2 or greater.');
+ }
+
+ if (10 * 1024 > $memLimit) {
+ throw new \InvalidArgumentException('$memLimit must be 10k or greater.');
+ }
+
+ if ($cost < 4 || 31 < $cost) {
+ throw new \InvalidArgumentException('$cost must be in the range of 4-31.');
+ }
+
+ $this->algo = \defined('PASSWORD_ARGON2I') ? max(PASSWORD_DEFAULT, \defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_ARGON2I) : PASSWORD_DEFAULT;
+ $this->options = [
+ 'cost' => $cost,
+ 'time_cost' => $opsLimit,
+ 'memory_cost' => $memLimit >> 10,
+ 'threads' => 1,
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function encodePassword($raw, $salt)
+ {
+ if (\strlen($raw) > self::MAX_PASSWORD_LENGTH) {
+ throw new BadCredentialsException('Invalid password.');
+ }
+
+ // Ignore $salt, the auto-generated one is always the best
+
+ $encoded = password_hash($raw, $this->algo, $this->options);
+
+ if (72 < \strlen($raw) && 0 === strpos($encoded, '$2')) {
+ // BCrypt encodes only the first 72 chars
+ throw new BadCredentialsException('Invalid password.');
+ }
+
+ return $encoded;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isPasswordValid($encoded, $raw, $salt)
+ {
+ if (72 < \strlen($raw) && 0 === strpos($encoded, '$2')) {
+ // BCrypt encodes only the first 72 chars
+ return false;
+ }
+
+ return \strlen($raw) <= self::MAX_PASSWORD_LENGTH && password_verify($raw, $encoded);
+ }
+}
diff --git a/src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php
index febca05dd0..96fbdca173 100644
--- a/src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php
+++ b/src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php
@@ -20,11 +20,32 @@ use Symfony\Component\Security\Core\Exception\LogicException;
* @author Robin Chalas
* @author Zan Baldwin
* @author Dominik Müller
- *
- * @final
*/
-class SodiumPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEncoderInterface
+final class SodiumPasswordEncoder implements PasswordEncoderInterface, SelfSaltingEncoderInterface
{
+ private const MAX_PASSWORD_LENGTH = 4096;
+
+ private $opsLimit;
+ private $memLimit;
+
+ public function __construct(int $opsLimit = null, int $memLimit = null)
+ {
+ if (!self::isSupported()) {
+ throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.');
+ }
+
+ $this->opsLimit = $opsLimit ?? max(6, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE : 6);
+ $this->memLimit = $memLimit ?? max(64 * 1024 * 1024, \defined('SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE : 64 * 1024 * 2014);
+
+ if (2 > $this->opsLimit) {
+ throw new \InvalidArgumentException('$opsLimit must be 2 or greater.');
+ }
+
+ if (10 * 1024 > $this->memLimit) {
+ throw new \InvalidArgumentException('$memLimit must be 10k or greater.');
+ }
+ }
+
public static function isSupported(): bool
{
if (\class_exists('ParagonIE_Sodium_Compat') && \method_exists('ParagonIE_Sodium_Compat', 'crypto_pwhash_is_available')) {
@@ -39,24 +60,16 @@ class SodiumPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEn
*/
public function encodePassword($raw, $salt)
{
- if ($this->isPasswordTooLong($raw)) {
+ if (\strlen($raw) > self::MAX_PASSWORD_LENGTH) {
throw new BadCredentialsException('Invalid password.');
}
if (\function_exists('sodium_crypto_pwhash_str')) {
- return \sodium_crypto_pwhash_str(
- $raw,
- \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
- \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
- );
+ return \sodium_crypto_pwhash_str($raw, $this->opsLimit, $this->memLimit);
}
if (\extension_loaded('libsodium')) {
- return \Sodium\crypto_pwhash_str(
- $raw,
- \Sodium\CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
- \Sodium\CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
- );
+ return \Sodium\crypto_pwhash_str($raw, $this->opsLimit, $this->memLimit);
}
throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.');
@@ -67,7 +80,7 @@ class SodiumPasswordEncoder extends BasePasswordEncoder implements SelfSaltingEn
*/
public function isPasswordValid($encoded, $raw, $salt)
{
- if ($this->isPasswordTooLong($raw)) {
+ if (\strlen($raw) > self::MAX_PASSWORD_LENGTH) {
return false;
}
diff --git a/src/Symfony/Component/Security/Core/Tests/Encoder/NativePasswordEncoderTest.php b/src/Symfony/Component/Security/Core/Tests/Encoder/NativePasswordEncoderTest.php
new file mode 100644
index 0000000000..681b91a1ee
--- /dev/null
+++ b/src/Symfony/Component/Security/Core/Tests/Encoder/NativePasswordEncoderTest.php
@@ -0,0 +1,70 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Security\Core\Tests\Encoder;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
+
+/**
+ * @author Elnur Abdurrakhimov
+ */
+class NativePasswordEncoderTest extends TestCase
+{
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testCostBelowRange()
+ {
+ new NativePasswordEncoder(null, null, 3);
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testCostAboveRange()
+ {
+ new NativePasswordEncoder(null, null, 32);
+ }
+
+ /**
+ * @dataProvider validRangeData
+ */
+ public function testCostInRange($cost)
+ {
+ $this->assertInstanceOf(NativePasswordEncoder::class, new NativePasswordEncoder(null, null, $cost));
+ }
+
+ public function validRangeData()
+ {
+ $costs = range(4, 31);
+ array_walk($costs, function (&$cost) { $cost = [$cost]; });
+
+ return $costs;
+ }
+
+ public function testValidation()
+ {
+ $encoder = new NativePasswordEncoder();
+ $result = $encoder->encodePassword('password', null);
+ $this->assertTrue($encoder->isPasswordValid($result, 'password', null));
+ $this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword', null));
+ }
+
+ public function testCheckPasswordLength()
+ {
+ $encoder = new NativePasswordEncoder(null, null, 4);
+ $result = password_hash(str_repeat('a', 72), PASSWORD_BCRYPT, ['cost' => 4]);
+
+ $this->assertFalse($encoder->isPasswordValid($result, str_repeat('a', 73), 'salt'));
+ $this->assertTrue($encoder->isPasswordValid($result, str_repeat('a', 72), 'salt'));
+ }
+}