-
Notifications
You must be signed in to change notification settings - Fork 222
/
PasswordStorage.php
333 lines (303 loc) · 10.4 KB
/
PasswordStorage.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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
<?php
class InvalidHashException extends Exception {}
class CannotPerformOperationException extends Exception {}
class PasswordStorage
{
// These constants may be changed without breaking existing hashes.
const PBKDF2_HASH_ALGORITHM = "sha1";
const PBKDF2_ITERATIONS = 64000;
const PBKDF2_SALT_BYTES = 24;
const PBKDF2_OUTPUT_BYTES = 18;
// These constants define the encoding and may not be changed.
const HASH_SECTIONS = 5;
const HASH_ALGORITHM_INDEX = 0;
const HASH_ITERATION_INDEX = 1;
const HASH_SIZE_INDEX = 2;
const HASH_SALT_INDEX = 3;
const HASH_PBKDF2_INDEX = 4;
/**
* Hash a password with PBKDF2
*
* @param string $password
* @return string
*/
public static function create_hash($password)
{
// format: algorithm:iterations:outputSize:salt:pbkdf2output
if (!\is_string($password)) {
throw new InvalidArgumentException(
"create_hash(): Expected a string"
);
}
if (\function_exists('random_bytes')) {
try {
$salt_raw = \random_bytes(self::PBKDF2_SALT_BYTES);
} catch (Error $e) {
$salt_raw = false;
} catch (Exception $e) {
$salt_raw = false;
} catch (TypeError $e) {
$salt_raw = false;
}
} else {
$salt_raw = @\mcrypt_create_iv(self::PBKDF2_SALT_BYTES, MCRYPT_DEV_URANDOM);
}
if ($salt_raw === false) {
throw new CannotPerformOperationException(
"Random number generator failed. Not safe to proceed."
);
}
$PBKDF2_Output = self::pbkdf2(
self::PBKDF2_HASH_ALGORITHM,
$password,
$salt_raw,
self::PBKDF2_ITERATIONS,
self::PBKDF2_OUTPUT_BYTES,
true
);
return self::PBKDF2_HASH_ALGORITHM .
":" .
self::PBKDF2_ITERATIONS .
":" .
self::PBKDF2_OUTPUT_BYTES .
":" .
\base64_encode($salt_raw) .
":" .
\base64_encode($PBKDF2_Output);
}
/**
* Verify that a password matches the stored hash
*
* @param string $password
* @param string $hash
* @return bool
*/
public static function verify_password($password, $hash)
{
if (!\is_string($password) || !\is_string($hash)) {
throw new InvalidArgumentException(
"verify_password(): Expected two strings"
);
}
$params = \explode(":", $hash);
if (\count($params) !== self::HASH_SECTIONS) {
throw new InvalidHashException(
"Fields are missing from the password hash."
);
}
$pbkdf2 = \base64_decode($params[self::HASH_PBKDF2_INDEX], true);
if ($pbkdf2 === false) {
throw new InvalidHashException(
"Base64 decoding of pbkdf2 output failed."
);
}
$salt_raw = \base64_decode($params[self::HASH_SALT_INDEX], true);
if ($salt_raw === false) {
throw new InvalidHashException(
"Base64 decoding of salt failed."
);
}
$storedOutputSize = (int) $params[self::HASH_SIZE_INDEX];
if (self::ourStrlen($pbkdf2) !== $storedOutputSize) {
throw new InvalidHashException(
"PBKDF2 output length doesn't match stored output length."
);
}
$iterations = (int) $params[self::HASH_ITERATION_INDEX];
if ($iterations < 1) {
throw new InvalidHashException(
"Invalid number of iterations. Must be >= 1."
);
}
return self::slow_equals(
$pbkdf2,
self::pbkdf2(
$params[self::HASH_ALGORITHM_INDEX],
$password,
$salt_raw,
$iterations,
self::ourStrlen($pbkdf2),
true
)
);
}
/**
* Compares two strings $a and $b in length-constant time.
*
* @param string $a
* @param string $b
* @return bool
*/
public static function slow_equals($a, $b)
{
if (!\is_string($a) || !\is_string($b)) {
throw new InvalidArgumentException(
"slow_equals(): expected two strings"
);
}
if (\function_exists('hash_equals')) {
return \hash_equals($a, $b);
}
// PHP < 5.6 polyfill:
$diff = self::ourStrlen($a) ^ self::ourStrlen($b);
for($i = 0; $i < self::ourStrlen($a) && $i < self::ourStrlen($b); $i++) {
$diff |= \ord($a[$i]) ^ \ord($b[$i]);
}
return $diff === 0;
}
/*
* PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt
* $algorithm - The hash algorithm to use. Recommended: SHA256
* $password - The password.
* $salt - A salt that is unique to the password.
* $count - Iteration count. Higher is better, but slower. Recommended: At least 1000.
* $key_length - The length of the derived key in bytes.
* $raw_output - If true, the key is returned in raw binary format. Hex encoded otherwise.
* Returns: A $key_length-byte key derived from the password and salt.
*
* Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt
*
* This implementation of PBKDF2 was originally created by https://defuse.ca
* With improvements by http://www.variations-of-shadow.com
*/
public static function pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false)
{
// Type checks:
if (!\is_string($algorithm)) {
throw new InvalidArgumentException(
"pbkdf2(): algorithm must be a string"
);
}
if (!\is_string($password)) {
throw new InvalidArgumentException(
"pbkdf2(): password must be a string"
);
}
if (!\is_string($salt)) {
throw new InvalidArgumentException(
"pbkdf2(): salt must be a string"
);
}
// Coerce strings to integers with no information loss or overflow
$count += 0;
$key_length += 0;
$algorithm = \strtolower($algorithm);
if (!\in_array($algorithm, \hash_algos(), true)) {
throw new CannotPerformOperationException(
"Invalid or unsupported hash algorithm."
);
}
// Whitelist, or we could end up with people using CRC32.
$ok_algorithms = array(
"sha1", "sha224", "sha256", "sha384", "sha512",
"ripemd160", "ripemd256", "ripemd320", "whirlpool"
);
if (!\in_array($algorithm, $ok_algorithms, true)) {
throw new CannotPerformOperationException(
"Algorithm is not a secure cryptographic hash function."
);
}
if ($count <= 0 || $key_length <= 0) {
throw new CannotPerformOperationException(
"Invalid PBKDF2 parameters."
);
}
if (\function_exists("hash_pbkdf2")) {
// The output length is in NIBBLES (4-bits) if $raw_output is false!
if (!$raw_output) {
$key_length = $key_length * 2;
}
return \hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output);
}
$hash_length = self::ourStrlen(\hash($algorithm, "", true));
$block_count = \ceil($key_length / $hash_length);
$output = "";
for($i = 1; $i <= $block_count; $i++) {
// $i encoded as 4 bytes, big endian.
$last = $salt . \pack("N", $i);
// first iteration
$last = $xorsum = \hash_hmac($algorithm, $last, $password, true);
// perform the other $count - 1 iterations
for ($j = 1; $j < $count; $j++) {
$xorsum ^= ($last = \hash_hmac($algorithm, $last, $password, true));
}
$output .= $xorsum;
}
if($raw_output) {
return self::ourSubstr($output, 0, $key_length);
} else {
return \bin2hex(self::ourSubstr($output, 0, $key_length));
}
}
/*
* We need these strlen() and substr() functions because when
* 'mbstring.func_overload' is set in php.ini, the standard strlen() and
* substr() are replaced by mb_strlen() and mb_substr().
*/
/**
* Calculate the length of a string
*
* @param string $str
* @return int
*/
private static function ourStrlen($str)
{
static $exists = null;
if ($exists === null) {
$exists = \function_exists('mb_strlen');
}
if (!\is_string($str)) {
throw new InvalidArgumentException(
"ourStrlen() expects a string"
);
}
if ($exists) {
$length = \mb_strlen($str, '8bit');
if ($length === false) {
throw new CannotPerformOperationException();
}
return $length;
} else {
return \strlen($str);
}
}
/**
* Substring
*
* @param string $str
* @param int $start
* @param int $length
* @return string
*/
private static function ourSubstr($str, $start, $length = null)
{
static $exists = null;
if ($exists === null) {
$exists = \function_exists('mb_substr');
}
// Type validation:
if (!\is_string($str)) {
throw new InvalidArgumentException(
"ourSubstr() expects a string"
);
}
if ($exists) {
// mb_substr($str, 0, NULL, '8bit') returns an empty string on PHP
// 5.3, so we have to find the length ourselves.
if (!isset($length)) {
if ($start >= 0) {
$length = self::ourStrlen($str) - $start;
} else {
$length = -$start;
}
}
return \mb_substr($str, $start, $length, '8bit');
}
// Unlike mb_substr(), substr() doesn't accept NULL for length
if (isset($length)) {
return \substr($str, $start, $length);
} else {
return \substr($str, $start);
}
}
}