1 /*
2 +----------------------------------------------------------------------+
3 | PHP Version 7 |
4 +----------------------------------------------------------------------+
5 | Copyright (c) The PHP Group |
6 +----------------------------------------------------------------------+
7 | This source file is subject to version 3.01 of the PHP license, |
8 | that is bundled with this package in the file LICENSE, and is |
9 | available through the world-wide-web at the following url: |
10 | http://www.php.net/license/3_01.txt |
11 | If you did not receive a copy of the PHP license and are unable to |
12 | obtain it through the world-wide-web, please send a note to |
13 | license@php.net so we can mail you a copy immediately. |
14 +----------------------------------------------------------------------+
15 | Authors: Anthony Ferrara <ircmaxell@php.net> |
16 | Charles R. Portwood II <charlesportwoodii@erianna.com> |
17 +----------------------------------------------------------------------+
18 */
19
20 #include <stdlib.h>
21
22 #include "php.h"
23
24 #include "fcntl.h"
25 #include "php_password.h"
26 #include "php_rand.h"
27 #include "php_crypt.h"
28 #include "base64.h"
29 #include "zend_interfaces.h"
30 #include "info.h"
31 #include "php_random.h"
32 #if HAVE_ARGON2LIB
33 #include "argon2.h"
34 #endif
35
36 #ifdef PHP_WIN32
37 #include "win32/winutil.h"
38 #endif
39
40 static zend_array php_password_algos;
41
php_password_algo_register(const char * ident,const php_password_algo * algo)42 int php_password_algo_register(const char *ident, const php_password_algo *algo) {
43 zval zalgo;
44 ZVAL_PTR(&zalgo, (php_password_algo*)algo);
45 if (zend_hash_str_add(&php_password_algos, ident, strlen(ident), &zalgo)) {
46 return SUCCESS;
47 }
48 return FAILURE;
49 }
50
php_password_algo_unregister(const char * ident)51 void php_password_algo_unregister(const char *ident) {
52 zend_hash_str_del(&php_password_algos, ident, strlen(ident));
53 }
54
php_password_salt_is_alphabet(const char * str,const size_t len)55 static int php_password_salt_is_alphabet(const char *str, const size_t len) /* {{{ */
56 {
57 size_t i = 0;
58
59 for (i = 0; i < len; i++) {
60 if (!((str[i] >= 'A' && str[i] <= 'Z') || (str[i] >= 'a' && str[i] <= 'z') || (str[i] >= '0' && str[i] <= '9') || str[i] == '.' || str[i] == '/')) {
61 return FAILURE;
62 }
63 }
64 return SUCCESS;
65 }
66 /* }}} */
67
php_password_salt_to64(const char * str,const size_t str_len,const size_t out_len,char * ret)68 static int php_password_salt_to64(const char *str, const size_t str_len, const size_t out_len, char *ret) /* {{{ */
69 {
70 size_t pos = 0;
71 zend_string *buffer;
72 if ((int) str_len < 0) {
73 return FAILURE;
74 }
75 buffer = php_base64_encode((unsigned char*) str, str_len);
76 if (ZSTR_LEN(buffer) < out_len) {
77 /* Too short of an encoded string generated */
78 zend_string_release_ex(buffer, 0);
79 return FAILURE;
80 }
81 for (pos = 0; pos < out_len; pos++) {
82 if (ZSTR_VAL(buffer)[pos] == '+') {
83 ret[pos] = '.';
84 } else if (ZSTR_VAL(buffer)[pos] == '=') {
85 zend_string_free(buffer);
86 return FAILURE;
87 } else {
88 ret[pos] = ZSTR_VAL(buffer)[pos];
89 }
90 }
91 zend_string_free(buffer);
92 return SUCCESS;
93 }
94 /* }}} */
95
php_password_make_salt(size_t length)96 static zend_string* php_password_make_salt(size_t length) /* {{{ */
97 {
98 zend_string *ret, *buffer;
99
100 if (length > (INT_MAX / 3)) {
101 php_error_docref(NULL, E_WARNING, "Length is too large to safely generate");
102 return NULL;
103 }
104
105 buffer = zend_string_alloc(length * 3 / 4 + 1, 0);
106 if (FAILURE == php_random_bytes_silent(ZSTR_VAL(buffer), ZSTR_LEN(buffer))) {
107 php_error_docref(NULL, E_WARNING, "Unable to generate salt");
108 zend_string_release_ex(buffer, 0);
109 return NULL;
110 }
111
112 ret = zend_string_alloc(length, 0);
113 if (php_password_salt_to64(ZSTR_VAL(buffer), ZSTR_LEN(buffer), length, ZSTR_VAL(ret)) == FAILURE) {
114 php_error_docref(NULL, E_WARNING, "Generated salt too short");
115 zend_string_release_ex(buffer, 0);
116 zend_string_release_ex(ret, 0);
117 return NULL;
118 }
119 zend_string_release_ex(buffer, 0);
120 ZSTR_VAL(ret)[length] = 0;
121 return ret;
122 }
123 /* }}} */
124
php_password_get_salt(zval * unused_,size_t required_salt_len,HashTable * options)125 static zend_string* php_password_get_salt(zval *unused_, size_t required_salt_len, HashTable *options) {
126 zend_string *buffer;
127 zval *option_buffer;
128
129 if (!options || !(option_buffer = zend_hash_str_find(options, "salt", sizeof("salt") - 1))) {
130 return php_password_make_salt(required_salt_len);
131 }
132
133 php_error_docref(NULL, E_DEPRECATED, "Use of the 'salt' option to password_hash is deprecated");
134
135 switch (Z_TYPE_P(option_buffer)) {
136 case IS_STRING:
137 buffer = zend_string_copy(Z_STR_P(option_buffer));
138 break;
139 case IS_LONG:
140 case IS_DOUBLE:
141 case IS_OBJECT:
142 buffer = zval_try_get_string(option_buffer);
143 if (UNEXPECTED(!buffer)) {
144 return NULL;
145 }
146 break;
147 case IS_FALSE:
148 case IS_TRUE:
149 case IS_NULL:
150 case IS_RESOURCE:
151 case IS_ARRAY:
152 default:
153 php_error_docref(NULL, E_WARNING, "Non-string salt parameter supplied");
154 return NULL;
155 }
156
157 /* XXX all the crypt related APIs work with int for string length.
158 That should be revised for size_t and then we maybe don't require
159 the > INT_MAX check. */
160 if (ZEND_SIZE_T_INT_OVFL(ZSTR_LEN(buffer))) {
161 php_error_docref(NULL, E_WARNING, "Supplied salt is too long");
162 zend_string_release_ex(buffer, 0);
163 return NULL;
164 }
165
166 if (ZSTR_LEN(buffer) < required_salt_len) {
167 php_error_docref(NULL, E_WARNING, "Provided salt is too short: %zd expecting %zd", ZSTR_LEN(buffer), required_salt_len);
168 zend_string_release_ex(buffer, 0);
169 return NULL;
170 }
171
172 if (php_password_salt_is_alphabet(ZSTR_VAL(buffer), ZSTR_LEN(buffer)) == FAILURE) {
173 zend_string *salt = zend_string_alloc(required_salt_len, 0);
174 if (php_password_salt_to64(ZSTR_VAL(buffer), ZSTR_LEN(buffer), required_salt_len, ZSTR_VAL(salt)) == FAILURE) {
175 php_error_docref(NULL, E_WARNING, "Provided salt is too short: %zd", ZSTR_LEN(buffer));
176 zend_string_release_ex(salt, 0);
177 zend_string_release_ex(buffer, 0);
178 return NULL;
179 }
180 zend_string_release_ex(buffer, 0);
181 return salt;
182 } else {
183 zend_string *salt = zend_string_alloc(required_salt_len, 0);
184 memcpy(ZSTR_VAL(salt), ZSTR_VAL(buffer), required_salt_len);
185 zend_string_release_ex(buffer, 0);
186 return salt;
187 }
188 }
189
190 /* bcrypt implementation */
191
php_password_bcrypt_valid(const zend_string * hash)192 static zend_bool php_password_bcrypt_valid(const zend_string *hash) {
193 const char *h = ZSTR_VAL(hash);
194 return (ZSTR_LEN(hash) == 60) &&
195 (h[0] == '$') && (h[1] == '2') && (h[2] == 'y');
196 }
197
php_password_bcrypt_get_info(zval * return_value,const zend_string * hash)198 static int php_password_bcrypt_get_info(zval *return_value, const zend_string *hash) {
199 zend_long cost = PHP_PASSWORD_BCRYPT_COST;
200
201 if (!php_password_bcrypt_valid(hash)) {
202 /* Should never get called this way. */
203 return FAILURE;
204 }
205
206 sscanf(ZSTR_VAL(hash), "$2y$" ZEND_LONG_FMT "$", &cost);
207 add_assoc_long(return_value, "cost", cost);
208
209 return SUCCESS;
210 }
211
php_password_bcrypt_needs_rehash(const zend_string * hash,zend_array * options)212 static zend_bool php_password_bcrypt_needs_rehash(const zend_string *hash, zend_array *options) {
213 zval *znew_cost;
214 zend_long old_cost = PHP_PASSWORD_BCRYPT_COST;
215 zend_long new_cost = PHP_PASSWORD_BCRYPT_COST;
216
217 if (!php_password_bcrypt_valid(hash)) {
218 /* Should never get called this way. */
219 return 1;
220 }
221
222 sscanf(ZSTR_VAL(hash), "$2y$" ZEND_LONG_FMT "$", &old_cost);
223 if (options && (znew_cost = zend_hash_str_find(options, "cost", sizeof("cost")-1)) != NULL) {
224 new_cost = zval_get_long(znew_cost);
225 }
226
227 return old_cost != new_cost;
228 }
229
php_password_bcrypt_verify(const zend_string * password,const zend_string * hash)230 static zend_bool php_password_bcrypt_verify(const zend_string *password, const zend_string *hash) {
231 size_t i;
232 int status = 0;
233 zend_string *ret = php_crypt(ZSTR_VAL(password), (int)ZSTR_LEN(password), ZSTR_VAL(hash), (int)ZSTR_LEN(hash), 1);
234
235 if (!ret) {
236 return 0;
237 }
238
239 if (ZSTR_LEN(ret) != ZSTR_LEN(hash) || ZSTR_LEN(hash) < 13) {
240 zend_string_free(ret);
241 return 0;
242 }
243
244 /* We're using this method instead of == in order to provide
245 * resistance towards timing attacks. This is a constant time
246 * equality check that will always check every byte of both
247 * values. */
248 for (i = 0; i < ZSTR_LEN(hash); i++) {
249 status |= (ZSTR_VAL(ret)[i] ^ ZSTR_VAL(hash)[i]);
250 }
251
252 zend_string_free(ret);
253 return status == 0;
254 }
255
php_password_bcrypt_hash(const zend_string * password,zend_array * options)256 static zend_string* php_password_bcrypt_hash(const zend_string *password, zend_array *options) {
257 char hash_format[10];
258 size_t hash_format_len;
259 zend_string *result, *hash, *salt;
260 zval *zcost;
261 zend_long cost = PHP_PASSWORD_BCRYPT_COST;
262
263 if (options && (zcost = zend_hash_str_find(options, "cost", sizeof("cost")-1)) != NULL) {
264 cost = zval_get_long(zcost);
265 }
266
267 if (cost < 4 || cost > 31) {
268 php_error_docref(NULL, E_WARNING, "Invalid bcrypt cost parameter specified: " ZEND_LONG_FMT, cost);
269 return NULL;
270 }
271
272 hash_format_len = snprintf(hash_format, sizeof(hash_format), "$2y$%02" ZEND_LONG_FMT_SPEC "$", cost);
273 if (!(salt = php_password_get_salt(NULL, Z_UL(22), options))) {
274 return NULL;
275 }
276 ZSTR_VAL(salt)[ZSTR_LEN(salt)] = 0;
277
278 hash = zend_string_alloc(ZSTR_LEN(salt) + hash_format_len, 0);
279 sprintf(ZSTR_VAL(hash), "%s%s", hash_format, ZSTR_VAL(salt));
280 ZSTR_VAL(hash)[hash_format_len + ZSTR_LEN(salt)] = 0;
281
282 zend_string_release_ex(salt, 0);
283
284 /* This cast is safe, since both values are defined here in code and cannot overflow */
285 result = php_crypt(ZSTR_VAL(password), (int)ZSTR_LEN(password), ZSTR_VAL(hash), (int)ZSTR_LEN(hash), 1);
286 zend_string_release_ex(hash, 0);
287
288 if (!result) {
289 return NULL;
290 }
291
292 if (ZSTR_LEN(result) < 13) {
293 zend_string_free(result);
294 return NULL;
295 }
296
297 return result;
298 }
299
300 const php_password_algo php_password_algo_bcrypt = {
301 "bcrypt",
302 php_password_bcrypt_hash,
303 php_password_bcrypt_verify,
304 php_password_bcrypt_needs_rehash,
305 php_password_bcrypt_get_info,
306 php_password_bcrypt_valid,
307 };
308
309
310 #if HAVE_ARGON2LIB
311 /* argon2i/argon2id shared implementation */
312
extract_argon2_parameters(const zend_string * hash,zend_long * v,zend_long * memory_cost,zend_long * time_cost,zend_long * threads)313 static int extract_argon2_parameters(const zend_string *hash,
314 zend_long *v, zend_long *memory_cost,
315 zend_long *time_cost, zend_long *threads) /* {{{ */
316 {
317 const char *p = ZSTR_VAL(hash);
318 if (!hash || (ZSTR_LEN(hash) < sizeof("$argon2id$"))) {
319 return FAILURE;
320 }
321 if (!memcmp(p, "$argon2i$", sizeof("$argon2i$") - 1)) {
322 p += sizeof("$argon2i$") - 1;
323 } else if (!memcmp(p, "$argon2id$", sizeof("$argon2id$") - 1)) {
324 p += sizeof("$argon2id$") - 1;
325 } else {
326 return FAILURE;
327 }
328
329 sscanf(p, "v=" ZEND_LONG_FMT "$m=" ZEND_LONG_FMT ",t=" ZEND_LONG_FMT ",p=" ZEND_LONG_FMT,
330 v, memory_cost, time_cost, threads);
331
332 return SUCCESS;
333 }
334 /* }}} */
335
php_password_argon2_get_info(zval * return_value,const zend_string * hash)336 static int php_password_argon2_get_info(zval *return_value, const zend_string *hash) {
337 zend_long v = 0;
338 zend_long memory_cost = PHP_PASSWORD_ARGON2_MEMORY_COST;
339 zend_long time_cost = PHP_PASSWORD_ARGON2_TIME_COST;
340 zend_long threads = PHP_PASSWORD_ARGON2_THREADS;
341
342 extract_argon2_parameters(hash, &v, &memory_cost, &time_cost, &threads);
343
344 add_assoc_long(return_value, "memory_cost", memory_cost);
345 add_assoc_long(return_value, "time_cost", time_cost);
346 add_assoc_long(return_value, "threads", threads);
347
348 return SUCCESS;
349 }
350
php_password_argon2_needs_rehash(const zend_string * hash,zend_array * options)351 static zend_bool php_password_argon2_needs_rehash(const zend_string *hash, zend_array *options) {
352 zend_long v = 0;
353 zend_long new_memory_cost = PHP_PASSWORD_ARGON2_MEMORY_COST, memory_cost = 0;
354 zend_long new_time_cost = PHP_PASSWORD_ARGON2_TIME_COST, time_cost = 0;
355 zend_long new_threads = PHP_PASSWORD_ARGON2_THREADS, threads = 0;
356 zval *option_buffer;
357
358 if (options && (option_buffer = zend_hash_str_find(options, "memory_cost", sizeof("memory_cost")-1)) != NULL) {
359 new_memory_cost = zval_get_long(option_buffer);
360 }
361
362 if (options && (option_buffer = zend_hash_str_find(options, "time_cost", sizeof("time_cost")-1)) != NULL) {
363 new_time_cost = zval_get_long(option_buffer);
364 }
365
366 if (options && (option_buffer = zend_hash_str_find(options, "threads", sizeof("threads")-1)) != NULL) {
367 new_threads = zval_get_long(option_buffer);
368 }
369
370 extract_argon2_parameters(hash, &v, &memory_cost, &time_cost, &threads);
371
372 return (new_time_cost != time_cost) ||
373 (new_memory_cost != memory_cost) ||
374 (new_threads != threads);
375 }
376
php_password_argon2_hash(const zend_string * password,zend_array * options,argon2_type type)377 static zend_string *php_password_argon2_hash(const zend_string *password, zend_array *options, argon2_type type) {
378 zval *option_buffer;
379 zend_string *salt, *out, *encoded;
380 size_t time_cost = PHP_PASSWORD_ARGON2_TIME_COST;
381 size_t memory_cost = PHP_PASSWORD_ARGON2_MEMORY_COST;
382 size_t threads = PHP_PASSWORD_ARGON2_THREADS;
383 size_t encoded_len;
384 int status = 0;
385
386 if (options && (option_buffer = zend_hash_str_find(options, "memory_cost", sizeof("memory_cost")-1)) != NULL) {
387 memory_cost = zval_get_long(option_buffer);
388 }
389
390 if (memory_cost > ARGON2_MAX_MEMORY || memory_cost < ARGON2_MIN_MEMORY) {
391 php_error_docref(NULL, E_WARNING, "Memory cost is outside of allowed memory range");
392 return NULL;
393 }
394
395 if (options && (option_buffer = zend_hash_str_find(options, "time_cost", sizeof("time_cost")-1)) != NULL) {
396 time_cost = zval_get_long(option_buffer);
397 }
398
399 if (time_cost > ARGON2_MAX_TIME || time_cost < ARGON2_MIN_TIME) {
400 php_error_docref(NULL, E_WARNING, "Time cost is outside of allowed time range");
401 return NULL;
402 }
403
404 if (options && (option_buffer = zend_hash_str_find(options, "threads", sizeof("threads")-1)) != NULL) {
405 threads = zval_get_long(option_buffer);
406 }
407
408 if (threads > ARGON2_MAX_LANES || threads == 0) {
409 php_error_docref(NULL, E_WARNING, "Invalid number of threads");
410 return NULL;
411 }
412
413 if (!(salt = php_password_get_salt(NULL, Z_UL(16), options))) {
414 return NULL;
415 }
416
417 out = zend_string_alloc(32, 0);
418 encoded_len = argon2_encodedlen(
419 time_cost,
420 memory_cost,
421 threads,
422 (uint32_t)ZSTR_LEN(salt),
423 ZSTR_LEN(out),
424 type
425 );
426
427 encoded = zend_string_alloc(encoded_len - 1, 0);
428 status = argon2_hash(
429 time_cost,
430 memory_cost,
431 threads,
432 ZSTR_VAL(password),
433 ZSTR_LEN(password),
434 ZSTR_VAL(salt),
435 ZSTR_LEN(salt),
436 ZSTR_VAL(out),
437 ZSTR_LEN(out),
438 ZSTR_VAL(encoded),
439 encoded_len,
440 type,
441 ARGON2_VERSION_NUMBER
442 );
443
444 zend_string_release_ex(out, 0);
445 zend_string_release_ex(salt, 0);
446
447 if (status != ARGON2_OK) {
448 zend_string_efree(encoded);
449 php_error_docref(NULL, E_WARNING, "%s", argon2_error_message(status));
450 return NULL;
451 }
452
453 ZSTR_VAL(encoded)[ZSTR_LEN(encoded)] = 0;
454 return encoded;
455 }
456
457 /* argon2i specific methods */
458
php_password_argon2i_verify(const zend_string * password,const zend_string * hash)459 static zend_bool php_password_argon2i_verify(const zend_string *password, const zend_string *hash) {
460 return ARGON2_OK == argon2_verify(ZSTR_VAL(hash), ZSTR_VAL(password), ZSTR_LEN(password), Argon2_i);
461 }
462
php_password_argon2i_hash(const zend_string * password,zend_array * options)463 static zend_string *php_password_argon2i_hash(const zend_string *password, zend_array *options) {
464 return php_password_argon2_hash(password, options, Argon2_i);
465 }
466
467 const php_password_algo php_password_algo_argon2i = {
468 "argon2i",
469 php_password_argon2i_hash,
470 php_password_argon2i_verify,
471 php_password_argon2_needs_rehash,
472 php_password_argon2_get_info,
473 NULL,
474 };
475
476 /* argon2id specific methods */
477
php_password_argon2id_verify(const zend_string * password,const zend_string * hash)478 static zend_bool php_password_argon2id_verify(const zend_string *password, const zend_string *hash) {
479 return ARGON2_OK == argon2_verify(ZSTR_VAL(hash), ZSTR_VAL(password), ZSTR_LEN(password), Argon2_id);
480 }
481
php_password_argon2id_hash(const zend_string * password,zend_array * options)482 static zend_string *php_password_argon2id_hash(const zend_string *password, zend_array *options) {
483 return php_password_argon2_hash(password, options, Argon2_id);
484 }
485
486 const php_password_algo php_password_algo_argon2id = {
487 "argon2id",
488 php_password_argon2id_hash,
489 php_password_argon2id_verify,
490 php_password_argon2_needs_rehash,
491 php_password_argon2_get_info,
492 NULL,
493 };
494 #endif
495
PHP_MINIT_FUNCTION(password)496 PHP_MINIT_FUNCTION(password) /* {{{ */
497 {
498 zend_hash_init(&php_password_algos, 4, NULL, ZVAL_PTR_DTOR, 1);
499 REGISTER_STRING_CONSTANT("PASSWORD_DEFAULT", "2y", CONST_CS | CONST_PERSISTENT);
500
501 if (FAILURE == php_password_algo_register("2y", &php_password_algo_bcrypt)) {
502 return FAILURE;
503 }
504 REGISTER_STRING_CONSTANT("PASSWORD_BCRYPT", "2y", CONST_CS | CONST_PERSISTENT);
505
506 #if HAVE_ARGON2LIB
507 if (FAILURE == php_password_algo_register("argon2i", &php_password_algo_argon2i)) {
508 return FAILURE;
509 }
510 REGISTER_STRING_CONSTANT("PASSWORD_ARGON2I", "argon2i", CONST_CS | CONST_PERSISTENT);
511
512 if (FAILURE == php_password_algo_register("argon2id", &php_password_algo_argon2id)) {
513 return FAILURE;
514 }
515 REGISTER_STRING_CONSTANT("PASSWORD_ARGON2ID", "argon2id", CONST_CS | CONST_PERSISTENT);
516 #endif
517
518 REGISTER_LONG_CONSTANT("PASSWORD_BCRYPT_DEFAULT_COST", PHP_PASSWORD_BCRYPT_COST, CONST_CS | CONST_PERSISTENT);
519 #if HAVE_ARGON2LIB
520 REGISTER_LONG_CONSTANT("PASSWORD_ARGON2_DEFAULT_MEMORY_COST", PHP_PASSWORD_ARGON2_MEMORY_COST, CONST_CS | CONST_PERSISTENT);
521 REGISTER_LONG_CONSTANT("PASSWORD_ARGON2_DEFAULT_TIME_COST", PHP_PASSWORD_ARGON2_TIME_COST, CONST_CS | CONST_PERSISTENT);
522 REGISTER_LONG_CONSTANT("PASSWORD_ARGON2_DEFAULT_THREADS", PHP_PASSWORD_ARGON2_THREADS, CONST_CS | CONST_PERSISTENT);
523
524 REGISTER_STRING_CONSTANT("PASSWORD_ARGON2_PROVIDER", "standard", CONST_CS | CONST_PERSISTENT);
525 #endif
526
527 return SUCCESS;
528 }
529 /* }}} */
530
PHP_MSHUTDOWN_FUNCTION(password)531 PHP_MSHUTDOWN_FUNCTION(password) /* {{{ */
532 {
533 #ifdef ZTS
534 if (!tsrm_is_main_thread()) {
535 return SUCCESS;
536 }
537 #endif
538 zend_hash_destroy(&php_password_algos);
539 return SUCCESS;
540 }
541 /* }}} */
542
php_password_algo_default()543 const php_password_algo* php_password_algo_default() {
544 return &php_password_algo_bcrypt;
545 }
546
php_password_algo_find(const zend_string * ident)547 const php_password_algo* php_password_algo_find(const zend_string *ident) {
548 zval *tmp;
549
550 if (!ident) {
551 return NULL;
552 }
553
554 tmp = zend_hash_find(&php_password_algos, (zend_string*)ident);
555 if (!tmp || (Z_TYPE_P(tmp) != IS_PTR)) {
556 return NULL;
557 }
558
559 return Z_PTR_P(tmp);
560 }
561
php_password_algo_find_zval_ex(zval * arg,const php_password_algo * default_algo)562 static const php_password_algo* php_password_algo_find_zval_ex(zval *arg, const php_password_algo* default_algo) {
563 if (!arg || (Z_TYPE_P(arg) == IS_NULL)) {
564 return default_algo;
565 }
566
567 if (Z_TYPE_P(arg) == IS_LONG) {
568 switch (Z_LVAL_P(arg)) {
569 case 0: return default_algo;
570 case 1: return &php_password_algo_bcrypt;
571 #if HAVE_ARGON2LIB
572 case 2: return &php_password_algo_argon2i;
573 case 3: return &php_password_algo_argon2id;
574 #else
575 case 2:
576 {
577 zend_string *n = zend_string_init("argon2i", sizeof("argon2i")-1, 0);
578 const php_password_algo* ret = php_password_algo_find(n);
579 zend_string_release(n);
580 return ret;
581 }
582 case 3:
583 {
584 zend_string *n = zend_string_init("argon2id", sizeof("argon2id")-1, 0);
585 const php_password_algo* ret = php_password_algo_find(n);
586 zend_string_release(n);
587 return ret;
588 }
589 #endif
590 }
591 return NULL;
592 }
593
594 if (Z_TYPE_P(arg) != IS_STRING) {
595 return NULL;
596 }
597
598 return php_password_algo_find(Z_STR_P(arg));
599 }
php_password_algo_find_zval(zval * arg)600 static const php_password_algo* php_password_algo_find_zval(zval *arg) {
601 return php_password_algo_find_zval_ex(arg, php_password_algo_default());
602 }
603
php_password_algo_extract_ident(const zend_string * hash)604 zend_string *php_password_algo_extract_ident(const zend_string* hash) {
605 const char *ident, *ident_end;
606
607 if (!hash || ZSTR_LEN(hash) < 3) {
608 /* Minimum prefix: "$x$" */
609 return NULL;
610 }
611
612 ident = ZSTR_VAL(hash) + 1;
613 ident_end = strchr(ident, '$');
614 if (!ident_end) {
615 /* No terminating '$' */
616 return NULL;
617 }
618
619 return zend_string_init(ident, ident_end - ident, 0);
620 }
621
php_password_algo_identify_ex(const zend_string * hash,const php_password_algo * default_algo)622 const php_password_algo* php_password_algo_identify_ex(const zend_string* hash, const php_password_algo *default_algo) {
623 const php_password_algo *algo;
624 zend_string *ident = php_password_algo_extract_ident(hash);
625
626 if (!ident) {
627 return default_algo;
628 }
629
630 algo = php_password_algo_find(ident);
631 zend_string_release(ident);
632 return (!algo || (algo->valid && !algo->valid(hash))) ? default_algo : algo;
633 }
634
635 /* {{{ proto array password_get_info(string $hash)
636 Retrieves information about a given hash */
PHP_FUNCTION(password_get_info)637 PHP_FUNCTION(password_get_info)
638 {
639 const php_password_algo *algo;
640 zend_string *hash, *ident;
641 zval options;
642
643 ZEND_PARSE_PARAMETERS_START(1, 1)
644 Z_PARAM_STR(hash)
645 ZEND_PARSE_PARAMETERS_END();
646
647 array_init(return_value);
648 array_init(&options);
649
650 ident = php_password_algo_extract_ident(hash);
651 algo = php_password_algo_find(ident);
652 if (!algo || (algo->valid && !algo->valid(hash))) {
653 if (ident) {
654 zend_string_release(ident);
655 }
656 add_assoc_null(return_value, "algo");
657 add_assoc_string(return_value, "algoName", "unknown");
658 add_assoc_zval(return_value, "options", &options);
659 return;
660 }
661
662 add_assoc_str(return_value, "algo", php_password_algo_extract_ident(hash));
663 zend_string_release(ident);
664
665 add_assoc_string(return_value, "algoName", algo->name);
666 if (algo->get_info &&
667 (FAILURE == algo->get_info(&options, hash))) {
668 zval_dtor(&options);
669 zval_dtor(return_value);
670 RETURN_NULL();
671 }
672 add_assoc_zval(return_value, "options", &options);
673 }
674 /** }}} */
675
676 /* {{{ proto bool password_needs_rehash(string $hash, mixed $algo[, array $options])
677 Determines if a given hash requires re-hashing based upon parameters */
PHP_FUNCTION(password_needs_rehash)678 PHP_FUNCTION(password_needs_rehash)
679 {
680 const php_password_algo *old_algo, *new_algo;
681 zend_string *hash;
682 zval *znew_algo;
683 zend_array *options = 0;
684
685 ZEND_PARSE_PARAMETERS_START(2, 3)
686 Z_PARAM_STR(hash)
687 Z_PARAM_ZVAL(znew_algo)
688 Z_PARAM_OPTIONAL
689 Z_PARAM_ARRAY_OR_OBJECT_HT(options)
690 ZEND_PARSE_PARAMETERS_END();
691
692 new_algo = php_password_algo_find_zval(znew_algo);
693 if (!new_algo) {
694 /* Unknown new algorithm, never prompt to rehash. */
695 RETURN_FALSE;
696 }
697
698 old_algo = php_password_algo_identify_ex(hash, NULL);
699 if (old_algo != new_algo) {
700 /* Different algorithm preferred, always rehash. */
701 RETURN_TRUE;
702 }
703
704 RETURN_BOOL(old_algo->needs_rehash(hash, options));
705 }
706 /* }}} */
707
708 /* {{{ proto bool password_verify(string password, string hash)
709 Verify a hash created using crypt() or password_hash() */
PHP_FUNCTION(password_verify)710 PHP_FUNCTION(password_verify)
711 {
712 zend_string *password, *hash;
713 const php_password_algo *algo;
714
715 ZEND_PARSE_PARAMETERS_START(2, 2)
716 Z_PARAM_STR(password)
717 Z_PARAM_STR(hash)
718 ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE);
719
720 algo = php_password_algo_identify(hash);
721 RETURN_BOOL(algo && (!algo->verify || algo->verify(password, hash)));
722 }
723 /* }}} */
724
725 /* {{{ proto string password_hash(string password, mixed algo[, array options = array()])
726 Hash a password */
PHP_FUNCTION(password_hash)727 PHP_FUNCTION(password_hash)
728 {
729 zend_string *password, *digest = NULL;
730 zval *zalgo;
731 const php_password_algo *algo;
732 zend_array *options = NULL;
733
734 ZEND_PARSE_PARAMETERS_START(2, 3)
735 Z_PARAM_STR(password)
736 Z_PARAM_ZVAL(zalgo)
737 Z_PARAM_OPTIONAL
738 Z_PARAM_ARRAY_OR_OBJECT_HT(options)
739 ZEND_PARSE_PARAMETERS_END();
740
741 algo = php_password_algo_find_zval(zalgo);
742 if (!algo) {
743 zend_string *algostr = zval_get_string(zalgo);
744 php_error_docref(NULL, E_WARNING, "Unknown password hashing algorithm: %s", ZSTR_VAL(algostr));
745 zend_string_release(algostr);
746 RETURN_NULL();
747 }
748
749 digest = algo->hash(password, options);
750 if (!digest) {
751 /* algo->hash should have raised an error. */
752 RETURN_NULL();
753 }
754
755 RETURN_NEW_STR(digest);
756 }
757 /* }}} */
758
759 /* {{{ proto array password_algos() */
PHP_FUNCTION(password_algos)760 PHP_FUNCTION(password_algos) {
761 zend_string *algo;
762
763 ZEND_PARSE_PARAMETERS_NONE();
764
765 array_init(return_value);
766 ZEND_HASH_FOREACH_STR_KEY(&php_password_algos, algo) {
767 add_next_index_str(return_value, zend_string_copy(algo));
768 } ZEND_HASH_FOREACH_END();
769 }
770 /* }}} */
771