NativePasswordEncoder.php 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Security\Core\Encoder;
  11. use Symfony\Component\Security\Core\Exception\BadCredentialsException;
  12. /**
  13. * Hashes passwords using password_hash().
  14. *
  15. * @author Elnur Abdurrakhimov <elnur@elnur.pro>
  16. * @author Terje Bråten <terje@braten.be>
  17. * @author Nicolas Grekas <p@tchwork.com>
  18. */
  19. final class NativePasswordEncoder implements PasswordEncoderInterface, SelfSaltingEncoderInterface
  20. {
  21. private const MAX_PASSWORD_LENGTH = 4096;
  22. private $algo = \PASSWORD_BCRYPT;
  23. private $options;
  24. /**
  25. * @param string|null $algo An algorithm supported by password_hash() or null to use the stronger available algorithm
  26. */
  27. public function __construct(int $opsLimit = null, int $memLimit = null, int $cost = null, string $algo = null)
  28. {
  29. $cost = $cost ?? 13;
  30. $opsLimit = $opsLimit ?? max(4, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE : 4);
  31. $memLimit = $memLimit ?? max(64 * 1024 * 1024, \defined('SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE : 64 * 1024 * 1024);
  32. if (3 > $opsLimit) {
  33. throw new \InvalidArgumentException('$opsLimit must be 3 or greater.');
  34. }
  35. if (10 * 1024 > $memLimit) {
  36. throw new \InvalidArgumentException('$memLimit must be 10k or greater.');
  37. }
  38. if ($cost < 4 || 31 < $cost) {
  39. throw new \InvalidArgumentException('$cost must be in the range of 4-31.');
  40. }
  41. $algos = [1 => \PASSWORD_BCRYPT, '2y' => \PASSWORD_BCRYPT];
  42. if (\defined('PASSWORD_ARGON2I')) {
  43. $this->algo = $algos[2] = $algos['argon2i'] = (string) \PASSWORD_ARGON2I;
  44. }
  45. if (\defined('PASSWORD_ARGON2ID')) {
  46. $this->algo = $algos[3] = $algos['argon2id'] = (string) \PASSWORD_ARGON2ID;
  47. }
  48. if (null !== $algo) {
  49. $this->algo = $algos[$algo] ?? $algo;
  50. }
  51. $this->options = [
  52. 'cost' => $cost,
  53. 'time_cost' => $opsLimit,
  54. 'memory_cost' => $memLimit >> 10,
  55. 'threads' => 1,
  56. ];
  57. }
  58. /**
  59. * {@inheritdoc}
  60. */
  61. public function encodePassword(string $raw, ?string $salt): string
  62. {
  63. if (\strlen($raw) > self::MAX_PASSWORD_LENGTH || ((string) \PASSWORD_BCRYPT === $this->algo && 72 < \strlen($raw))) {
  64. throw new BadCredentialsException('Invalid password.');
  65. }
  66. // Ignore $salt, the auto-generated one is always the best
  67. return password_hash($raw, $this->algo, $this->options);
  68. }
  69. /**
  70. * {@inheritdoc}
  71. */
  72. public function isPasswordValid(string $encoded, string $raw, ?string $salt): bool
  73. {
  74. if ('' === $raw) {
  75. return false;
  76. }
  77. if (\strlen($raw) > self::MAX_PASSWORD_LENGTH) {
  78. return false;
  79. }
  80. if (0 !== strpos($encoded, '$argon')) {
  81. // BCrypt encodes only the first 72 chars
  82. return (72 >= \strlen($raw) || 0 !== strpos($encoded, '$2')) && password_verify($raw, $encoded);
  83. }
  84. if (\extension_loaded('sodium') && version_compare(\SODIUM_LIBRARY_VERSION, '1.0.14', '>=')) {
  85. return sodium_crypto_pwhash_str_verify($encoded, $raw);
  86. }
  87. if (\extension_loaded('libsodium') && version_compare(phpversion('libsodium'), '1.0.14', '>=')) {
  88. return \Sodium\crypto_pwhash_str_verify($encoded, $raw);
  89. }
  90. return password_verify($raw, $encoded);
  91. }
  92. /**
  93. * {@inheritdoc}
  94. */
  95. public function needsRehash(string $encoded): bool
  96. {
  97. return password_needs_rehash($encoded, $this->algo, $this->options);
  98. }
  99. }