• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

avoutic / web-framework / 21881087493

10 Feb 2026 08:22PM UTC coverage: 73.637% (-0.02%) from 73.654%
21881087493

push

github

avoutic
Support empty hashes for blocked accounts

1 of 2 new or added lines in 1 file covered. (50.0%)

2067 of 2807 relevant lines covered (73.64%)

2.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.67
/src/Security/PasswordHashService.php
1
<?php
2

3
/*
4
 * This file is part of WebFramework.
5
 *
6
 * (c) Avoutic <avoutic@gmail.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
namespace WebFramework\Security;
13

14
/**
15
 * Class PasswordHashService.
16
 *
17
 * Handles password hashing and verification.
18
 */
19
class PasswordHashService
20
{
21
    /**
22
     * PasswordHashService constructor.
23
     *
24
     * @param RandomProvider $randomProvider The random provider service
25
     */
26
    public function __construct(
×
27
        private RandomProvider $randomProvider,
28
    ) {}
×
29

30
    /**
31
     * Implement the PBKDF2 key derivation function.
32
     *
33
     * @param string $algorithm The hash algorithm to use
34
     * @param string $password  The password to derive from
35
     * @param string $salt      The salt
36
     * @param int    $count     The iteration count
37
     * @param int    $keyLength The length of the derived key
38
     * @param bool   $rawOutput When set to true, outputs raw binary data. false outputs lowercase hexits.
39
     *
40
     * @return string A $keyLength-byte derived key
41
     */
42
    public function pbkdf2(string $algorithm, string $password, string $salt, int $count, int $keyLength, bool $rawOutput = false): string
4✔
43
    {
44
        $algorithm = strtolower($algorithm);
4✔
45
        if (!in_array($algorithm, hash_algos(), true))
4✔
46
        {
47
            exit('PBKDF2 ERROR: Invalid hash algorithm.');
×
48
        }
49
        if ($count <= 0 || $keyLength <= 0)
4✔
50
        {
51
            exit('PBKDF2 ERROR: Invalid parameters.');
×
52
        }
53

54
        $hashLength = strlen(hash($algorithm, '', true));
4✔
55
        $blockCount = ceil($keyLength / $hashLength);
4✔
56

57
        $output = '';
4✔
58
        for ($i = 1; $i <= $blockCount; $i++)
4✔
59
        {
60
            // $i encoded as 4 bytes, big endian.
61
            $last = $salt.pack('N', $i);
4✔
62
            // first iteration
63
            $last = $xorsum = hash_hmac($algorithm, $last, $password, true);
4✔
64
            // perform the other $count - 1 iterations
65
            for ($j = 1; $j < $count; $j++)
4✔
66
            {
67
                $xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true));
4✔
68
            }
69
            $output .= $xorsum;
4✔
70
        }
71

72
        if ($rawOutput)
4✔
73
        {
74
            return substr($output, 0, $keyLength);
×
75
        }
76

77
        return bin2hex(substr($output, 0, $keyLength));
4✔
78
    }
79

80
    /**
81
     * Generate a password hash.
82
     *
83
     * @param string $password The password to hash
84
     *
85
     * @return string The generated password hash
86
     */
87
    public function generateHash(string $password): string
2✔
88
    {
89
        $salt = base64_encode($this->randomProvider->getRandom(24));
2✔
90

91
        return 'sha256:1000:'.$salt.':'.
2✔
92
                $this->pbkdf2('sha256', $password, $salt, 1000, 24, false);
2✔
93
    }
94

95
    /**
96
     * Get the stored and calculated hashes for a password.
97
     *
98
     * @param string $passwordHash The stored password hash
99
     * @param string $password     The password to check
100
     *
101
     * @return array{stored: string, calculated: string} The stored and calculated hashes
102
     *
103
     * @throws \InvalidArgumentException If the hash format is invalid or unknown
104
     */
105
    private function getHashes(string $passwordHash, string $password): array
2✔
106
    {
107
        $params = explode(':', $passwordHash);
2✔
108

109
        if ($params[0] == 'sha256')
2✔
110
        {
111
            if (count($params) !== 4)
2✔
112
            {
113
                throw new \InvalidArgumentException('sha256 hash format mismatch');
×
114
            }
115

116
            return [
2✔
117
                'stored' => $params[3],
2✔
118
                'calculated' => $this->pbkdf2(
2✔
119
                    'sha256',
2✔
120
                    $password,
2✔
121
                    $params[2],
2✔
122
                    (int) $params[1],
2✔
123
                    (int) (strlen($params[3]) / 2),
2✔
124
                    false
2✔
125
                ),
2✔
126
            ];
2✔
127
        }
128

129
        if ($params[0] == 'bootstrap')
×
130
        {
131
            if (count($params) !== 2)
×
132
            {
133
                throw new \InvalidArgumentException('Bootstrap hash format mismatch');
×
134
            }
135

136
            return [
×
137
                'stored' => $params[1],
×
138
                'calculated' => $password,
×
139
            ];
×
140
        }
141

142
        if ($params[0] == 'dolphin')
×
143
        {
144
            if (count($params) !== 3)
×
145
            {
146
                throw new \InvalidArgumentException('Dolphin hash format mismatch');
×
147
            }
148

149
            return [
×
150
                'stored' => $params[2],
×
151
                'calculated' => sha1(md5($password).$params[1]),
×
152
            ];
×
153
        }
154

155
        throw new \InvalidArgumentException('Unknown password hash format');
×
156
    }
157

158
    /**
159
     * Check if a password matches a hash.
160
     *
161
     * @param string $passwordHash The stored password hash
162
     * @param string $password     The password to check
163
     *
164
     * @return bool True if the password matches, false otherwise
165
     */
166
    public function checkPassword(string $passwordHash, string $password): bool
2✔
167
    {
168
        // Empty hashes are used to indicate blocked accounts
169
        if ($passwordHash === '')
2✔
170
        {
171
            // No need for constant time comparison
NEW
172
            return false;
×
173
        }
174

175
        $hashed = $this->getHashes($passwordHash, $password);
2✔
176

177
        // Slow compare (time-constant)
178
        $diff = strlen($hashed['stored']) ^ strlen($hashed['calculated']);
2✔
179
        for ($i = 0; $i < strlen($hashed['stored']) && $i < strlen($hashed['calculated']); $i++)
2✔
180
        {
181
            $diff |= ord($hashed['stored'][$i]) ^ ord($hashed['calculated'][$i]);
2✔
182
        }
183

184
        return ($diff === 0);
2✔
185
    }
186

187
    /**
188
     * Check if a password hash should be migrated to a newer format.
189
     *
190
     * @param string $passwordHash The stored password hash
191
     *
192
     * @return bool True if the hash should be migrated, false otherwise
193
     *
194
     * @throws \InvalidArgumentException If the hash format is unknown
195
     */
196
    public function shouldMigrate(string $passwordHash): bool
2✔
197
    {
198
        $params = explode(':', $passwordHash);
2✔
199

200
        if ($params[0] == 'sha256')
2✔
201
        {
202
            return false;
1✔
203
        }
204

205
        if ($params[0] == 'bootstrap')
1✔
206
        {
207
            return true;
×
208
        }
209

210
        if ($params[0] == 'dolphin')
1✔
211
        {
212
            return true;
×
213
        }
214

215
        throw new \InvalidArgumentException('Unknown password hash format');
1✔
216
    }
217
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc