-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
/
NotCompromisedPasswordValidator.php
105 lines (87 loc) · 3.38 KB
/
NotCompromisedPasswordValidator.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Validator\Constraints;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* Checks if a password has been leaked in a data breach using haveibeenpwned.com's API.
* Use a k-anonymity model to protect the password being searched for.
*
* @see https://haveibeenpwned.com/API/v2#SearchingPwnedPasswordsByRange
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class NotCompromisedPasswordValidator extends ConstraintValidator
{
private const DEFAULT_API_ENDPOINT = 'https://api.pwnedpasswords.com/range/%s';
private $httpClient;
private $charset;
private $enabled;
private $endpoint;
public function __construct(HttpClientInterface $httpClient = null, string $charset = 'UTF-8', bool $enabled = true, string $endpoint = null)
{
if (null === $httpClient && !class_exists(HttpClient::class)) {
throw new \LogicException(sprintf('The "%s" class requires the "HttpClient" component. Try running "composer require symfony/http-client".', self::class));
}
$this->httpClient = $httpClient ?? HttpClient::create();
$this->charset = $charset;
$this->enabled = $enabled;
$this->endpoint = $endpoint ?? self::DEFAULT_API_ENDPOINT;
}
/**
* {@inheritdoc}
*
* @throws ExceptionInterface
*/
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof NotCompromisedPassword) {
throw new UnexpectedTypeException($constraint, NotCompromisedPassword::class);
}
if (!$this->enabled) {
return;
}
if (null !== $value && !is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) {
throw new UnexpectedValueException($value, 'string');
}
$value = (string) $value;
if ('' === $value) {
return;
}
if ('UTF-8' !== $this->charset) {
$value = mb_convert_encoding($value, 'UTF-8', $this->charset);
}
$hash = strtoupper(sha1($value));
$hashPrefix = substr($hash, 0, 5);
$url = sprintf($this->endpoint, $hashPrefix);
try {
$result = $this->httpClient->request('GET', $url)->getContent();
} catch (ExceptionInterface $e) {
if ($constraint->skipOnError) {
return;
}
throw $e;
}
foreach (explode("\r\n", $result) as $line) {
[$hashSuffix, $count] = explode(':', $line);
if ($hashPrefix.$hashSuffix === $hash && $constraint->threshold <= (int) $count) {
$this->context->buildViolation($constraint->message)
->setCode(NotCompromisedPassword::COMPROMISED_PASSWORD_ERROR)
->addViolation();
return;
}
}
}
}