diff options
| author | Sara Golemon <pollita@php.net> | 2018-10-15 14:58:34 -0400 |
|---|---|---|
| committer | Sara Golemon <pollita@php.net> | 2018-11-20 17:26:55 -0500 |
| commit | 534df87c9e3c28001986e70844e0ad04e5708d3d (patch) | |
| tree | 0d0175681d4c00ca3493e9cda3711544ab324da5 | |
| parent | f4faa69b575bd70aa6877d7c295834f5be1f133c (diff) | |
| download | php-git-534df87c9e3c28001986e70844e0ad04e5708d3d.tar.gz | |
Implement password mechanism registry
RFC: https://wiki.php.net/rfc/password_registry
| -rw-r--r-- | ext/standard/basic_functions.c | 3 | ||||
| -rw-r--r-- | ext/standard/password.c | 961 | ||||
| -rw-r--r-- | ext/standard/php_password.h | 31 | ||||
| -rw-r--r-- | ext/standard/tests/password/password_get_info.phpt | 8 | ||||
| -rw-r--r-- | ext/standard/tests/password/password_get_info_argon2.phpt | 4 | ||||
| -rw-r--r-- | ext/standard/tests/password/password_hash.phpt | 13 | ||||
| -rw-r--r-- | ext/standard/tests/password/password_hash_argon2.phpt | 28 | ||||
| -rw-r--r-- | ext/standard/tests/password/password_hash_error.phpt | 4 | ||||
| -rw-r--r-- | ext/standard/tests/password/password_needs_rehash.phpt | 8 | ||||
| -rw-r--r-- | ext/standard/tests/password/password_needs_rehash_error.phpt | 10 |
10 files changed, 633 insertions, 437 deletions
diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index 171ab79c8b..9aeb2fb3a9 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -1860,6 +1860,8 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_password_verify, 0, 0, 2) ZEND_ARG_INFO(0, password) ZEND_ARG_INFO(0, hash) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_INFO(arginfo_password_algos, 0) +ZEND_END_ARG_INFO(); /* }}} */ /* {{{ proc_open.c */ #ifdef PHP_CAN_SUPPORT_PROC_OPEN @@ -2926,6 +2928,7 @@ static const zend_function_entry basic_functions[] = { /* {{{ */ PHP_FE(password_get_info, arginfo_password_get_info) PHP_FE(password_needs_rehash, arginfo_password_needs_rehash) PHP_FE(password_verify, arginfo_password_verify) + PHP_FE(password_algos, arginfo_password_algos) PHP_FE(convert_uuencode, arginfo_convert_uuencode) PHP_FE(convert_uudecode, arginfo_convert_uudecode) diff --git a/ext/standard/password.c b/ext/standard/password.c index 5cf0d397f5..36c14b8ef2 100644 --- a/ext/standard/password.c +++ b/ext/standard/password.c @@ -37,61 +37,19 @@ #include "win32/winutil.h" #endif -PHP_MINIT_FUNCTION(password) /* {{{ */ -{ - REGISTER_LONG_CONSTANT("PASSWORD_DEFAULT", PHP_PASSWORD_DEFAULT, CONST_CS | CONST_PERSISTENT); - REGISTER_LONG_CONSTANT("PASSWORD_BCRYPT", PHP_PASSWORD_BCRYPT, CONST_CS | CONST_PERSISTENT); -#if HAVE_ARGON2LIB - REGISTER_LONG_CONSTANT("PASSWORD_ARGON2I", PHP_PASSWORD_ARGON2I, CONST_CS | CONST_PERSISTENT); - REGISTER_LONG_CONSTANT("PASSWORD_ARGON2ID", PHP_PASSWORD_ARGON2ID, CONST_CS | CONST_PERSISTENT); -#endif - - REGISTER_LONG_CONSTANT("PASSWORD_BCRYPT_DEFAULT_COST", PHP_PASSWORD_BCRYPT_COST, CONST_CS | CONST_PERSISTENT); -#if HAVE_ARGON2LIB - REGISTER_LONG_CONSTANT("PASSWORD_ARGON2_DEFAULT_MEMORY_COST", PHP_PASSWORD_ARGON2_MEMORY_COST, CONST_CS | CONST_PERSISTENT); - REGISTER_LONG_CONSTANT("PASSWORD_ARGON2_DEFAULT_TIME_COST", PHP_PASSWORD_ARGON2_TIME_COST, CONST_CS | CONST_PERSISTENT); - REGISTER_LONG_CONSTANT("PASSWORD_ARGON2_DEFAULT_THREADS", PHP_PASSWORD_ARGON2_THREADS, CONST_CS | CONST_PERSISTENT); -#endif - - return SUCCESS; -} -/* }}} */ +static zend_array php_password_algos; -static zend_string* php_password_get_algo_name(const php_password_algo algo) -{ - switch (algo) { - case PHP_PASSWORD_BCRYPT: - return zend_string_init("bcrypt", sizeof("bcrypt") - 1, 0); -#if HAVE_ARGON2LIB - case PHP_PASSWORD_ARGON2I: - return zend_string_init("argon2i", sizeof("argon2i") - 1, 0); - case PHP_PASSWORD_ARGON2ID: - return zend_string_init("argon2id", sizeof("argon2id") - 1, 0); -#endif - case PHP_PASSWORD_UNKNOWN: - default: - return zend_string_init("unknown", sizeof("unknown") - 1, 0); +int php_password_algo_register(const char *ident, const php_password_algo *algo) { + zval zalgo; + ZVAL_PTR(&zalgo, (php_password_algo*)algo); + if (zend_hash_str_add(&php_password_algos, ident, strlen(ident), &zalgo)) { + return SUCCESS; } + return FAILURE; } -static php_password_algo php_password_determine_algo(const zend_string *hash) -{ - const char *h = ZSTR_VAL(hash); - const size_t len = ZSTR_LEN(hash); - if (len == 60 && h[0] == '$' && h[1] == '2' && h[2] == 'y') { - return PHP_PASSWORD_BCRYPT; - } -#if HAVE_ARGON2LIB - if (len >= sizeof("$argon2id$")-1 && !memcmp(h, "$argon2id$", sizeof("$argon2id$")-1)) { - return PHP_PASSWORD_ARGON2ID; - } - - if (len >= sizeof("$argon2i$")-1 && !memcmp(h, "$argon2i$", sizeof("$argon2i$")-1)) { - return PHP_PASSWORD_ARGON2I; - } -#endif - - return PHP_PASSWORD_UNKNOWN; +void php_password_algo_unregister(const char *ident) { + zend_hash_str_del(&php_password_algos, ident, strlen(ident)); } static int php_password_salt_is_alphabet(const char *str, const size_t len) /* {{{ */ @@ -164,225 +122,12 @@ static zend_string* php_password_make_salt(size_t length) /* {{{ */ } /* }}} */ -#if HAVE_ARGON2LIB -static void extract_argon2_parameters(const php_password_algo algo, const zend_string *hash, - zend_long *v, zend_long *memory_cost, - zend_long *time_cost, zend_long *threads) /* {{{ */ -{ - if (algo == PHP_PASSWORD_ARGON2ID) { - sscanf(ZSTR_VAL(hash), "$%*[argon2id]$v=" ZEND_LONG_FMT "$m=" ZEND_LONG_FMT ",t=" ZEND_LONG_FMT ",p=" ZEND_LONG_FMT, v, memory_cost, time_cost, threads); - } else if (algo == PHP_PASSWORD_ARGON2I) { - sscanf(ZSTR_VAL(hash), "$%*[argon2i]$v=" ZEND_LONG_FMT "$m=" ZEND_LONG_FMT ",t=" ZEND_LONG_FMT ",p=" ZEND_LONG_FMT, v, memory_cost, time_cost, threads); - } - - return; -} -#endif - -/* {{{ proto array password_get_info(string $hash) -Retrieves information about a given hash */ -PHP_FUNCTION(password_get_info) -{ - php_password_algo algo; - zend_string *hash, *algo_name; - zval options; - - ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_STR(hash) - ZEND_PARSE_PARAMETERS_END(); - - array_init(&options); - - algo = php_password_determine_algo(hash); - algo_name = php_password_get_algo_name(algo); - - switch (algo) { - case PHP_PASSWORD_BCRYPT: - { - zend_long cost = PHP_PASSWORD_BCRYPT_COST; - sscanf(ZSTR_VAL(hash), "$2y$" ZEND_LONG_FMT "$", &cost); - add_assoc_long(&options, "cost", cost); - } - break; -#if HAVE_ARGON2LIB - case PHP_PASSWORD_ARGON2I: - case PHP_PASSWORD_ARGON2ID: - { - zend_long v = 0; - zend_long memory_cost = PHP_PASSWORD_ARGON2_MEMORY_COST; - zend_long time_cost = PHP_PASSWORD_ARGON2_TIME_COST; - zend_long threads = PHP_PASSWORD_ARGON2_THREADS; - - extract_argon2_parameters(algo, hash, &v, &memory_cost, &time_cost, &threads); - - add_assoc_long(&options, "memory_cost", memory_cost); - add_assoc_long(&options, "time_cost", time_cost); - add_assoc_long(&options, "threads", threads); - } - break; -#endif - case PHP_PASSWORD_UNKNOWN: - default: - break; - } - - array_init(return_value); - - add_assoc_long(return_value, "algo", algo); - add_assoc_str(return_value, "algoName", algo_name); - add_assoc_zval(return_value, "options", &options); -} -/** }}} */ - -/* {{{ proto bool password_needs_rehash(string $hash, int $algo[, array $options]) -Determines if a given hash requires re-hashing based upon parameters */ -PHP_FUNCTION(password_needs_rehash) -{ - zend_long new_algo = 0; - php_password_algo algo; - zend_string *hash; - HashTable *options = 0; - zval *option_buffer; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_STR(hash) - Z_PARAM_LONG(new_algo) - Z_PARAM_OPTIONAL - Z_PARAM_ARRAY_OR_OBJECT_HT(options) - ZEND_PARSE_PARAMETERS_END(); - - algo = php_password_determine_algo(hash); - - if ((zend_long)algo != new_algo) { - RETURN_TRUE; - } - - switch (algo) { - case PHP_PASSWORD_BCRYPT: - { - zend_long new_cost = PHP_PASSWORD_BCRYPT_COST, cost = 0; - - if (options && (option_buffer = zend_hash_str_find(options, "cost", sizeof("cost")-1)) != NULL) { - new_cost = zval_get_long(option_buffer); - } - - sscanf(ZSTR_VAL(hash), "$2y$" ZEND_LONG_FMT "$", &cost); - if (cost != new_cost) { - RETURN_TRUE; - } - } - break; -#if HAVE_ARGON2LIB - case PHP_PASSWORD_ARGON2I: - case PHP_PASSWORD_ARGON2ID: - { - zend_long v = 0; - zend_long new_memory_cost = PHP_PASSWORD_ARGON2_MEMORY_COST, memory_cost = 0; - zend_long new_time_cost = PHP_PASSWORD_ARGON2_TIME_COST, time_cost = 0; - zend_long new_threads = PHP_PASSWORD_ARGON2_THREADS, threads = 0; - - if (options && (option_buffer = zend_hash_str_find(options, "memory_cost", sizeof("memory_cost")-1)) != NULL) { - new_memory_cost = zval_get_long(option_buffer); - } - - if (options && (option_buffer = zend_hash_str_find(options, "time_cost", sizeof("time_cost")-1)) != NULL) { - new_time_cost = zval_get_long(option_buffer); - } - - if (options && (option_buffer = zend_hash_str_find(options, "threads", sizeof("threads")-1)) != NULL) { - new_threads = zval_get_long(option_buffer); - } - - extract_argon2_parameters(algo, hash, &v, &memory_cost, &time_cost, &threads); - - if (new_time_cost != time_cost || new_memory_cost != memory_cost || new_threads != threads) { - RETURN_TRUE; - } - } - break; -#endif - case PHP_PASSWORD_UNKNOWN: - default: - break; - } - RETURN_FALSE; -} -/* }}} */ - -/* {{{ proto bool password_verify(string password, string hash) -Verify a hash created using crypt() or password_hash() */ -PHP_FUNCTION(password_verify) -{ - zend_string *password, *hash; - php_password_algo algo; - - ZEND_PARSE_PARAMETERS_START(2, 2) - Z_PARAM_STR(password) - Z_PARAM_STR(hash) - ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE); - - algo = php_password_determine_algo(hash); - - switch(algo) { -#if HAVE_ARGON2LIB - case PHP_PASSWORD_ARGON2I: - case PHP_PASSWORD_ARGON2ID: - { - argon2_type type; - if (algo == PHP_PASSWORD_ARGON2ID) { - type = Argon2_id; - } else if (algo == PHP_PASSWORD_ARGON2I) { - type = Argon2_i; - } - RETURN_BOOL(ARGON2_OK == argon2_verify(ZSTR_VAL(hash), ZSTR_VAL(password), ZSTR_LEN(password), type)); - } - break; -#endif - case PHP_PASSWORD_BCRYPT: - case PHP_PASSWORD_UNKNOWN: - default: - { - size_t i; - int status = 0; - zend_string *ret = php_crypt(ZSTR_VAL(password), (int)ZSTR_LEN(password), ZSTR_VAL(hash), (int)ZSTR_LEN(hash), 1); - - if (!ret) { - RETURN_FALSE; - } - - if (ZSTR_LEN(ret) != ZSTR_LEN(hash) || ZSTR_LEN(hash) < 13) { - zend_string_free(ret); - RETURN_FALSE; - } - - /* We're using this method instead of == in order to provide - * resistance towards timing attacks. This is a constant time - * equality check that will always check every byte of both - * values. */ - for (i = 0; i < ZSTR_LEN(hash); i++) { - status |= (ZSTR_VAL(ret)[i] ^ ZSTR_VAL(hash)[i]); - } - - zend_string_free(ret); - - RETURN_BOOL(status == 0); - } - } - - RETURN_FALSE; -} -/* }}} */ - -static zend_string* php_password_get_salt(zval *return_value, size_t required_salt_len, HashTable *options) { +static zend_string* php_password_get_salt(zval *unused_, size_t required_salt_len, HashTable *options) { zend_string *buffer; zval *option_buffer; if (!options || !(option_buffer = zend_hash_str_find(options, "salt", sizeof("salt") - 1))) { - buffer = php_password_make_salt(required_salt_len); - if (!buffer) { - RETVAL_FALSE; - } - return buffer; + return php_password_make_salt(required_salt_len); } php_error_docref(NULL, E_DEPRECATED, "Use of the 'salt' option to password_hash is deprecated"); @@ -439,162 +184,556 @@ static zend_string* php_password_get_salt(zval *return_value, size_t required_sa } } -/* {{{ proto string password_hash(string password, int algo[, array options = array()]) +/* bcrypt implementation */ + +static zend_bool php_password_bcrypt_valid(const zend_string *hash) { + const char *h = ZSTR_VAL(hash); + return (ZSTR_LEN(hash) == 60) && + (h[0] == '$') && (h[1] == '2') && (h[2] == 'y'); +} + +static int php_password_bcrypt_get_info(zval *return_value, const zend_string *hash) { + zend_long cost = PHP_PASSWORD_BCRYPT_COST; + + if (!php_password_bcrypt_valid(hash)) { + /* Should never get called this way. */ + return FAILURE; + } + + sscanf(ZSTR_VAL(hash), "$2y$" ZEND_LONG_FMT "$", &cost); + add_assoc_long(return_value, "cost", cost); + + return SUCCESS; +} + +static zend_bool php_password_bcrypt_needs_rehash(const zend_string *hash, zend_array *options) { + zval *znew_cost; + zend_long old_cost = PHP_PASSWORD_BCRYPT_COST; + zend_long new_cost = PHP_PASSWORD_BCRYPT_COST; + + if (!php_password_bcrypt_valid(hash)) { + /* Should never get called this way. */ + return 1; + } + + sscanf(ZSTR_VAL(hash), "$2y$" ZEND_LONG_FMT "$", &old_cost); + if (options && (znew_cost = zend_hash_str_find(options, "cost", sizeof("cost")-1)) != NULL) { + new_cost = zval_get_long(znew_cost); + } + + return old_cost != new_cost; +} + +static zend_bool php_password_bcrypt_verify(const zend_string *password, const zend_string *hash) { + size_t i; + int status = 0; + zend_string *ret = php_crypt(ZSTR_VAL(password), (int)ZSTR_LEN(password), ZSTR_VAL(hash), (int)ZSTR_LEN(hash), 1); + + if (!ret) { + return 0; + } + + if (ZSTR_LEN(ret) != ZSTR_LEN(hash) || ZSTR_LEN(hash) < 13) { + zend_string_free(ret); + return 0; + } + + /* We're using this method instead of == in order to provide + * resistance towards timing attacks. This is a constant time + * equality check that will always check every byte of both + * values. */ + for (i = 0; i < ZSTR_LEN(hash); i++) { + status |= (ZSTR_VAL(ret)[i] ^ ZSTR_VAL(hash)[i]); + } + + zend_string_free(ret); + return status == 0; +} + +static zend_string* php_password_bcrypt_hash(const zend_string *password, zend_array *options) { + char hash_format[10]; + size_t hash_format_len; + zend_string *result, *hash, *salt; + zval *zcost; + zend_long cost = PHP_PASSWORD_BCRYPT_COST; + + if (options && (zcost = zend_hash_str_find(options, "cost", sizeof("cost")-1)) != NULL) { + cost = zval_get_long(zcost); + } + + if (cost < 4 || cost > 31) { + php_error_docref(NULL, E_WARNING, "Invalid bcrypt cost parameter specified: " ZEND_LONG_FMT, cost); + return NULL; + } + + hash_format_len = snprintf(hash_format, sizeof(hash_format), "$2y$%02" ZEND_LONG_FMT_SPEC "$", cost); + if (!(salt = php_password_get_salt(NULL, Z_UL(22), options))) { + return NULL; + } + ZSTR_VAL(salt)[ZSTR_LEN(salt)] = 0; + + hash = zend_string_alloc(ZSTR_LEN(salt) + hash_format_len, 0); + sprintf(ZSTR_VAL(hash), "%s%s", hash_format, ZSTR_VAL(salt)); + ZSTR_VAL(hash)[hash_format_len + ZSTR_LEN(salt)] = 0; + + zend_string_release_ex(salt, 0); + + /* This cast is safe, since both values are defined here in code and cannot overflow */ + result = php_crypt(ZSTR_VAL(password), (int)ZSTR_LEN(password), ZSTR_VAL(hash), (int)ZSTR_LEN(hash), 1); + zend_string_release_ex(hash, 0); + + if (!result) { + return NULL; + } + + if (ZSTR_LEN(result) < 13) { + zend_string_free(result); + return NULL; + } + + return result; +} + +const php_password_algo php_password_algo_bcrypt = { + "bcrypt", + php_password_bcrypt_hash, + php_password_bcrypt_verify, + php_password_bcrypt_needs_rehash, + php_password_bcrypt_get_info, + php_password_bcrypt_valid, +}; + + +#if HAVE_ARGON2LIB +/* argon2i/argon2id shared implementation */ + +static int extract_argon2_parameters(const zend_string *hash, + zend_long *v, zend_long *memory_cost, + zend_long *time_cost, zend_long *threads) /* {{{ */ +{ + const char *p = ZSTR_VAL(hash); + if (!hash || (ZSTR_LEN(hash) < sizeof("$argon2id$"))) { + return FAILURE; + } + if (!memcmp(p, "$argon2i$", sizeof("$argon2i$") - 1)) { + p += sizeof("$argon2i$") - 1; + } else if (!memcmp(p, "$argon2id$", sizeof("$argon2id$") - 1)) { + p += sizeof("$argon2id$") - 1; + } else { + return FAILURE; + } + + sscanf(p, "v=" ZEND_LONG_FMT "$m=" ZEND_LONG_FMT ",t=" ZEND_LONG_FMT ",p=" ZEND_LONG_FMT, + v, memory_cost, time_cost, threads); + + return SUCCESS; +} +/* }}} */ + +static int php_password_argon2_get_info(zval *return_value, const zend_string *hash) { + zend_long v = 0; + zend_long memory_cost = PHP_PASSWORD_ARGON2_MEMORY_COST; + zend_long time_cost = PHP_PASSWORD_ARGON2_TIME_COST; + zend_long threads = PHP_PASSWORD_ARGON2_THREADS; + + extract_argon2_parameters(hash, &v, &memory_cost, &time_cost, &threads); + + add_assoc_long(return_value, "memory_cost", memory_cost); + add_assoc_long(return_value, "time_cost", time_cost); + add_assoc_long(return_value, "threads", threads); + + return SUCCESS; +} + +static zend_bool php_password_argon2_needs_rehash(const zend_string *hash, zend_array *options) { + zend_long v = 0; + zend_long new_memory_cost = PHP_PASSWORD_ARGON2_MEMORY_COST, memory_cost = 0; + zend_long new_time_cost = PHP_PASSWORD_ARGON2_TIME_COST, time_cost = 0; + zend_long new_threads = PHP_PASSWORD_ARGON2_THREADS, threads = 0; + zval *option_buffer; + + if (options && (option_buffer = zend_hash_str_find(options, "memory_cost", sizeof("memory_cost")-1)) != NULL) { + new_memory_cost = zval_get_long(option_buffer); + } + + if (options && (option_buffer = zend_hash_str_find(options, "time_cost", sizeof("time_cost")-1)) != NULL) { + new_time_cost = zval_get_long(option_buffer); + } + + if (options && (option_buffer = zend_hash_str_find(options, "threads", sizeof("threads")-1)) != NULL) { + new_threads = zval_get_long(option_buffer); + } + + extract_argon2_parameters(hash, &v, &memory_cost, &time_cost, &threads); + + return (new_time_cost != time_cost) || + (new_memory_cost != memory_cost) || + (new_threads != threads); +} + +static zend_string *php_password_argon2_hash(const zend_string *password, zend_array *options, argon2_type type) { + zval *option_buffer; + zend_string *salt, *out, *encoded; + size_t time_cost = PHP_PASSWORD_ARGON2_TIME_COST; + size_t memory_cost = PHP_PASSWORD_ARGON2_MEMORY_COST; + size_t threads = PHP_PASSWORD_ARGON2_THREADS; + size_t encoded_len; + int status = 0; + + if (options && (option_buffer = zend_hash_str_find(options, "memory_cost", sizeof("memory_cost")-1)) != NULL) { + memory_cost = zval_get_long(option_buffer); + } + + if (memory_cost > ARGON2_MAX_MEMORY || memory_cost < ARGON2_MIN_MEMORY) { + php_error_docref(NULL, E_WARNING, "Memory cost is outside of allowed memory range"); + return NULL; + } + + if (options && (option_buffer = zend_hash_str_find(options, "time_cost", sizeof("time_cost")-1)) != NULL) { + time_cost = zval_get_long(option_buffer); + } + + if (time_cost > ARGON2_MAX_TIME || time_cost < ARGON2_MIN_TIME) { + php_error_docref(NULL, E_WARNING, "Time cost is outside of allowed time range"); + return NULL; + } + + if (options && (option_buffer = zend_hash_str_find(options, "threads", sizeof("threads")-1)) != NULL) { + threads = zval_get_long(option_buffer); + } + + if (threads > ARGON2_MAX_LANES || threads == 0) { + php_error_docref(NULL, E_WARNING, "Invalid number of threads"); + return NULL; + } + + if (!(salt = php_password_get_salt(NULL, Z_UL(16), options))) { + return NULL; + } + + out = zend_string_alloc(32, 0); + encoded_len = argon2_encodedlen( + time_cost, + memory_cost, + threads, + (uint32_t)ZSTR_LEN(salt), + ZSTR_LEN(out), + type + ); + + encoded = zend_string_alloc(encoded_len - 1, 0); + status = argon2_hash( + time_cost, + memory_cost, + threads, + ZSTR_VAL(password), + ZSTR_LEN(password), + ZSTR_VAL(salt), + ZSTR_LEN(salt), + ZSTR_VAL(out), + ZSTR_LEN(out), + ZSTR_VAL(encoded), + encoded_len, + type, + ARGON2_VERSION_NUMBER + ); + + zend_string_release_ex(out, 0); + zend_string_release_ex(salt, 0); + + if (status != ARGON2_OK) { + zend_string_efree(encoded); + php_error_docref(NULL, E_WARNING, "%s", argon2_error_message(status)); + return NULL; + } + + ZSTR_VAL(encoded)[ZSTR_LEN(encoded)] = 0; + return encoded; +} + +/* argon2i specific methods */ + +static zend_bool php_password_argon2i_verify(const zend_string *password, const zend_string *hash) { + return ARGON2_OK == argon2_verify(ZSTR_VAL(hash), ZSTR_VAL(password), ZSTR_LEN(password), Argon2_i); +} + +static zend_string *php_password_argon2i_hash(const zend_string *password, zend_array *options) { + return php_password_argon2_hash(password, options, Argon2_i); +} + +const php_password_algo php_password_algo_argon2i = { + "argon2i", + php_password_argon2i_hash, + php_password_argon2i_verify, + php_password_argon2_needs_rehash, + php_password_argon2_get_info, + NULL, +}; + +/* argon2id specific methods */ + +static zend_bool php_password_argon2id_verify(const zend_string *password, const zend_string *hash) { + return ARGON2_OK == argon2_verify(ZSTR_VAL(hash), ZSTR_VAL(password), ZSTR_LEN(password), Argon2_id); +} + +static zend_string *php_password_argon2id_hash(const zend_string *password, zend_array *options) { + return php_password_argon2_hash(password, options, Argon2_id); +} + +const php_password_algo php_password_algo_argon2id = { + "argon2id", + php_password_argon2id_hash, + php_password_argon2id_verify, + php_password_argon2_needs_rehash, + php_password_argon2_get_info, + NULL, +}; +#endif + +PHP_MINIT_FUNCTION(password) /* {{{ */ +{ + zend_hash_init(&php_password_algos, 4, NULL, ZVAL_PTR_DTOR, 1); + REGISTER_NULL_CONSTANT("PASSWORD_DEFAULT", CONST_CS | CONST_PERSISTENT); + + if (FAILURE == php_password_algo_register("2y", &php_password_algo_bcrypt)) { + return FAILURE; + } + REGISTER_STRING_CONSTANT("PASSWORD_BCRYPT", "2y", CONST_CS | CONST_PERSISTENT); + +#if HAVE_ARGON2LIB + if (FAILURE == php_password_algo_register("argon2i", &php_password_algo_argon2i)) { + return FAILURE; + } + REGISTER_STRING_CONSTANT("PASSWORD_ARGON2I", "argon2i", CONST_CS | CONST_PERSISTENT); + + if (FAILURE == php_password_algo_register("argon2id", &php_password_algo_argon2id)) { + return FAILURE; + } + REGISTER_STRING_CONSTANT("PASSWORD_ARGON2ID", "argon2id", CONST_CS | CONST_PERSISTENT); +#endif + + REGISTER_LONG_CONSTANT("PASSWORD_BCRYPT_DEFAULT_COST", PHP_PASSWORD_BCRYPT_COST, CONST_CS | CONST_PERSISTENT); +#if HAVE_ARGON2LIB + REGISTER_LONG_CONSTANT("PASSWORD_ARGON2_DEFAULT_MEMORY_COST", PHP_PASSWORD_ARGON2_MEMORY_COST, CONST_CS | CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("PASSWORD_ARGON2_DEFAULT_TIME_COST", PHP_PASSWORD_ARGON2_TIME_COST, CONST_CS | CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("PASSWORD_ARGON2_DEFAULT_THREADS", PHP_PASSWORD_ARGON2_THREADS, CONST_CS | CONST_PERSISTENT); +#endif + + return SUCCESS; +} +/* }}} */ + +const php_password_algo* php_password_algo_default() { + return &php_password_algo_bcrypt; +} + +const php_password_algo* php_password_algo_find(const zend_string *ident) { + zval *tmp; + + if (!ident) { + return NULL; + } + + tmp = zend_hash_find(&php_password_algos, (zend_string*)ident); + if (!tmp || (Z_TYPE_P(tmp) != IS_PTR)) { + return NULL; + } + + return Z_PTR_P(tmp); +} + +static const php_password_algo* php_password_algo_find_zval_ex(zval *arg, const php_password_algo* default_algo) { + if (!arg || (Z_TYPE_P(arg) == IS_NULL)) { + return default_algo; + } + + if (Z_TYPE_P(arg) == IS_LONG) { + switch (Z_LVAL_P(arg)) { + case 0: return default_algo; + case 1: return &php_password_algo_bcrypt; +#if HAVE_ARGON2LIB + case 2: return &php_password_algo_argon2i; + case 3: return &php_password_algo_argon2id; +#endif + } + return NULL; + } + + if (Z_TYPE_P(arg) != IS_STRING) { + return NULL; + } + + return php_password_algo_find(Z_STR_P(arg)); +} +static const php_password_algo* php_password_algo_find_zval(zval *arg) { + return php_password_algo_find_zval_ex(arg, php_password_algo_default()); +} + +zend_string *php_password_algo_extract_ident(const zend_string* hash) { + const char *ident, *ident_end; + + if (!hash || ZSTR_LEN(hash) < 3) { + /* Minimum prefix: "$x$" */ + return NULL; + } + + ident = ZSTR_VAL(hash) + 1; + ident_end = strchr(ident, '$'); + if (!ident_end) { + /* No terminating '$' */ + return NULL; + } + + return zend_string_init(ident, ident_end - ident, 0); +} + +const php_password_algo* php_password_algo_identify_ex(const zend_string* hash, const php_password_algo *default_algo) { + const php_password_algo *algo; + zend_string *ident = php_password_algo_extract_ident(hash); + + if (!ident) { + return default_algo; + } + + algo = php_password_algo_find(ident); + zend_string_release(ident); + return (!algo || (algo->valid && !algo->valid(hash))) ? default_algo : algo; +} + +/* {{{ proto array password_get_info(string $hash) +Retrieves information about a given hash */ +PHP_FUNCTION(password_get_info) +{ + const php_password_algo *algo; + zend_string *hash, *ident; + zval options; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(hash) + ZEND_PARSE_PARAMETERS_END(); + + array_init(return_value); + array_init(&options); + + ident = php_password_algo_extract_ident(hash); + algo = php_password_algo_find(ident); + if (!algo || (algo->valid && !algo->valid(hash))) { + if (ident) { + zend_string_release(ident); + } + add_assoc_null(return_value, "algo"); + add_assoc_string(return_value, "algoName", "unknown"); + add_assoc_zval(return_value, "options", &options); + return; + } + + add_assoc_str(return_value, "algo", php_password_algo_extract_ident(hash)); + zend_string_release(ident); + + add_assoc_string(return_value, "algoName", algo->name); + if (algo->get_info && + (FAILURE == algo->get_info(&options, hash))) { + zval_dtor(&options); + zval_dtor(return_value); + RETURN_NULL(); + } + add_assoc_zval(return_value, "options", &options); +} +/** }}} */ + +/* {{{ proto bool password_needs_rehash(string $hash, mixed $algo[, array $options]) +Determines if a given hash requires re-hashing based upon parameters */ +PHP_FUNCTION(password_needs_rehash) +{ + const php_password_algo *old_algo, *new_algo; + zend_string *hash; + zval *znew_algo; + zend_array *options = 0; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STR(hash) + Z_PARAM_ZVAL_DEREF(znew_algo) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_OBJECT_HT(options) + ZEND_PARSE_PARAMETERS_END(); + + new_algo = php_password_algo_find_zval_ex(znew_algo, NULL); + if (!new_algo) { + /* Unknown new algorithm, never prompt to rehash. */ + RETURN_FALSE; + } + + old_algo = php_password_algo_identify_ex(hash, NULL); + if (old_algo != new_algo) { + /* Different algorithm preferred, always rehash. */ + RETURN_TRUE; + } + + RETURN_BOOL(old_algo->needs_rehash(hash, options)); +} +/* }}} */ + +/* {{{ proto bool password_verify(string password, string hash) +Verify a hash created using crypt() or password_hash() */ +PHP_FUNCTION(password_verify) +{ + zend_string *password, *hash; + const php_password_algo *algo; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_STR(password) + Z_PARAM_STR(hash) + ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE); + + algo = php_password_algo_identify(hash); + RETURN_BOOL(algo && (!algo->verify || algo->verify(password, hash))); +} +/* }}} */ + +/* {{{ proto string password_hash(string password, mixed algo[, array options = array()]) Hash a password */ PHP_FUNCTION(password_hash) { - zend_string *password; - zend_long algo = PHP_PASSWORD_DEFAULT; - HashTable *options = NULL; + zend_string *password, *digest = NULL; + zval *zalgo; + const php_password_algo *algo; + zend_array *options = NULL; ZEND_PARSE_PARAMETERS_START(2, 3) Z_PARAM_STR(password) - Z_PARAM_LONG(algo) + Z_PARAM_ZVAL_DEREF(zalgo) Z_PARAM_OPTIONAL Z_PARAM_ARRAY_OR_OBJECT_HT(options) ZEND_PARSE_PARAMETERS_END(); - switch (algo) { - case PHP_PASSWORD_BCRYPT: - { - char hash_format[10]; - size_t hash_format_len; - zend_string *result, *hash, *salt; - zval *option_buffer; - zend_long cost = PHP_PASSWORD_BCRYPT_COST; - - if (options && (option_buffer = zend_hash_str_find(options, "cost", sizeof("cost")-1)) != NULL) { - cost = zval_get_long(option_buffer); - } - - if (cost < 4 || cost > 31) { - php_error_docref(NULL, E_WARNING, "Invalid bcrypt cost parameter specified: " ZEND_LONG_FMT, cost); - RETURN_NULL(); - } - - hash_format_len = snprintf(hash_format, sizeof(hash_format), "$2y$%02" ZEND_LONG_FMT_SPEC "$", cost); - if (!(salt = php_password_get_salt(return_value, Z_UL(22), options))) { - return; - } - ZSTR_VAL(salt)[ZSTR_LEN(salt)] = 0; - - hash = zend_string_alloc(ZSTR_LEN(salt) + hash_format_len, 0); - sprintf(ZSTR_VAL(hash), "%s%s", hash_format, ZSTR_VAL(salt)); - ZSTR_VAL(hash)[hash_format_len + ZSTR_LEN(salt)] = 0; - - zend_string_release_ex(salt, 0); - - /* This cast is safe, since both values are defined here in code and cannot overflow */ - result = php_crypt(ZSTR_VAL(password), (int)ZSTR_LEN(password), ZSTR_VAL(hash), (int)ZSTR_LEN(hash), 1); - zend_string_release_ex(hash, 0); - - if (!result) { - RETURN_FALSE; - } - - if (ZSTR_LEN(result) < 13) { - zend_string_free(result); - RETURN_FALSE; - } - - RETURN_STR(result); - } - break; -#if HAVE_ARGON2LIB - case PHP_PASSWORD_ARGON2I: - case PHP_PASSWORD_ARGON2ID: - { - zval *option_buffer; - zend_string *salt, *out, *encoded; - size_t time_cost = PHP_PASSWORD_ARGON2_TIME_COST; - size_t memory_cost = PHP_PASSWORD_ARGON2_MEMORY_COST; - size_t threads = PHP_PASSWORD_ARGON2_THREADS; - argon2_type type; - if (algo == PHP_PASSWORD_ARGON2ID) { - type = Argon2_id; - } else if (algo == PHP_PASSWORD_ARGON2I) { - type = Argon2_i; - } - size_t encoded_len; - int status = 0; - - if (options && (option_buffer = zend_hash_str_find(options, "memory_cost", sizeof("memory_cost")-1)) != NULL) { - memory_cost = zval_get_long(option_buffer); - } - - if (memory_cost > ARGON2_MAX_MEMORY || memory_cost < ARGON2_MIN_MEMORY) { - php_error_docref(NULL, E_WARNING, "Memory cost is outside of allowed memory range"); - RETURN_NULL(); - } - - if (options && (option_buffer = zend_hash_str_find(options, "time_cost", sizeof("time_cost")-1)) != NULL) { - time_cost = zval_get_long(option_buffer); - } - - if (time_cost > ARGON2_MAX_TIME || time_cost < ARGON2_MIN_TIME) { - php_error_docref(NULL, E_WARNING, "Time cost is outside of allowed time range"); - RETURN_NULL(); - } - - if (options && (option_buffer = zend_hash_str_find(options, "threads", sizeof("threads")-1)) != NULL) { - threads = zval_get_long(option_buffer); - } - - if (threads > ARGON2_MAX_LANES || threads == 0) { - php_error_docref(NULL, E_WARNING, "Invalid number of threads"); - RETURN_NULL(); - } - - if (!(salt = php_password_get_salt(return_value, Z_UL(16), options))) { - return; - } - - out = zend_string_alloc(32, 0); - encoded_len = argon2_encodedlen( - time_cost, - memory_cost, - threads, - (uint32_t)ZSTR_LEN(salt), - ZSTR_LEN(out), - type - ); - - encoded = zend_string_alloc(encoded_len - 1, 0); - status = argon2_hash( - time_cost, - memory_cost, - threads, - ZSTR_VAL(password), - ZSTR_LEN(password), - ZSTR_VAL(salt), - ZSTR_LEN(salt), - ZSTR_VAL(out), - ZSTR_LEN(out), - ZSTR_VAL(encoded), - encoded_len, - type, - ARGON2_VERSION_NUMBER - ); - - zend_string_release_ex(out, 0); - zend_string_release_ex(salt, 0); - - if (status != ARGON2_OK) { - zend_string_efree(encoded); - php_error_docref(NULL, E_WARNING, "%s", argon2_error_message(status)); - RETURN_FALSE; - } - - ZSTR_VAL(encoded)[ZSTR_LEN(encoded)] = 0; - RETURN_NEW_STR(encoded); - } - break; -#endif - case PHP_PASSWORD_UNKNOWN: - default: - php_error_docref(NULL, E_WARNING, "Unknown password hashing algorithm: " ZEND_LONG_FMT, algo); - RETURN_NULL(); + algo = php_password_algo_find_zval(zalgo); + if (!algo) { + zend_string *algostr = zval_get_string(zalgo); + php_error_docref(NULL, E_WARNING, "Unknown password hashing algorithm: %s", ZSTR_VAL(algostr)); + zend_string_release(algostr); + RETURN_NULL(); + } + + digest = algo->hash(password, options); + if (!digest) { + /* algo->hash should have raised an error. */ + RETURN_NULL(); } + + RETURN_NEW_STR(digest); +} +/* }}} */ + +/* {{{ proto array password_algos() */ +PHP_FUNCTION(password_algos) { + zend_string *algo; + + ZEND_PARSE_PARAMETERS_NONE(); + + array_init(return_value); + ZEND_HASH_FOREACH_STR_KEY(&php_password_algos, algo) { + add_next_index_str(return_value, zend_string_copy(algo)); + } ZEND_HASH_FOREACH_END(); } /* }}} */ diff --git a/ext/standard/php_password.h b/ext/standard/php_password.h index 0c2f83c650..4d9d6b46f1 100644 --- a/ext/standard/php_password.h +++ b/ext/standard/php_password.h @@ -24,6 +24,7 @@ PHP_FUNCTION(password_hash); PHP_FUNCTION(password_verify); PHP_FUNCTION(password_needs_rehash); PHP_FUNCTION(password_get_info); +PHP_FUNCTION(password_algos); PHP_MINIT_FUNCTION(password); @@ -36,14 +37,32 @@ PHP_MINIT_FUNCTION(password); #define PHP_PASSWORD_ARGON2_THREADS 2 #endif -typedef enum { - PHP_PASSWORD_UNKNOWN, - PHP_PASSWORD_BCRYPT, +typedef struct _php_password_algo { + const char *name; + zend_string *(*hash)(const zend_string *password, zend_array *options); + zend_bool (*verify)(const zend_string *password, const zend_string *hash); + zend_bool (*needs_rehash)(const zend_string *password, zend_array *options); + int (*get_info)(zval *return_value, const zend_string *hash); + zend_bool (*valid)(const zend_string *hash); +} php_password_algo; + +extern const php_password_algo php_password_algo_bcrypt; #if HAVE_ARGON2LIB - PHP_PASSWORD_ARGON2I, - PHP_PASSWORD_ARGON2ID, +extern const php_password_algo php_password_algo_argon2i; +extern const php_password_algo php_password_algo_argon2id; #endif -} php_password_algo; + +PHPAPI int php_password_algo_register(const char*, const php_password_algo*); +PHPAPI void php_password_algo_unregister(const char*); +PHPAPI const php_password_algo* php_password_algo_default(); +PHPAPI zend_string *php_password_algo_extract_ident(const zend_string*); +PHPAPI const php_password_algo* php_password_algo_find(const zend_string*); + +PHPAPI const php_password_algo* php_password_algo_identify_ex(const zend_string*, const php_password_algo*); +static inline const php_password_algo* php_password_algo_identify(const zend_string *hash) { + return php_password_algo_identify_ex(hash, php_password_algo_default()); +} + #endif diff --git a/ext/standard/tests/password/password_get_info.phpt b/ext/standard/tests/password/password_get_info.phpt index 4c8dc04ff8..22c4ce4c52 100644 --- a/ext/standard/tests/password/password_get_info.phpt +++ b/ext/standard/tests/password/password_get_info.phpt @@ -17,7 +17,7 @@ echo "OK!"; --EXPECT-- array(3) { ["algo"]=> - int(1) + string(2) "2y" ["algoName"]=> string(6) "bcrypt" ["options"]=> @@ -28,7 +28,7 @@ array(3) { } array(3) { ["algo"]=> - int(1) + string(2) "2y" ["algoName"]=> string(6) "bcrypt" ["options"]=> @@ -39,7 +39,7 @@ array(3) { } array(3) { ["algo"]=> - int(0) + NULL ["algoName"]=> string(7) "unknown" ["options"]=> @@ -48,7 +48,7 @@ array(3) { } array(3) { ["algo"]=> - int(0) + NULL ["algoName"]=> string(7) "unknown" ["options"]=> diff --git a/ext/standard/tests/password/password_get_info_argon2.phpt b/ext/standard/tests/password/password_get_info_argon2.phpt index b67fc5790e..1965d103db 100644 --- a/ext/standard/tests/password/password_get_info_argon2.phpt +++ b/ext/standard/tests/password/password_get_info_argon2.phpt @@ -15,7 +15,7 @@ echo "OK!"; --EXPECT-- array(3) { ["algo"]=> - int(2) + string(7) "argon2i" ["algoName"]=> string(7) "argon2i" ["options"]=> @@ -30,7 +30,7 @@ array(3) { } array(3) { ["algo"]=> - int(3) + string(8) "argon2id" ["algoName"]=> string(8) "argon2id" ["options"]=> diff --git a/ext/standard/tests/password/password_hash.phpt b/ext/standard/tests/password/password_hash.phpt index 47335c376a..2ddfda32d1 100644 --- a/ext/standard/tests/password/password_hash.phpt +++ b/ext/standard/tests/password/password_hash.phpt @@ -6,13 +6,22 @@ Test normal operation of password_hash() var_dump(strlen(password_hash("foo", PASSWORD_BCRYPT))); -$hash = password_hash("foo", PASSWORD_BCRYPT); +$algos = [ + PASSWORD_BCRYPT, + '2y', + 1, +]; -var_dump($hash === crypt("foo", $hash)); +foreach ($algos as $algo) { + $hash = password_hash("foo", $algo); + var_dump($hash === crypt("foo", $hash)); +} echo "OK!"; ?> --EXPECT-- int(60) bool(true) +bool(true) +bool(true) OK! diff --git a/ext/standard/tests/password/password_hash_argon2.phpt b/ext/standard/tests/password/password_hash_argon2.phpt index 184bac4ac6..a7612effa9 100644 --- a/ext/standard/tests/password/password_hash_argon2.phpt +++ b/ext/standard/tests/password/password_hash_argon2.phpt @@ -9,15 +9,33 @@ if (!defined('PASSWORD_ARGON2ID')) die('skip password_hash not built with Argon2 $password = "the password for testing 12345!"; -$hash = password_hash($password, PASSWORD_ARGON2I); -var_dump(password_verify($password, $hash)); - -$hash = password_hash($password, PASSWORD_ARGON2ID); -var_dump(password_verify($password, $hash)); +$algos = [ + PASSWORD_ARGON2I, + 'argon2i', + 2, + PASSWORD_ARGON2ID, + 'argon2id', + 3, +]; +foreach ($algos as $algo) { + $hash = password_hash($password, $algo); + var_dump(password_verify($password, $hash)); + var_dump(password_get_info($hash)['algo']); +} echo "OK!"; ?> --EXPECT-- bool(true) +string(7) "argon2i" +bool(true) +string(7) "argon2i" +bool(true) +string(7) "argon2i" +bool(true) +string(8) "argon2id" +bool(true) +string(8) "argon2id" bool(true) +string(8) "argon2id" OK! diff --git a/ext/standard/tests/password/password_hash_error.phpt b/ext/standard/tests/password/password_hash_error.phpt index 5f576c3122..6416eca91b 100644 --- a/ext/standard/tests/password/password_hash_error.phpt +++ b/ext/standard/tests/password/password_hash_error.phpt @@ -29,7 +29,9 @@ NULL Warning: password_hash() expects at least 2 parameters, 1 given in %s on line %d NULL -Warning: password_hash() expects parameter 2 to be int, array given in %s on line %d +Notice: Array to string conversion in %s on line %d + +Warning: password_hash(): Unknown password hashing algorithm: Array in %s on line %d NULL Warning: password_hash(): Unknown password hashing algorithm: 19 in %s on line %d diff --git a/ext/standard/tests/password/password_needs_rehash.phpt b/ext/standard/tests/password/password_needs_rehash.phpt index 8efd0add8f..688d57ed32 100644 --- a/ext/standard/tests/password/password_needs_rehash.phpt +++ b/ext/standard/tests/password/password_needs_rehash.phpt @@ -6,9 +6,13 @@ Test normal operation of password_needs_rehash() // Invalid Hash, always rehash var_dump(password_needs_rehash('', PASSWORD_BCRYPT)); +var_dump(password_needs_rehash('', 1)); +var_dump(password_needs_rehash('', '2y')); // Valid, as it's an unknown algorithm +var_dump(password_needs_rehash('', PASSWORD_DEFAULT)); var_dump(password_needs_rehash('', 0)); +var_dump(password_needs_rehash('', NULL)); // Valid with cost the same var_dump(password_needs_rehash('$2y$10$MTIzNDU2Nzg5MDEyMzQ1Nej0NmcAWSLR.oP7XOR9HD/vjUuOj100y', PASSWORD_BCRYPT, array('cost' => 10))); @@ -35,6 +39,10 @@ echo "OK!"; ?> --EXPECT-- bool(true) +bool(true) +bool(true) +bool(false) +bool(false) bool(false) bool(false) bool(false) diff --git a/ext/standard/tests/password/password_needs_rehash_error.phpt b/ext/standard/tests/password/password_needs_rehash_error.phpt index 65766cb3fe..7180d11de7 100644 --- a/ext/standard/tests/password/password_needs_rehash_error.phpt +++ b/ext/standard/tests/password/password_needs_rehash_error.phpt @@ -7,11 +7,11 @@ var_dump(password_needs_rehash()); var_dump(password_needs_rehash('')); -var_dump(password_needs_rehash('', "foo")); +var_dump(password_needs_rehash('', [])); -var_dump(password_needs_rehash(array(), 1)); +var_dump(password_needs_rehash(array(), PASSWORD_BCRYPT)); -var_dump(password_needs_rehash("", 1, "foo")); +var_dump(password_needs_rehash("", PASSWORD_BCRYPT, "foo")); echo "OK!"; ?> @@ -21,9 +21,7 @@ NULL Warning: password_needs_rehash() expects at least 2 parameters, 1 given in %s on line %d NULL - -Warning: password_needs_rehash() expects parameter 2 to be int, string given in %s on line %d -NULL +bool(false) Warning: password_needs_rehash() expects parameter 1 to be string, array given in %s on line %d NULL |
