PasswordHasher   A
last analyzed

Complexity

Total Complexity 28

Size/Duplication

Total Lines 332
Duplicated Lines 0 %

Test Coverage

Coverage 98.45%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 28
eloc 124
c 5
b 0
f 0
dl 0
loc 332
ccs 127
cts 129
cp 0.9845
rs 10

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 4
A verifyHashedPassword() 0 24 6
A hashPasswordV2() 0 21 1
A verifyWithV2() 0 10 3
A verifyWithV3() 0 11 3
A verifyHashedPasswordV2() 0 27 2
A verifyHashedPasswordV3() 0 35 3
A hashPassword() 0 10 3
A writeNetworkByteOrder() 0 6 1
A readNetworkByteOrder() 0 6 1
A hashPasswordV3() 0 28 1
1
<?php
2
3
namespace MDHearing\AspNetCore\Identity;
4
5
use InvalidArgumentException;
6
7
/**
8
 * Implements the standard Identity password hashing.
9
 */
10
class PasswordHasher implements IPasswordHasher
11
{
12
    /* =======================
13
     * HASHED PASSWORD FORMATS
14
     * =======================
15
     *
16
     * Version 2:
17
     * PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
18
     * (See also: SDL crypto guidelines v5.1, Part III)
19
     * Format: { 0x00, salt, subkey }
20
     *
21
     * Version 3:
22
     * PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
23
     * Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
24
     * (All UInt32s are stored big-endian.)
25
     */
26
27
    /**
28
     * Compatibility mode
29
     * @var integer
30
     */
31
    private $compatibilityMode;
32
33
    /**
34
     * Iteration count
35
     * @var integer
36
     */
37
    private $iterCount;
38
39
    /**
40
      * Creates a new instance of password hasher.
41
      *
42
      * @param $optionsAccessor The options for this instance.
0 ignored issues
show
Bug introduced by
The type MDHearing\AspNetCore\Identity\The was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
43
      */
44 42
    public function __construct(
45
        $compatibilityMode = PasswordHasherCompatibilityMode::IDENTITY_V3,
46
        $iterationsCount = 10000
47
    ) {
48 42
        $this->compatibilityMode = $compatibilityMode;
49 42
        switch ($this->compatibilityMode) {
50 42
            case PasswordHasherCompatibilityMode::IDENTITY_V2:
51
                // nothing else to do
52 12
                break;
53
54 33
            case PasswordHasherCompatibilityMode::IDENTITY_V3:
55 30
                $this->iterCount = $iterationsCount;
56 30
                if ($this->iterCount < 1) {
57 3
                    throw new InvalidArgumentException('Invalid password hasher iteration count.');
58
                }
59 27
                break;
60
61
            default:
62 3
                throw new InvalidArgumentException('Invalid password hasher compatibility mode.');
63
        }
64 36
    }
65
66
    /**
67
      * Returns a hashed representation of the supplied <paramref name="password"/>
68
      * for the specified <paramref name="user"/>.
69
      *
70
      * @param $password The password to hash.
71
      *
72
      * @returns A hashed representation of the supplied password for the specified user.
73
      */
74 21
    public function hashPassword($password)
75
    {
76 21
        if (is_null($password)) {
77 3
            throw new InvalidArgumentException('Password cannot be null');
78
        }
79
80 18
        if ($this->compatibilityMode == PasswordHasherCompatibilityMode::IDENTITY_V2) {
81 12
            return base64_encode(static::hashPasswordV2($password));
82
        } else {
83 6
            return base64_encode($this->hashPasswordV3($password));
84
        }
85
    }
86
87
    /**
88
     * Creates a hashed password using version 2 hashing algorithm.
89
     *
90
     * @param  string $password
91
     * @return string
92
     */
93 12
    private static function hashPasswordV2($password)
94
    {
95 12
        $pbkdf2Prf = KeyDerivationPrf::HMACSHA1; // default for Rfc2898DeriveBytes
96 12
        $pbkdf2IterCount = 1000; // default for Rfc2898DeriveBytes
97 12
        $pbkdf2SubkeyLength = intdiv(256, 8); // 256 bits
98 12
        $saltSize = intdiv(128, 8); // 128 bits
99
100
        // Produce a version 2 (see comment above) text hash.
101 12
        $salt = random_bytes($saltSize);
102 12
        $subkey = hash_pbkdf2(
103 12
            KeyDerivationPrf::ALGO_NAME[$pbkdf2Prf],
104 8
            $password,
105 8
            $salt,
106 8
            $pbkdf2IterCount,
107 8
            $pbkdf2SubkeyLength,
108 12
            true
109
        );
110
111 12
        $outputBytes = chr(0) . $salt . $subkey;
112
113 12
        return $outputBytes;
114
    }
115
116
    /**
117
     * Creates a hashed password using version 3 hashing algorithm.
118
     *
119
     * @param  string $password
120
     * @return string
121
     */
122 6
    private function hashPasswordV3($password)
123
    {
124 6
        $prf = KeyDerivationPrf::HMACSHA256;
125 6
        $iterCount = $this->iterCount;
126 6
        $saltSize = intdiv(128, 8);
127 6
        $numBytesRequested = intdiv(256, 8);
128
129
        // Produce a version 3 (see comment above) text hash.
130 6
        $salt = random_bytes($saltSize);
131 6
        $subkey = hash_pbkdf2(
132 6
            KeyDerivationPrf::ALGO_NAME[$prf],
133 4
            $password,
134 4
            $salt,
135 4
            $iterCount,
136 4
            $numBytesRequested,
137 6
            true
138
        );
139
140 6
        $outputBytes = '';
141 6
        $outputBytes[0] = chr(0x01); // format marker
142 6
        static::WriteNetworkByteOrder($outputBytes, 1, $prf);
143 6
        static::WriteNetworkByteOrder($outputBytes, 5, $iterCount);
144 6
        static::WriteNetworkByteOrder($outputBytes, 9, $saltSize);
145
146 6
        $outputBytes .= $salt;
147 6
        $outputBytes .= $subkey;
148
149 6
        return $outputBytes;
150
    }
151
152
    /**
153
      * Returns a PasswordVerificationResult indicating the result of a password hash comparison.
154
      *
155
      * @param $hashedPassword The hash value for a user's stored password.
156
      * @param $providedPassword The password supplied for comparison.
157
      *
158
      * @returns A PasswordVerificationResult indicating the result of a password hash comparison.
159
      *
160
      * Implementations of this method should be time consistent.
161
      */
162 33
    public function verifyHashedPassword($hashedPassword, $providedPassword)
163
    {
164 33
        if (is_null($hashedPassword)) {
165 3
            throw new InvalidArgumentException('hashedPassword is null');
166
        }
167
168 30
        if (is_null($providedPassword)) {
169 3
            throw new InvalidArgumentException('providedPassword is null');
170
        }
171
172 27
        $decodedHashedPassword = base64_decode($hashedPassword);
173
174
        // Read the format marker from the hashed password
175 27
        if (strlen($decodedHashedPassword) == 0) {
176 3
            return PasswordVerificationResult::FAILED;
177
        }
178
179 24
        switch (ord($decodedHashedPassword[0])) {
180 24
            case 0x00:
181 12
                return $this->verifyWithV2($decodedHashedPassword, $providedPassword);
182 12
            case 0x01:
183 9
                return $this->verifyWithV3($decodedHashedPassword, $providedPassword);
184
            default:
185 3
                return PasswordVerificationResult::FAILED; // unknown format marker
186
        }
187
    }
188
189
    /**
190
     * Performs verification using version 2 password hashing scheme.
191
     *
192
     * @param  string $decodedHashedPassword
193
     * @param  string $providedPassword
194
     * @return integer
195
     */
196 12
    private function verifyWithV2($decodedHashedPassword, $providedPassword)
197
    {
198 12
        if (static::verifyHashedPasswordV2($decodedHashedPassword, $providedPassword)) {
199
            // This is an old password hash format - the caller needs to
200
            // rehash if we're not running in an older compat mode.
201 6
            return ($this->compatibilityMode == PasswordHasherCompatibilityMode::IDENTITY_V3)
202 3
                ? PasswordVerificationResult::SUCCESS_REHASH_NEEDED
203 6
                : PasswordVerificationResult::SUCCESS;
204
        } else {
205 6
            return PasswordVerificationResult::FAILED;
206
        }
207
    }
208
209
    /**
210
     * Performs verification using version 3 password hashing scheme.
211
     *
212
     * @param  string $decodedHashedPassword
213
     * @param  string $providedPassword
214
     * @return integer
215
     */
216 9
    private function verifyWithV3($decodedHashedPassword, $providedPassword)
217
    {
218 9
        $embeddedIterCount = null;
219
220 9
        if (static::verifyHashedPasswordV3($decodedHashedPassword, $providedPassword, $embeddedIterCount)) {
221
            // If this hasher was configured with a higher iteration count, change the entry now.
222 9
            return ($embeddedIterCount < $this->iterCount)
223 3
                ? PasswordVerificationResult::SUCCESS_REHASH_NEEDED
224 9
                : PasswordVerificationResult::SUCCESS;
225
        } else {
226 3
            return PasswordVerificationResult::FAILED;
227
        }
228
    }
229
230
    /**
231
     * Attempts to verify the given password against the given hashed password
232
     * using version 2 password hashing scheme.
233
     *
234
     * @param  string $hashedPassword
235
     * @param  string $password
236
     * @return boolean
237
     */
238 12
    private static function verifyHashedPasswordV2($hashedPassword, $password)
239
    {
240 12
        $pbkdf2Prf = KeyDerivationPrf::HMACSHA1; // default for Rfc2898DeriveBytes
241 12
        $pbkdf2IterCount = 1000; // default for Rfc2898DeriveBytes
242 12
        $pbkdf2SubkeyLength = intdiv(256, 8); // 256 bits
243 12
        $saltSize = intdiv(128, 8); // 128 bits
244
245
        // We know ahead of time the exact length of a valid hashed password payload.
246 12
        if (strlen($hashedPassword) != 1 + $saltSize + $pbkdf2SubkeyLength) {
247 3
            return false; // bad size
248
        }
249
250 9
        $salt = substr($hashedPassword, 1, $saltSize);
251
252 9
        $expectedSubkey = substr($hashedPassword, 1 + $saltSize, $pbkdf2SubkeyLength);
253
254
        // Hash the incoming password and verify it
255 9
        $actualSubkey = hash_pbkdf2(
256 9
            KeyDerivationPrf::ALGO_NAME[$pbkdf2Prf],
257 6
            $password,
258 6
            $salt,
259 6
            $pbkdf2IterCount,
260 6
            $pbkdf2SubkeyLength,
261 9
            true
262
        );
263
264 9
        return $actualSubkey === $expectedSubkey;
265
    }
266
267
    /**
268
     * Attempts to verify the given password against the given hashed password
269
     * using version 3 password hashing scheme.
270
     *
271
     * @param  string  $hashedPassword
272
     * @param  string  $password
273
     * @param  integer &$iterCount
274
     * @return boolean
275
     */
276 9
    private static function verifyHashedPasswordV3($hashedPassword, $password, &$iterCount)
277
    {
278 9
        $iterCount = 0;
279
280
        // Read header information
281 9
        $prf = static::readNetworkByteOrder($hashedPassword, 1);
282 9
        $iterCount = static::readNetworkByteOrder($hashedPassword, 5);
283 9
        $saltLength = static::readNetworkByteOrder($hashedPassword, 9);
284
285
        // Read the salt: must be >= 128 bits
286 9
        if ($saltLength < intdiv(128, 8)) {
287
            return false;
288
        }
289
290 9
        $salt = substr($hashedPassword, 13, $saltLength);
291
292
        // Read the subkey (the rest of the payload): must be >= 128 bits
293 9
        $subkeyLength = strlen($hashedPassword) - 13 - strlen($salt);
294 9
        if ($subkeyLength < intdiv(128, 8)) {
295
            return false;
296
        }
297
298 9
        $expectedSubkey = substr($hashedPassword, 13 + strlen($salt), $subkeyLength);
299
300
        // Hash the incoming password and verify it
301 9
        $actualSubkey = hash_pbkdf2(
302 9
            KeyDerivationPrf::ALGO_NAME[$prf],
303 6
            $password,
304 6
            $salt,
305 6
            $iterCount,
306 6
            $subkeyLength,
307 9
            true
308
        );
309
310 9
        return $actualSubkey === $expectedSubkey;
311
    }
312
313
    /**
314
     * Updates the given buffer to include network byte order.
315
     *
316
     * @param  string  &$buffer
317
     * @param  integer $offset
318
     * @param  integer $value
319
     * @return void
320
     */
321 6
    private static function writeNetworkByteOrder(&$buffer, $offset, $value)
322
    {
323 6
        $buffer[$offset] = chr($value >> 24);
324 6
        $buffer[$offset + 1] = chr(($value >> 16) & 0xFF);
325 6
        $buffer[$offset + 2] = chr(($value >> 8) & 0xFF);
326 6
        $buffer[$offset + 3] = chr($value & 0xFF);
327 6
    }
328
329
    /**
330
     * Reads and returns the network byte order stored in the given buffer.
331
     *
332
     * @param  string  $buffer
333
     * @param  integer $offset
334
     * @return integer
335
     */
336 9
    private static function readNetworkByteOrder($buffer, $offset)
337
    {
338 9
        return ord($buffer[$offset]) << 24
339 9
            | ord($buffer[$offset + 1]) << 16
340 9
            | ord($buffer[$offset + 2]) << 8
341 9
            | ord($buffer[$offset + 3]);
342
    }
343
}
344