xref: /PHP-7.4/ext/standard/password.c (revision ea1b8788)
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