Completed
Push — master ( 895118...cbd892 )
by Steven
03:07 queued 02:03
created

PasswordHasher::verifyWithV2()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 6
c 1
b 0
f 0
dl 0
loc 10
ccs 0
cts 6
cp 0
rs 10
cc 3
nc 3
nop 2
crap 12
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
    private $compatibilityMode;
28
    private $iterCount;
29
30
    /**
31
      * Creates a new instance of <see cref="PasswordHasher{TUser}"/>.
32
      *
33
      * @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...
34
      */
35 3
    public function __construct(
36
        $compatibilityMode = PasswordHasherCompatibilityMode::IDENTITYV3,
37
        $iterationsCount = 10000
38
    ) {
39 3
        $this->compatibilityMode = $compatibilityMode;
40 3
        switch ($this->compatibilityMode) {
41 3
            case PasswordHasherCompatibilityMode::IDENTITYV2:
42
                // nothing else to do
43
                break;
44
45 3
            case PasswordHasherCompatibilityMode::IDENTITYV3:
46 3
                $this->iterCount = $iterationsCount;
47 3
                if ($this->iterCount < 1) {
48
                    throw new InvalidArgumentException('Invalid password hasher iteration count.');
49
                }
50 3
                break;
51
52
            default:
53
                throw new InvalidArgumentException('Invalid password hasher compatibility mode.');
54
        }
55 3
    }
56
57
    /**
58
      * Returns a hashed representation of the supplied <paramref name="password"/>
59
      * for the specified <paramref name="user"/>.
60
      *
61
      * @param $password The password to hash.
62
      *
63
      * @returns A hashed representation of the supplied password for the specified user.
64
      */
65
    public function hashPassword($password)
66
    {
67
        if ($password == null) {
68
            throw new ArgumentNullException('password');
0 ignored issues
show
Bug introduced by
The type MDHearing\AspNetCore\Ide...y\ArgumentNullException 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...
69
        }
70
71
        if ($this->compatibilityMode == PasswordHasherCompatibilityMode::IDENTITYV2) {
72
            return base64_encode(static::hashPasswordV2($password));
73
        } else {
74
            return base64_encode($this->hashPasswordV3($password));
75
        }
76
    }
77
78
    private static function hashPasswordV2($password)
79
    {
80
        $Pbkdf2Prf = KeyDerivationPrf::HMACSHA1; // default for Rfc2898DeriveBytes
81
        $Pbkdf2IterCount = 1000; // default for Rfc2898DeriveBytes
82
        $Pbkdf2SubkeyLength = intdiv(256, 8); // 256 bits
83
        $SaltSize = intdiv(128, 8); // 128 bits
84
85
        // Produce a version 2 (see comment above) text hash.
86
        $salt = random_bytes($SaltSize);
87
        $subkey = hash_pbkdf2(
88
            KeyDerivationPrf::ALGO_NAME[$Pbkdf2Prf],
89
            $password,
90
            $salt,
91
            $Pbkdf2IterCount,
92
            $Pbkdf2SubkeyLength,
93
            true
94
        );
95
96
        $outputBytes = chr(0) . $salt . $subkey;
97
98
        return $outputBytes;
99
    }
100
101
    private function hashPasswordV3($password)
102
    {
103
        $prf = KeyDerivationPrf::HMACSHA256;
104
        $iterCount = $this->iterCount;
105
        $saltSize = intdiv(128, 8);
106
        $numBytesRequested = intdiv(256, 8);
107
108
        // Produce a version 3 (see comment above) text hash.
109
        $salt = random_bytes($saltSize);
110
        $subkey = hash_pbkdf2(
111
            KeyDerivationPrf::ALGO_NAME[$prf],
112
            $password,
113
            $salt,
114
            $iterCount,
115
            $numBytesRequested,
116
            true
117
        );
118
119
        $outputBytes = '';
120
        $outputBytes[0] = chr(0x01); // format marker
121
        static::WriteNetworkByteOrder($outputBytes, 1, $prf);
122
        static::WriteNetworkByteOrder($outputBytes, 5, $iterCount);
123
        static::WriteNetworkByteOrder($outputBytes, 9, $saltSize);
124
125
        $outputBytes .= $salt;
126
        $outputBytes .= $subkey;
127
128
        return $outputBytes;
129
    }
130
131
    /**
132
      * Returns a PasswordVerificationResult indicating the result of a password hash comparison.
133
      *
134
      * @param $hashedPassword The hash value for a user's stored password.
135
      * @param $providedPassword The password supplied for comparison.
136
      *
137
      * @returns A PasswordVerificationResult indicating the result of a password hash comparison.
138
      *
139
      * Implementations of this method should be time consistent.
140
      */
141 3
    public function verifyHashedPassword($hashedPassword, $providedPassword)
142
    {
143 3
        if ($hashedPassword == null) {
144
            throw new InvalidArgumentException('hashedPassword is null');
145
        }
146
147 3
        if ($providedPassword == null) {
148
            throw new InvalidArgumentException('providedPassword is null');
149
        }
150
151 3
        $decodedHashedPassword = base64_decode($hashedPassword);
152
153
        // read the format marker from the hashed password
154 3
        if (strlen($decodedHashedPassword) == 0) {
155
            return PasswordVerificationResult::FAILED;
156
        }
157
158 3
        switch (ord($decodedHashedPassword[0])) {
159 3
            case 0x00:
160
                return $this->verifyWithV2($decodedHashedPassword, $providedPassword);
161 3
            case 0x01:
162 3
                return $this->verifyWithV3($decodedHashedPassword, $providedPassword);
163
            default:
164
                return PasswordVerificationResult::FAILED; // unknown format marker
165
        }
166
    }
167
168
    /**
169
     * Performs verification using strategy version 2.
170
     *
171
     * @param  string $decodedHashedPassword
172
     * @param  string $providedPassword
173
     * @return integer
174
     */
175
    private function verifyWithV2($decodedHashedPassword, $providedPassword)
176
    {
177
        if (static::verifyHashedPasswordV2($decodedHashedPassword, $providedPassword)) {
178
            // This is an old password hash format - the caller needs to
179
            // rehash if we're not running in an older compat mode.
180
            return ($this->compatibilityMode == PasswordHasherCompatibilityMode::IDENTITYV3)
181
                ? PasswordVerificationResult::SUCCESS_REHASH_NEEDED
182
                : PasswordVerificationResult::SUCCESS;
183
        } else {
184
            return PasswordVerificationResult::FAILED;
185
        }
186
    }
187
188
    /**
189
     * Performs verification using strategy version 3.
190
     *
191
     * @param  string $decodedHashedPassword
192
     * @param  string $providedPassword
193
     * @return integer
194
     */
195 3
    private function verifyWithV3($decodedHashedPassword, $providedPassword)
196
    {
197 3
        $embeddedIterCount = null;
198
199 3
        if (static::verifyHashedPasswordV3($decodedHashedPassword, $providedPassword, $embeddedIterCount)) {
200
            // If this hasher was configured with a higher iteration count, change the entry now.
201 3
            return ($embeddedIterCount < $this->iterCount)
202
                ? PasswordVerificationResult::SUCCESS_REHASH_NEEDED
203 3
                : PasswordVerificationResult::SUCCESS;
204
        } else {
205 3
            return PasswordVerificationResult::FAILED;
206
        }
207
    }
208
209
    private static function verifyHashedPasswordV2($hashedPassword, $password)
210
    {
211
        $Pbkdf2Prf = KeyDerivationPrf::HMACSHA1; // default for Rfc2898DeriveBytes
212
        $Pbkdf2IterCount = 1000; // default for Rfc2898DeriveBytes
213
        $Pbkdf2SubkeyLength = intdiv(256, 8); // 256 bits
214
        $SaltSize = intdiv(128, 8); // 128 bits
215
216
        // We know ahead of time the exact length of a valid hashed password payload.
217
        if (strlen($hashedPassword) != 1 + $SaltSize + $Pbkdf2SubkeyLength) {
218
            return false; // bad size
219
        }
220
221
        $salt = substr($hashedPassword, 1, $SaltSize);
222
223
        $expectedSubkey = substr($hashedPassword, 1 + $SaltSize, $Pbkdf2SubkeyLength);
224
225
        // Hash the incoming password and verify it
226
        $actualSubkey = hash_pbkdf2(
227
            KeyDerivationPrf::ALGO_NAME[$Pbkdf2Prf],
228
            $password,
229
            $salt,
230
            $Pbkdf2IterCount,
231
            $Pbkdf2SubkeyLength,
232
            true
233
        );
234
235
        return $actualSubkey === $expectedSubkey;
236
    }
237
238 3
    private static function verifyHashedPasswordV3($hashedPassword, $password, &$iterCount)
239
    {
240 3
        $iterCount = 0;
241
242
        // Read header information
243 3
        $prf = static::readNetworkByteOrder($hashedPassword, 1);
244 3
        $iterCount = static::readNetworkByteOrder($hashedPassword, 5);
245 3
        $saltLength = static::readNetworkByteOrder($hashedPassword, 9);
246
247
        // Read the salt: must be >= 128 bits
248 3
        if ($saltLength < intdiv(128, 8)) {
249
            return false;
250
        }
251
252 3
        $salt = substr($hashedPassword, 13, $saltLength);
253
254
        // Read the subkey (the rest of the payload): must be >= 128 bits
255 3
        $subkeyLength = strlen($hashedPassword) - 13 - strlen($salt);
256 3
        if ($subkeyLength < intdiv(128, 8)) {
257
            return false;
258
        }
259
260 3
        $expectedSubkey = substr($hashedPassword, 13 + strlen($salt), $subkeyLength);
261
262
        // Hash the incoming password and verify it
263 3
        $actualSubkey = hash_pbkdf2(
264 3
            KeyDerivationPrf::ALGO_NAME[$prf],
265 1
            $password,
266 1
            $salt,
267 1
            $iterCount,
268 1
            $subkeyLength,
269 3
            true
270
        );
271
272 3
        return $actualSubkey === $expectedSubkey;
273
    }
274
275
    private static function writeNetworkByteOrder(&$buffer, $offset, $value)
276
    {
277
        $buffer[$offset] = chr($value >> 24);
278
        $buffer[$offset + 1] = chr(($value >> 16) & 0xFF);
279
        $buffer[$offset + 2] = chr(($value >> 8) & 0xFF);
280
        $buffer[$offset + 3] = chr($value & 0xFF);
281
    }
282
283 3
    private static function readNetworkByteOrder($buffer, $offset)
284
    {
285 3
        return ord($buffer[$offset]) << 24
286 3
            | ord($buffer[$offset + 1]) << 16
287 3
            | ord($buffer[$offset + 2]) << 8
288 3
            | ord($buffer[$offset + 3]);
289
    }
290
}
291