PasswordPwnedApiValidator   A
last analyzed

Complexity

Total Complexity 11

Size/Duplication

Total Lines 87
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 11
eloc 35
dl 0
loc 87
rs 10
c 0
b 0
f 0

3 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 3
A validate() 0 16 4
A getBlacklistPasswords() 0 24 4
1
<?php
2
declare(strict_types=1);
3
4
namespace Porthou\Password\Validators;
5
6
use Generator;
7
use Http\Client\HttpClient;
8
use Http\Discovery\HttpClientDiscovery;
9
use Http\Discovery\MessageFactoryDiscovery;
10
use Http\Message\MessageFactory;
11
use Porthou\Password\PasswordException;
12
use Porthou\Password\Validator;
13
14
class PasswordPwnedApiValidator implements Validator
15
{
16
    /** @var int $minimumThreshold */
17
    private $minimumThreshold;
18
19
    /** @var HttpClient */
20
    private $httpClient;
21
22
    /** @var MessageFactory */
23
    private $messageFactory;
24
25
    /**
26
     * PasswordPwnedApiValidator constructor.
27
     *
28
     * @param int $minimumThreshold How many times a password must appear before we consider it invalid
29
     *
30
     * @param HttpClient|null $httpClient The HTTP Client to use, or null for automatic discovery
31
     * @param MessageFactory|null $messageFactory The Message Factory to use or null for automatic discovery
32
     *
33
     * @see https://haveibeenpwned.com/API/v2 for information on the API.
34
     */
35
    public function __construct(int $minimumThreshold = 50, HttpClient $httpClient = null, MessageFactory $messageFactory = null)
36
    {
37
        $this->minimumThreshold = $minimumThreshold;
38
39
        if ($httpClient === null) {
40
            $this->httpClient = HttpClientDiscovery::find();
41
        } else {
42
            $this->httpClient = $httpClient;
43
        }
44
45
        if ($messageFactory === null) {
46
            $this->messageFactory = MessageFactoryDiscovery::find();
47
        } else {
48
            $this->messageFactory = $messageFactory;
49
        }
50
    }
51
52
    /** {@inheritdoc} */
53
    public function validate(string $password): bool
54
    {
55
        $passwordHash = sha1($password);
56
        $prefix = substr($passwordHash, 0, 5);
57
58
        foreach ($this->getBlacklistPasswords($prefix) as $badPassword) {
59
            [$badHash, $count] = $badPassword;
60
            if (
61
                $passwordHash === $prefix . $badHash
62
                && $count >= $this->minimumThreshold
63
            ) {
64
                throw new PasswordException('Password has been pwned.');
65
            }
66
        }
67
68
        return true;
69
    }
70
71
    /**
72
     * Iterates over and yields each blacklisted password
73
     *
74
     * @param string $partHash The first 5 characters of the sha1'd password
75
     * @return Generator
76
     */
77
    private function getBlacklistPasswords(string $partHash): Generator
78
    {
79
        try {
80
            $response = $this->httpClient->sendRequest(
81
                $this->messageFactory->createRequest(
82
                    'GET',
83
                    'https://api.pwnedpasswords.com/range/' . $partHash
84
                )
85
            );
86
        } catch (\Http\Client\Exception $e) {
87
            return;
88
        }
89
90
        $bodyStream = $response->getBody()->detach();
91
92
        if ($bodyStream === null) {
93
            return;
94
        }
95
96
        while (($password = fgets($bodyStream)) !== false) {
97
            yield explode(':', trim((string)$password));
98
        }
99
100
        fclose($bodyStream);
101
    }
102
}
103