vendor/symfony/validator/Constraints/NotCompromisedPasswordValidator.php line 39

  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\Validator\Constraints;
  11. use Symfony\Component\HttpClient\HttpClient;
  12. use Symfony\Component\Validator\Constraint;
  13. use Symfony\Component\Validator\ConstraintValidator;
  14. use Symfony\Component\Validator\Exception\UnexpectedTypeException;
  15. use Symfony\Component\Validator\Exception\UnexpectedValueException;
  16. use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
  17. use Symfony\Contracts\HttpClient\HttpClientInterface;
  18. /**
  19.  * Checks if a password has been leaked in a data breach using haveibeenpwned.com's API.
  20.  * Use a k-anonymity model to protect the password being searched for.
  21.  *
  22.  * @see https://haveibeenpwned.com/API/v2#SearchingPwnedPasswordsByRange
  23.  *
  24.  * @author Kévin Dunglas <dunglas@gmail.com>
  25.  */
  26. class NotCompromisedPasswordValidator extends ConstraintValidator
  27. {
  28.     private const DEFAULT_API_ENDPOINT 'https://api.pwnedpasswords.com/range/%s';
  29.     private HttpClientInterface $httpClient;
  30.     private string $charset;
  31.     private bool $enabled;
  32.     private string $endpoint;
  33.     public function __construct(HttpClientInterface $httpClient nullstring $charset 'UTF-8'bool $enabled truestring $endpoint null)
  34.     {
  35.         if (null === $httpClient && !class_exists(HttpClient::class)) {
  36.             throw new \LogicException(sprintf('The "%s" class requires the "HttpClient" component. Try running "composer require symfony/http-client".'self::class));
  37.         }
  38.         $this->httpClient $httpClient ?? HttpClient::create();
  39.         $this->charset $charset;
  40.         $this->enabled $enabled;
  41.         $this->endpoint $endpoint ?? self::DEFAULT_API_ENDPOINT;
  42.     }
  43.     /**
  44.      * @throws ExceptionInterface
  45.      */
  46.     public function validate(mixed $valueConstraint $constraint)
  47.     {
  48.         if (!$constraint instanceof NotCompromisedPassword) {
  49.             throw new UnexpectedTypeException($constraintNotCompromisedPassword::class);
  50.         }
  51.         if (!$this->enabled) {
  52.             return;
  53.         }
  54.         if (null !== $value && !\is_scalar($value) && !$value instanceof \Stringable) {
  55.             throw new UnexpectedValueException($value'string');
  56.         }
  57.         $value = (string) $value;
  58.         if ('' === $value) {
  59.             return;
  60.         }
  61.         if ('UTF-8' !== $this->charset) {
  62.             $value mb_convert_encoding($value'UTF-8'$this->charset);
  63.         }
  64.         $hash strtoupper(sha1($value));
  65.         $hashPrefix substr($hash05);
  66.         $url sprintf($this->endpoint$hashPrefix);
  67.         try {
  68.             $result $this->httpClient->request('GET'$url, ['headers' => ['Add-Padding' => 'true']])->getContent();
  69.         } catch (ExceptionInterface $e) {
  70.             if ($constraint->skipOnError) {
  71.                 return;
  72.             }
  73.             throw $e;
  74.         }
  75.         foreach (explode("\r\n"$result) as $line) {
  76.             if (!str_contains($line':')) {
  77.                 continue;
  78.             }
  79.             [$hashSuffix$count] = explode(':'$line);
  80.             if ($hashPrefix.$hashSuffix === $hash && $constraint->threshold <= (int) $count) {
  81.                 $this->context->buildViolation($constraint->message)
  82.                     ->setCode(NotCompromisedPassword::COMPROMISED_PASSWORD_ERROR)
  83.                     ->addViolation();
  84.                 return;
  85.             }
  86.         }
  87.     }
  88. }