/src/php-src/ext/standard/password.c
Line | Count | Source |
1 | | /* |
2 | | +----------------------------------------------------------------------+ |
3 | | | Copyright © The PHP Group and Contributors. | |
4 | | +----------------------------------------------------------------------+ |
5 | | | This source file is subject to the Modified BSD License that is | |
6 | | | bundled with this package in the file LICENSE, and is available | |
7 | | | through the World Wide Web at <https://www.php.net/license/>. | |
8 | | | | |
9 | | | SPDX-License-Identifier: BSD-3-Clause | |
10 | | +----------------------------------------------------------------------+ |
11 | | | Authors: Anthony Ferrara <ircmaxell@php.net> | |
12 | | | Charles R. Portwood II <charlesportwoodii@erianna.com> | |
13 | | +----------------------------------------------------------------------+ |
14 | | */ |
15 | | |
16 | | #include <stdlib.h> |
17 | | |
18 | | #include "php.h" |
19 | | |
20 | | #include "fcntl.h" |
21 | | #include "php_password.h" |
22 | | #include "php_crypt.h" |
23 | | #include "base64.h" |
24 | | #include "zend_interfaces.h" |
25 | | #include "info.h" |
26 | | #include "ext/random/php_random_csprng.h" |
27 | | #include "password_arginfo.h" |
28 | | #ifdef HAVE_ARGON2LIB |
29 | | #include "argon2.h" |
30 | | #endif |
31 | | |
32 | | #ifdef PHP_WIN32 |
33 | | #include "win32/winutil.h" |
34 | | #endif |
35 | | |
36 | | static zend_array php_password_algos; |
37 | | |
38 | 16 | int php_password_algo_register(const char *ident, const php_password_algo *algo) { |
39 | 16 | zend_string *key = zend_string_init_interned(ident, strlen(ident), 1); |
40 | 16 | return zend_hash_add_ptr(&php_password_algos, key, (void *) algo) ? SUCCESS : FAILURE; |
41 | 16 | } |
42 | | |
43 | 0 | void php_password_algo_unregister(const char *ident) { |
44 | 0 | zend_hash_str_del(&php_password_algos, ident, strlen(ident)); |
45 | 0 | } |
46 | | |
47 | | static int php_password_salt_to64(const char *str, const size_t str_len, const size_t out_len, char *ret) /* {{{ */ |
48 | 0 | { |
49 | 0 | size_t pos = 0; |
50 | 0 | zend_string *buffer; |
51 | 0 | if ((int) str_len < 0) { |
52 | 0 | return FAILURE; |
53 | 0 | } |
54 | 0 | buffer = php_base64_encode((unsigned char*) str, str_len); |
55 | 0 | if (ZSTR_LEN(buffer) < out_len) { |
56 | | /* Too short of an encoded string generated */ |
57 | 0 | zend_string_release_ex(buffer, 0); |
58 | 0 | return FAILURE; |
59 | 0 | } |
60 | 0 | for (pos = 0; pos < out_len; pos++) { |
61 | 0 | if (ZSTR_VAL(buffer)[pos] == '+') { |
62 | 0 | ret[pos] = '.'; |
63 | 0 | } else if (ZSTR_VAL(buffer)[pos] == '=') { |
64 | 0 | zend_string_free(buffer); |
65 | 0 | return FAILURE; |
66 | 0 | } else { |
67 | 0 | ret[pos] = ZSTR_VAL(buffer)[pos]; |
68 | 0 | } |
69 | 0 | } |
70 | 0 | zend_string_free(buffer); |
71 | 0 | return SUCCESS; |
72 | 0 | } |
73 | | /* }}} */ |
74 | | |
75 | | static zend_string* php_password_make_salt(size_t length) /* {{{ */ |
76 | 0 | { |
77 | 0 | zend_string *ret, *buffer; |
78 | |
|
79 | 0 | if (length > (INT_MAX / 3)) { |
80 | 0 | zend_value_error("Length is too large to safely generate"); |
81 | 0 | return NULL; |
82 | 0 | } |
83 | | |
84 | 0 | buffer = zend_string_alloc(length * 3 / 4 + 1, 0); |
85 | 0 | if (FAILURE == php_random_bytes_throw(ZSTR_VAL(buffer), ZSTR_LEN(buffer))) { |
86 | 0 | zend_value_error("Unable to generate salt"); |
87 | 0 | zend_string_efree(buffer); |
88 | 0 | return NULL; |
89 | 0 | } |
90 | | |
91 | 0 | ret = zend_string_alloc(length, 0); |
92 | 0 | if (php_password_salt_to64(ZSTR_VAL(buffer), ZSTR_LEN(buffer), length, ZSTR_VAL(ret)) == FAILURE) { |
93 | 0 | zend_value_error("Generated salt too short"); |
94 | 0 | zend_string_efree(buffer); |
95 | 0 | zend_string_efree(ret); |
96 | 0 | return NULL; |
97 | 0 | } |
98 | 0 | zend_string_efree(buffer); |
99 | 0 | ZSTR_VAL(ret)[length] = 0; |
100 | 0 | return ret; |
101 | 0 | } |
102 | | /* }}} */ |
103 | | |
104 | 0 | static zend_string* php_password_get_salt(zval *unused_, size_t required_salt_len, HashTable *options) { |
105 | 0 | if (options && zend_hash_str_exists(options, "salt", sizeof("salt") - 1)) { |
106 | 0 | php_error_docref(NULL, E_WARNING, "The \"salt\" option has been ignored, since providing a custom salt is no longer supported"); |
107 | 0 | } |
108 | |
|
109 | 0 | return php_password_make_salt(required_salt_len); |
110 | 0 | } |
111 | | |
112 | | /* bcrypt implementation */ |
113 | | |
114 | 0 | static bool php_password_bcrypt_get_cost(const zend_string *hash, zend_long *cost) { |
115 | 0 | const char *h = ZSTR_VAL(hash); |
116 | |
|
117 | 0 | if ((ZSTR_LEN(hash) != 60) || |
118 | 0 | (h[0] != '$') || (h[1] != '2') || (h[2] != 'y') || (h[3] != '$') || |
119 | 0 | !ZEND_IS_DIGIT(h[4]) || !ZEND_IS_DIGIT(h[5]) || (h[6] != '$')) { |
120 | 0 | return false; |
121 | 0 | } |
122 | | |
123 | 0 | *cost = (h[4] - '0') * 10 + (h[5] - '0'); |
124 | |
|
125 | 0 | return *cost >= 4 && *cost <= 31; |
126 | 0 | } |
127 | | |
128 | 0 | static bool php_password_bcrypt_valid(const zend_string *hash) { |
129 | 0 | zend_long cost; |
130 | |
|
131 | 0 | return php_password_bcrypt_get_cost(hash, &cost); |
132 | 0 | } |
133 | | |
134 | 0 | static int php_password_bcrypt_get_info(zval *return_value, const zend_string *hash) { |
135 | 0 | zend_long cost; |
136 | |
|
137 | 0 | if (!php_password_bcrypt_get_cost(hash, &cost)) { |
138 | | /* Should never get called this way. */ |
139 | 0 | return FAILURE; |
140 | 0 | } |
141 | 0 | add_assoc_long(return_value, "cost", cost); |
142 | |
|
143 | 0 | return SUCCESS; |
144 | 0 | } |
145 | | |
146 | 0 | static bool php_password_bcrypt_needs_rehash(const zend_string *hash, zend_array *options) { |
147 | 0 | zval *znew_cost; |
148 | 0 | zend_long old_cost; |
149 | 0 | zend_long new_cost = PHP_PASSWORD_BCRYPT_COST; |
150 | |
|
151 | 0 | if (!php_password_bcrypt_get_cost(hash, &old_cost)) { |
152 | | /* Should never get called this way. */ |
153 | 0 | return true; |
154 | 0 | } |
155 | 0 | if (options && (znew_cost = zend_hash_str_find(options, "cost", sizeof("cost")-1)) != NULL) { |
156 | 0 | new_cost = zval_get_long(znew_cost); |
157 | 0 | } |
158 | |
|
159 | 0 | return old_cost != new_cost; |
160 | 0 | } |
161 | | |
162 | 0 | static bool php_password_bcrypt_verify(const zend_string *password, const zend_string *hash) { |
163 | 0 | int status = 0; |
164 | 0 | zend_string *ret = php_crypt(ZSTR_VAL(password), (int)ZSTR_LEN(password), ZSTR_VAL(hash), (int)ZSTR_LEN(hash), 1); |
165 | |
|
166 | 0 | if (!ret) { |
167 | 0 | return false; |
168 | 0 | } |
169 | | |
170 | 0 | if (ZSTR_LEN(hash) < 13) { |
171 | 0 | zend_string_free(ret); |
172 | 0 | return false; |
173 | 0 | } |
174 | | |
175 | | /* We're using this method instead of == in order to provide |
176 | | * resistance towards timing attacks. This is a constant time |
177 | | * equality check that will always check every byte of both |
178 | | * values. */ |
179 | 0 | status = php_safe_bcmp(ret, hash); |
180 | |
|
181 | 0 | zend_string_free(ret); |
182 | 0 | return status == 0; |
183 | 0 | } |
184 | | |
185 | 0 | static zend_string* php_password_bcrypt_hash(const zend_string *password, zend_array *options) { |
186 | 0 | char hash_format[10]; |
187 | 0 | size_t hash_format_len; |
188 | 0 | zend_string *result, *hash, *salt; |
189 | 0 | zval *zcost; |
190 | 0 | zend_long cost = PHP_PASSWORD_BCRYPT_COST; |
191 | |
|
192 | 0 | if (memchr(ZSTR_VAL(password), '\0', ZSTR_LEN(password))) { |
193 | 0 | zend_value_error("Bcrypt password must not contain null character"); |
194 | 0 | return NULL; |
195 | 0 | } |
196 | | |
197 | 0 | if (options && (zcost = zend_hash_str_find(options, "cost", sizeof("cost")-1)) != NULL) { |
198 | 0 | cost = zval_get_long(zcost); |
199 | 0 | } |
200 | |
|
201 | 0 | if (cost < 4 || cost > 31) { |
202 | 0 | zend_value_error("Invalid bcrypt cost parameter specified: " ZEND_LONG_FMT, cost); |
203 | 0 | return NULL; |
204 | 0 | } |
205 | | |
206 | 0 | hash_format_len = snprintf(hash_format, sizeof(hash_format), "$2y$%02" ZEND_LONG_FMT_SPEC "$", cost); |
207 | 0 | if (!(salt = php_password_get_salt(NULL, Z_UL(22), options))) { |
208 | 0 | return NULL; |
209 | 0 | } |
210 | 0 | ZSTR_VAL(salt)[ZSTR_LEN(salt)] = 0; |
211 | |
|
212 | 0 | hash = zend_string_concat2(hash_format, hash_format_len, ZSTR_VAL(salt), ZSTR_LEN(salt)); |
213 | |
|
214 | 0 | zend_string_release_ex(salt, 0); |
215 | | |
216 | | /* This cast is safe, since both values are defined here in code and cannot overflow */ |
217 | 0 | result = php_crypt(ZSTR_VAL(password), (int)ZSTR_LEN(password), ZSTR_VAL(hash), (int)ZSTR_LEN(hash), 1); |
218 | 0 | zend_string_release_ex(hash, 0); |
219 | |
|
220 | 0 | if (!result) { |
221 | 0 | return NULL; |
222 | 0 | } |
223 | | |
224 | 0 | if (ZSTR_LEN(result) < 13) { |
225 | 0 | zend_string_free(result); |
226 | 0 | return NULL; |
227 | 0 | } |
228 | | |
229 | 0 | return result; |
230 | 0 | } |
231 | | |
232 | | const php_password_algo php_password_algo_bcrypt = { |
233 | | "bcrypt", |
234 | | php_password_bcrypt_hash, |
235 | | php_password_bcrypt_verify, |
236 | | php_password_bcrypt_needs_rehash, |
237 | | php_password_bcrypt_get_info, |
238 | | php_password_bcrypt_valid, |
239 | | }; |
240 | | |
241 | | |
242 | | #ifdef HAVE_ARGON2LIB |
243 | | /* argon2i/argon2id shared implementation */ |
244 | | |
245 | | static int extract_argon2_parameters(const zend_string *hash, |
246 | | zend_long *v, zend_long *memory_cost, |
247 | | zend_long *time_cost, zend_long *threads) /* {{{ */ |
248 | | { |
249 | | const char *p = ZSTR_VAL(hash); |
250 | | if (!hash || (ZSTR_LEN(hash) < sizeof("$argon2id$"))) { |
251 | | return FAILURE; |
252 | | } |
253 | | if (!memcmp(p, "$argon2i$", sizeof("$argon2i$") - 1)) { |
254 | | p += sizeof("$argon2i$") - 1; |
255 | | } else if (!memcmp(p, "$argon2id$", sizeof("$argon2id$") - 1)) { |
256 | | p += sizeof("$argon2id$") - 1; |
257 | | } else { |
258 | | return FAILURE; |
259 | | } |
260 | | |
261 | | sscanf(p, "v=" ZEND_LONG_FMT "$m=" ZEND_LONG_FMT ",t=" ZEND_LONG_FMT ",p=" ZEND_LONG_FMT, |
262 | | v, memory_cost, time_cost, threads); |
263 | | |
264 | | return SUCCESS; |
265 | | } |
266 | | /* }}} */ |
267 | | |
268 | | static int php_password_argon2_get_info(zval *return_value, const zend_string *hash) { |
269 | | zend_long v = 0; |
270 | | zend_long memory_cost = PHP_PASSWORD_ARGON2_MEMORY_COST; |
271 | | zend_long time_cost = PHP_PASSWORD_ARGON2_TIME_COST; |
272 | | zend_long threads = PHP_PASSWORD_ARGON2_THREADS; |
273 | | |
274 | | extract_argon2_parameters(hash, &v, &memory_cost, &time_cost, &threads); |
275 | | |
276 | | add_assoc_long(return_value, "memory_cost", memory_cost); |
277 | | add_assoc_long(return_value, "time_cost", time_cost); |
278 | | add_assoc_long(return_value, "threads", threads); |
279 | | |
280 | | return SUCCESS; |
281 | | } |
282 | | |
283 | | static bool php_password_argon2_needs_rehash(const zend_string *hash, zend_array *options) { |
284 | | zend_long v = 0; |
285 | | zend_long new_memory_cost = PHP_PASSWORD_ARGON2_MEMORY_COST, memory_cost = 0; |
286 | | zend_long new_time_cost = PHP_PASSWORD_ARGON2_TIME_COST, time_cost = 0; |
287 | | zend_long new_threads = PHP_PASSWORD_ARGON2_THREADS, threads = 0; |
288 | | zval *option_buffer; |
289 | | |
290 | | if (options && (option_buffer = zend_hash_str_find(options, "memory_cost", sizeof("memory_cost")-1)) != NULL) { |
291 | | new_memory_cost = zval_get_long(option_buffer); |
292 | | } |
293 | | |
294 | | if (options && (option_buffer = zend_hash_str_find(options, "time_cost", sizeof("time_cost")-1)) != NULL) { |
295 | | new_time_cost = zval_get_long(option_buffer); |
296 | | } |
297 | | |
298 | | if (options && (option_buffer = zend_hash_str_find(options, "threads", sizeof("threads")-1)) != NULL) { |
299 | | new_threads = zval_get_long(option_buffer); |
300 | | } |
301 | | |
302 | | extract_argon2_parameters(hash, &v, &memory_cost, &time_cost, &threads); |
303 | | |
304 | | return (new_time_cost != time_cost) || |
305 | | (new_memory_cost != memory_cost) || |
306 | | (new_threads != threads); |
307 | | } |
308 | | |
309 | | static zend_string *php_password_argon2_hash(const zend_string *password, zend_array *options, argon2_type type) { |
310 | | zval *option_buffer; |
311 | | zend_string *salt, *out, *encoded; |
312 | | size_t time_cost = PHP_PASSWORD_ARGON2_TIME_COST; |
313 | | size_t memory_cost = PHP_PASSWORD_ARGON2_MEMORY_COST; |
314 | | size_t threads = PHP_PASSWORD_ARGON2_THREADS; |
315 | | size_t encoded_len; |
316 | | int status = 0; |
317 | | |
318 | | if (options && (option_buffer = zend_hash_str_find(options, "memory_cost", sizeof("memory_cost")-1)) != NULL) { |
319 | | memory_cost = zval_get_long(option_buffer); |
320 | | } |
321 | | |
322 | | if (memory_cost > ARGON2_MAX_MEMORY || memory_cost < ARGON2_MIN_MEMORY) { |
323 | | zend_value_error("Memory cost is outside of allowed memory range"); |
324 | | return NULL; |
325 | | } |
326 | | |
327 | | if (options && (option_buffer = zend_hash_str_find(options, "time_cost", sizeof("time_cost")-1)) != NULL) { |
328 | | time_cost = zval_get_long(option_buffer); |
329 | | } |
330 | | |
331 | | if (time_cost > ARGON2_MAX_TIME || time_cost < ARGON2_MIN_TIME) { |
332 | | zend_value_error("Time cost is outside of allowed time range"); |
333 | | return NULL; |
334 | | } |
335 | | |
336 | | if (options && (option_buffer = zend_hash_str_find(options, "threads", sizeof("threads")-1)) != NULL) { |
337 | | threads = zval_get_long(option_buffer); |
338 | | } |
339 | | |
340 | | if (threads > ARGON2_MAX_LANES || threads == 0) { |
341 | | zend_value_error("Invalid number of threads"); |
342 | | return NULL; |
343 | | } |
344 | | |
345 | | if (!(salt = php_password_get_salt(NULL, Z_UL(16), options))) { |
346 | | return NULL; |
347 | | } |
348 | | |
349 | | out = zend_string_alloc(32, 0); |
350 | | encoded_len = argon2_encodedlen( |
351 | | time_cost, |
352 | | memory_cost, |
353 | | threads, |
354 | | (uint32_t)ZSTR_LEN(salt), |
355 | | ZSTR_LEN(out), |
356 | | type |
357 | | ); |
358 | | |
359 | | encoded = zend_string_alloc(encoded_len - 1, 0); |
360 | | status = argon2_hash( |
361 | | time_cost, |
362 | | memory_cost, |
363 | | threads, |
364 | | ZSTR_VAL(password), |
365 | | ZSTR_LEN(password), |
366 | | ZSTR_VAL(salt), |
367 | | ZSTR_LEN(salt), |
368 | | ZSTR_VAL(out), |
369 | | ZSTR_LEN(out), |
370 | | ZSTR_VAL(encoded), |
371 | | encoded_len, |
372 | | type, |
373 | | ARGON2_VERSION_NUMBER |
374 | | ); |
375 | | |
376 | | zend_string_release_ex(out, 0); |
377 | | zend_string_release_ex(salt, 0); |
378 | | |
379 | | if (status != ARGON2_OK) { |
380 | | zend_string_efree(encoded); |
381 | | zend_value_error("%s", argon2_error_message(status)); |
382 | | return NULL; |
383 | | } |
384 | | |
385 | | ZSTR_VAL(encoded)[ZSTR_LEN(encoded)] = 0; |
386 | | return encoded; |
387 | | } |
388 | | |
389 | | /* argon2i specific methods */ |
390 | | |
391 | | static bool php_password_argon2i_verify(const zend_string *password, const zend_string *hash) { |
392 | | return ARGON2_OK == argon2_verify(ZSTR_VAL(hash), ZSTR_VAL(password), ZSTR_LEN(password), Argon2_i); |
393 | | } |
394 | | |
395 | | static zend_string *php_password_argon2i_hash(const zend_string *password, zend_array *options) { |
396 | | return php_password_argon2_hash(password, options, Argon2_i); |
397 | | } |
398 | | |
399 | | const php_password_algo php_password_algo_argon2i = { |
400 | | "argon2i", |
401 | | php_password_argon2i_hash, |
402 | | php_password_argon2i_verify, |
403 | | php_password_argon2_needs_rehash, |
404 | | php_password_argon2_get_info, |
405 | | NULL, |
406 | | }; |
407 | | |
408 | | /* argon2id specific methods */ |
409 | | |
410 | | static bool php_password_argon2id_verify(const zend_string *password, const zend_string *hash) { |
411 | | return ARGON2_OK == argon2_verify(ZSTR_VAL(hash), ZSTR_VAL(password), ZSTR_LEN(password), Argon2_id); |
412 | | } |
413 | | |
414 | | static zend_string *php_password_argon2id_hash(const zend_string *password, zend_array *options) { |
415 | | return php_password_argon2_hash(password, options, Argon2_id); |
416 | | } |
417 | | |
418 | | const php_password_algo php_password_algo_argon2id = { |
419 | | "argon2id", |
420 | | php_password_argon2id_hash, |
421 | | php_password_argon2id_verify, |
422 | | php_password_argon2_needs_rehash, |
423 | | php_password_argon2_get_info, |
424 | | NULL, |
425 | | }; |
426 | | #endif |
427 | | |
428 | | PHP_MINIT_FUNCTION(password) /* {{{ */ |
429 | 16 | { |
430 | 16 | zend_hash_init(&php_password_algos, 4, NULL, ZVAL_PTR_DTOR, 1); |
431 | | |
432 | 16 | register_password_symbols(module_number); |
433 | | |
434 | 16 | if (FAILURE == php_password_algo_register("2y", &php_password_algo_bcrypt)) { |
435 | 0 | return FAILURE; |
436 | 0 | } |
437 | | |
438 | | #ifdef HAVE_ARGON2LIB |
439 | | if (FAILURE == php_password_algo_register("argon2i", &php_password_algo_argon2i)) { |
440 | | return FAILURE; |
441 | | } |
442 | | if (FAILURE == php_password_algo_register("argon2id", &php_password_algo_argon2id)) { |
443 | | return FAILURE; |
444 | | } |
445 | | #endif |
446 | | |
447 | 16 | return SUCCESS; |
448 | 16 | } |
449 | | /* }}} */ |
450 | | |
451 | | PHP_MSHUTDOWN_FUNCTION(password) /* {{{ */ |
452 | 0 | { |
453 | | #ifdef ZTS |
454 | | if (!tsrm_is_main_thread()) { |
455 | | return SUCCESS; |
456 | | } |
457 | | #endif |
458 | 0 | zend_hash_destroy(&php_password_algos); |
459 | 0 | return SUCCESS; |
460 | 0 | } |
461 | | /* }}} */ |
462 | | |
463 | 0 | const php_password_algo* php_password_algo_default(void) { |
464 | 0 | return &php_password_algo_bcrypt; |
465 | 0 | } |
466 | | |
467 | 0 | const php_password_algo* php_password_algo_find(const zend_string *ident) { |
468 | 0 | zval *tmp; |
469 | |
|
470 | 0 | if (!ident) { |
471 | 0 | return NULL; |
472 | 0 | } |
473 | | |
474 | 0 | tmp = zend_hash_find(&php_password_algos, (zend_string*)ident); |
475 | 0 | if (!tmp || (Z_TYPE_P(tmp) != IS_PTR)) { |
476 | 0 | return NULL; |
477 | 0 | } |
478 | | |
479 | 0 | return Z_PTR_P(tmp); |
480 | 0 | } |
481 | | |
482 | 0 | static const php_password_algo* php_password_algo_find_zval(zend_string *arg_str, zend_long arg_long, bool arg_is_null) { |
483 | 0 | if (arg_is_null) { |
484 | 0 | return php_password_algo_default(); |
485 | 0 | } |
486 | | |
487 | 0 | if (arg_str) { |
488 | 0 | return php_password_algo_find(arg_str); |
489 | 0 | } |
490 | | |
491 | 0 | switch (arg_long) { |
492 | 0 | case 0: return php_password_algo_default(); |
493 | 0 | case 1: return &php_password_algo_bcrypt; |
494 | | #ifdef HAVE_ARGON2LIB |
495 | | case 2: return &php_password_algo_argon2i; |
496 | | case 3: return &php_password_algo_argon2id; |
497 | | #else |
498 | 0 | case 2: |
499 | 0 | { |
500 | 0 | zend_string *n = ZSTR_INIT_LITERAL("argon2i", 0); |
501 | 0 | const php_password_algo* ret = php_password_algo_find(n); |
502 | 0 | zend_string_release(n); |
503 | 0 | return ret; |
504 | 0 | } |
505 | 0 | case 3: |
506 | 0 | { |
507 | 0 | zend_string *n = ZSTR_INIT_LITERAL("argon2id", 0); |
508 | 0 | const php_password_algo* ret = php_password_algo_find(n); |
509 | 0 | zend_string_release(n); |
510 | 0 | return ret; |
511 | 0 | } |
512 | 0 | #endif |
513 | 0 | } |
514 | | |
515 | 0 | return NULL; |
516 | 0 | } |
517 | | |
518 | 0 | zend_string *php_password_algo_extract_ident(const zend_string* hash) { |
519 | 0 | const char *ident, *ident_end; |
520 | |
|
521 | 0 | if (!hash || ZSTR_LEN(hash) < 3) { |
522 | | /* Minimum prefix: "$x$" */ |
523 | 0 | return NULL; |
524 | 0 | } |
525 | | |
526 | 0 | ident = ZSTR_VAL(hash) + 1; |
527 | 0 | ident_end = strchr(ident, '$'); |
528 | 0 | if (!ident_end) { |
529 | | /* No terminating '$' */ |
530 | 0 | return NULL; |
531 | 0 | } |
532 | | |
533 | 0 | return zend_string_init(ident, ident_end - ident, 0); |
534 | 0 | } |
535 | | |
536 | 0 | const php_password_algo* php_password_algo_identify_ex(const zend_string* hash, const php_password_algo *default_algo) { |
537 | 0 | const php_password_algo *algo; |
538 | 0 | zend_string *ident = php_password_algo_extract_ident(hash); |
539 | |
|
540 | 0 | if (!ident) { |
541 | 0 | return default_algo; |
542 | 0 | } |
543 | | |
544 | 0 | algo = php_password_algo_find(ident); |
545 | 0 | zend_string_release(ident); |
546 | 0 | return (!algo || (algo->valid && !algo->valid(hash))) ? default_algo : algo; |
547 | 0 | } |
548 | | |
549 | | /* {{{ Retrieves information about a given hash */ |
550 | | PHP_FUNCTION(password_get_info) |
551 | 0 | { |
552 | 0 | const php_password_algo *algo; |
553 | 0 | zend_string *hash, *ident; |
554 | 0 | zval options; |
555 | |
|
556 | 0 | ZEND_PARSE_PARAMETERS_START(1, 1) |
557 | 0 | Z_PARAM_STR(hash) |
558 | 0 | ZEND_PARSE_PARAMETERS_END(); |
559 | | |
560 | 0 | array_init(return_value); |
561 | 0 | array_init(&options); |
562 | |
|
563 | 0 | ident = php_password_algo_extract_ident(hash); |
564 | 0 | algo = php_password_algo_find(ident); |
565 | 0 | if (!algo || (algo->valid && !algo->valid(hash))) { |
566 | 0 | if (ident) { |
567 | 0 | zend_string_release(ident); |
568 | 0 | } |
569 | 0 | add_assoc_null(return_value, "algo"); |
570 | 0 | add_assoc_string(return_value, "algoName", "unknown"); |
571 | 0 | add_assoc_zval(return_value, "options", &options); |
572 | 0 | return; |
573 | 0 | } |
574 | | |
575 | 0 | add_assoc_str(return_value, "algo", php_password_algo_extract_ident(hash)); |
576 | 0 | zend_string_release(ident); |
577 | |
|
578 | 0 | add_assoc_string(return_value, "algoName", algo->name); |
579 | 0 | if (algo->get_info) { |
580 | 0 | algo->get_info(&options, hash); |
581 | 0 | } |
582 | 0 | add_assoc_zval(return_value, "options", &options); |
583 | 0 | } |
584 | | /** }}} */ |
585 | | |
586 | | /* {{{ Determines if a given hash requires re-hashing based upon parameters */ |
587 | | PHP_FUNCTION(password_needs_rehash) |
588 | 0 | { |
589 | 0 | const php_password_algo *old_algo, *new_algo; |
590 | 0 | zend_string *hash; |
591 | 0 | zend_string *new_algo_str; |
592 | 0 | zend_long new_algo_long = 0; |
593 | 0 | bool new_algo_is_null; |
594 | 0 | zend_array *options = NULL; |
595 | |
|
596 | 0 | ZEND_PARSE_PARAMETERS_START(2, 3) |
597 | 0 | Z_PARAM_STR(hash) |
598 | 0 | Z_PARAM_STR_OR_LONG_OR_NULL(new_algo_str, new_algo_long, new_algo_is_null) |
599 | 0 | Z_PARAM_OPTIONAL |
600 | 0 | Z_PARAM_ARRAY_HT(options) |
601 | 0 | ZEND_PARSE_PARAMETERS_END(); |
602 | | |
603 | 0 | new_algo = php_password_algo_find_zval(new_algo_str, new_algo_long, new_algo_is_null); |
604 | 0 | if (!new_algo) { |
605 | | /* Unknown new algorithm, never prompt to rehash. */ |
606 | 0 | RETURN_FALSE; |
607 | 0 | } |
608 | | |
609 | 0 | old_algo = php_password_algo_identify_ex(hash, NULL); |
610 | 0 | if (old_algo != new_algo) { |
611 | | /* Different algorithm preferred, always rehash. */ |
612 | 0 | RETURN_TRUE; |
613 | 0 | } |
614 | | |
615 | 0 | RETURN_BOOL(old_algo->needs_rehash(hash, options)); |
616 | 0 | } |
617 | | /* }}} */ |
618 | | |
619 | | /* {{{ Verify a hash created using crypt() or password_hash() */ |
620 | | PHP_FUNCTION(password_verify) |
621 | 0 | { |
622 | 0 | zend_string *password, *hash; |
623 | 0 | const php_password_algo *algo; |
624 | |
|
625 | 0 | ZEND_PARSE_PARAMETERS_START(2, 2) |
626 | 0 | Z_PARAM_STR(password) |
627 | 0 | Z_PARAM_STR(hash) |
628 | 0 | ZEND_PARSE_PARAMETERS_END(); |
629 | | |
630 | 0 | algo = php_password_algo_identify(hash); |
631 | 0 | RETURN_BOOL(algo && (!algo->verify || algo->verify(password, hash))); |
632 | 0 | } |
633 | | /* }}} */ |
634 | | |
635 | | /* {{{ Hash a password */ |
636 | | PHP_FUNCTION(password_hash) |
637 | 0 | { |
638 | 0 | zend_string *password, *digest = NULL; |
639 | 0 | zend_string *algo_str; |
640 | 0 | zend_long algo_long = 0; |
641 | 0 | bool algo_is_null; |
642 | 0 | const php_password_algo *algo; |
643 | 0 | zend_array *options = NULL; |
644 | |
|
645 | 0 | ZEND_PARSE_PARAMETERS_START(2, 3) |
646 | 0 | Z_PARAM_STR(password) |
647 | 0 | Z_PARAM_STR_OR_LONG_OR_NULL(algo_str, algo_long, algo_is_null) |
648 | 0 | Z_PARAM_OPTIONAL |
649 | 0 | Z_PARAM_ARRAY_HT(options) |
650 | 0 | ZEND_PARSE_PARAMETERS_END(); |
651 | | |
652 | 0 | algo = php_password_algo_find_zval(algo_str, algo_long, algo_is_null); |
653 | 0 | if (!algo) { |
654 | 0 | zend_argument_value_error(2, "must be a valid password hashing algorithm"); |
655 | 0 | RETURN_THROWS(); |
656 | 0 | } |
657 | | |
658 | 0 | digest = algo->hash(password, options); |
659 | 0 | if (!digest) { |
660 | 0 | if (!EG(exception)) { |
661 | 0 | zend_throw_error(NULL, "Password hashing failed for unknown reason"); |
662 | 0 | } |
663 | 0 | RETURN_THROWS(); |
664 | 0 | } |
665 | | |
666 | 0 | RETURN_NEW_STR(digest); |
667 | 0 | } |
668 | | /* }}} */ |
669 | | |
670 | | /* {{{ */ |
671 | 0 | PHP_FUNCTION(password_algos) { |
672 | 0 | zend_string *algo; |
673 | |
|
674 | 0 | ZEND_PARSE_PARAMETERS_NONE(); |
675 | | |
676 | 0 | array_init(return_value); |
677 | 0 | ZEND_HASH_MAP_FOREACH_STR_KEY(&php_password_algos, algo) { |
678 | 0 | add_next_index_str(return_value, zend_string_copy(algo)); |
679 | 0 | } ZEND_HASH_FOREACH_END(); |
680 | 0 | } |
681 | | /* }}} */ |