diff --git a/ext/standard/config.m4 b/ext/standard/config.m4 index 26eb05632172b..0b407ca27cf2e 100644 --- a/ext/standard/config.m4 +++ b/ext/standard/config.m4 @@ -550,6 +550,38 @@ dnl Check for getrandom on newer Linux kernels dnl AC_CHECK_DECLS([getrandom]) +dnl +dnl Check for argon2 +dnl +PHP_ARG_WITH(password-argon2, for Argon2 support, +[ --with-password-argon2[=DIR] Include Argon2 support in password_*. DIR is the Argon2 shared library path]]) + +if test "$PHP_PASSWORD_ARGON2" != "no"; then + AC_MSG_CHECKING([for Argon2 library]) + for i in $PHP_PASSWORD_ARGON2 /usr /usr/local ; do + if test -r $i/include/argon2.h; then + ARGON2_DIR=$i; + AC_MSG_RESULT(found in $i) + break + fi + done + + if test -z "$ARGON2_DIR"; then + AC_MSG_RESULT([not found]) + AC_MSG_ERROR([Please ensure the argon2 header and library are installed]) + fi + + PHP_ADD_LIBRARY_WITH_PATH(argon2, $ARGON2_DIR/$PHP_LIBDIR) + PHP_ADD_INCLUDE($ARGON2_DIR/include) + + AC_CHECK_LIB(argon2, argon2_hash, [ + LIBS="$LIBS -largon2" + AC_DEFINE(HAVE_ARGON2LIB, 1, [ Define to 1 if you have the header file ]) + ], [ + AC_MSG_ERROR([Problem with libargon2.(a|so). Please verify that Argon2 header and libaries are installed]) + ]) +fi + dnl dnl Setup extension sources dnl diff --git a/ext/standard/config.w32 b/ext/standard/config.w32 index adff3d8c878af..b752d9cb8c374 100644 --- a/ext/standard/config.w32 +++ b/ext/standard/config.w32 @@ -1,6 +1,17 @@ // vim:ft=javascript // $Id$ +ARG_WITH("password-argon2", "Argon2 support", "no"); + +if (PHP_PASSWORD_ARGON2 != "no") { + if (CHECK_LIB("Argon2Ref.lib", null, PHP_PASSWORD_ARGON2) + && CHECK_HEADER_ADD_INCLUDE("argon2.h", "CFLAGS")) { + AC_DEFINE('HAVE_ARGON2LIB', 1); + } else { + WARNING("Argon2 not enabled; libaries and headers not found"); + } +} + ARG_WITH("config-file-scan-dir", "Dir to check for additional php ini files", ""); AC_DEFINE("PHP_CONFIG_FILE_SCAN_DIR", PHP_CONFIG_FILE_SCAN_DIR); diff --git a/ext/standard/password.c b/ext/standard/password.c index d01ddb0563c6a..117db2bf1648f 100644 --- a/ext/standard/password.c +++ b/ext/standard/password.c @@ -13,6 +13,7 @@ | license@php.net so we can mail you a copy immediately. | +----------------------------------------------------------------------+ | Authors: Anthony Ferrara | + | Charles R. Portwood II | +----------------------------------------------------------------------+ */ @@ -30,6 +31,9 @@ #include "zend_interfaces.h" #include "info.h" #include "php_random.h" +#if HAVE_ARGON2LIB +#include "argon2.h" +#endif #if PHP_WIN32 #include "win32/winutil.h" @@ -39,8 +43,16 @@ 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); +#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; } @@ -51,6 +63,10 @@ static char* php_password_get_algo_name(const php_password_algo algo) switch (algo) { case PHP_PASSWORD_BCRYPT: return "bcrypt"; +#if HAVE_ARGON2LIB + case PHP_PASSWORD_ARGON2I: + return "argon2i"; +#endif case PHP_PASSWORD_UNKNOWN: default: return "unknown"; @@ -61,7 +77,12 @@ static php_password_algo php_password_determine_algo(const char *hash, const siz { if (len > 3 && hash[0] == '$' && hash[1] == '2' && hash[2] == 'y' && len == 60) { return PHP_PASSWORD_BCRYPT; + } +#if HAVE_ARGON2LIB + if (len >= sizeof("$argon2i$")-1 && !memcmp(hash, "$argon2i$", sizeof("$argon2i$")-1)) { + return PHP_PASSWORD_ARGON2I; } +#endif return PHP_PASSWORD_UNKNOWN; } @@ -143,6 +164,8 @@ static int php_password_make_salt(size_t length, char *ret) /* {{{ */ } /* }}} */ +/* {{{ proto array password_get_info(string $hash) +Retrieves information about a given hash */ PHP_FUNCTION(password_get_info) { php_password_algo algo; @@ -167,6 +190,21 @@ PHP_FUNCTION(password_get_info) add_assoc_long(&options, "cost", cost); } break; +#if HAVE_ARGON2LIB + case PHP_PASSWORD_ARGON2I: + { + 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; + + sscanf(hash, "$%*[argon2i]$v=" ZEND_LONG_FMT "$m=" ZEND_LONG_FMT ",t=" ZEND_LONG_FMT ",p=" ZEND_LONG_FMT, &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; @@ -178,7 +216,10 @@ PHP_FUNCTION(password_get_info) add_assoc_string(return_value, "algoName", algo_name); add_assoc_zval(return_value, "options", &options); } +/** }}} */ +/* {{{ proto boolean password_needs_rehash(string $hash, integer $algo[, array $options]) +Determines if a given hash requires re-hashing based upon parameters */ PHP_FUNCTION(password_needs_rehash) { zend_long new_algo = 0; @@ -213,14 +254,43 @@ PHP_FUNCTION(password_needs_rehash) } } break; +#if HAVE_ARGON2LIB + case PHP_PASSWORD_ARGON2I: + { + 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); + } + + sscanf(hash, "$%*[argon2i]$v=" ZEND_LONG_FMT "$m=" ZEND_LONG_FMT ",t=" ZEND_LONG_FMT ",p=" ZEND_LONG_FMT, &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 boolean password_make_salt(string password, string hash) +/* {{{ proto boolean password_verify(string password, string hash) Verify a hash created using crypt() or password_hash() */ PHP_FUNCTION(password_verify) { @@ -228,35 +298,62 @@ PHP_FUNCTION(password_verify) size_t i, password_len, hash_len; char *password, *hash; zend_string *ret; + php_password_algo algo; if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss", &password, &password_len, &hash, &hash_len) == FAILURE) { RETURN_FALSE; } - if ((ret = php_crypt(password, (int)password_len, hash, (int)hash_len, 1)) == NULL) { - RETURN_FALSE; - } - if (ZSTR_LEN(ret) != hash_len || hash_len < 13) { - zend_string_free(ret); - RETURN_FALSE; - } + algo = php_password_determine_algo(hash, (size_t) hash_len); - /* 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 < hash_len; i++) { - status |= (ZSTR_VAL(ret)[i] ^ hash[i]); - } + switch(algo) { +#if HAVE_ARGON2LIB + case PHP_PASSWORD_ARGON2I: + { + argon2_type type = Argon2_i; - zend_string_free(ret); + status = argon2_verify(hash, password, password_len, type); + + if (status == ARGON2_OK) { + RETURN_TRUE; + } - RETURN_BOOL(status == 0); + RETURN_FALSE; + } + break; +#endif + case PHP_PASSWORD_BCRYPT: + case PHP_PASSWORD_UNKNOWN: + default: + { + if ((ret = php_crypt(password, (int)password_len, hash, (int)hash_len, 1)) == NULL) { + RETURN_FALSE; + } + + if (ZSTR_LEN(ret) != hash_len || hash_len < 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 < hash_len; i++) { + status |= (ZSTR_VAL(ret)[i] ^ hash[i]); + } + + zend_string_free(ret); + + RETURN_BOOL(status == 0); + } + } + RETURN_FALSE; } /* }}} */ -/* {{{ proto string password_hash(string password, int algo, array options = array()) +/* {{{ proto string password_hash(string password, int algo[, array options = array()]) Hash a password */ PHP_FUNCTION(password_hash) { @@ -267,7 +364,13 @@ PHP_FUNCTION(password_hash) size_t salt_len = 0, required_salt_len = 0, hash_format_len; HashTable *options = 0; zval *option_buffer; - zend_string *result; + +#if HAVE_ARGON2LIB + 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 = Argon2_i; +#endif if (zend_parse_parameters(ZEND_NUM_ARGS(), "sl|H", &password, &password_len, &algo, &options) == FAILURE) { return; @@ -275,23 +378,57 @@ PHP_FUNCTION(password_hash) switch (algo) { case PHP_PASSWORD_BCRYPT: - { - zend_long cost = PHP_PASSWORD_BCRYPT_COST; + { + 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 (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(); + if (cost < 4 || cost > 31) { + php_error_docref(NULL, E_WARNING, "Invalid bcrypt cost parameter specified: " ZEND_LONG_FMT, cost); + RETURN_NULL(); + } + + required_salt_len = 22; + sprintf(hash_format, "$2y$%02ld$", (long) cost); + hash_format_len = 7; } + break; +#if HAVE_ARGON2LIB + case PHP_PASSWORD_ARGON2I: + { + if (options && (option_buffer = zend_hash_str_find(options, "memory_cost", sizeof("memory_cost")-1)) != NULL) { + memory_cost = zval_get_long(option_buffer); + } - required_salt_len = 22; - sprintf(hash_format, "$2y$%02ld$", (long) cost); - hash_format_len = 7; - } - break; + 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", memory_cost); + 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", time_cost); + 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", threads); + RETURN_NULL(); + } + + required_salt_len = 16; + } + break; +#endif case PHP_PASSWORD_UNKNOWN: default: php_error_docref(NULL, E_WARNING, "Unknown password hashing algorithm: " ZEND_LONG_FMT, algo); @@ -356,30 +493,86 @@ PHP_FUNCTION(password_hash) salt_len = required_salt_len; } - salt[salt_len] = 0; + switch (algo) { + case PHP_PASSWORD_BCRYPT: + { + zend_string *result; + salt[salt_len] = 0; - hash = safe_emalloc(salt_len + hash_format_len, 1, 1); - sprintf(hash, "%s%s", hash_format, salt); - hash[hash_format_len + salt_len] = 0; + hash = safe_emalloc(salt_len + hash_format_len, 1, 1); + sprintf(hash, "%s%s", hash_format, salt); + hash[hash_format_len + salt_len] = 0; - efree(salt); + efree(salt); - /* This cast is safe, since both values are defined here in code and cannot overflow */ - hash_len = (int) (hash_format_len + salt_len); + /* This cast is safe, since both values are defined here in code and cannot overflow */ + hash_len = (int) (hash_format_len + salt_len); - if ((result = php_crypt(password, (int)password_len, hash, hash_len, 1)) == NULL) { - efree(hash); - RETURN_FALSE; - } + if ((result = php_crypt(password, (int)password_len, hash, hash_len, 1)) == NULL) { + efree(hash); + RETURN_FALSE; + } - efree(hash); + efree(hash); - if (ZSTR_LEN(result) < 13) { - zend_string_free(result); - RETURN_FALSE; - } + if (ZSTR_LEN(result) < 13) { + zend_string_free(result); + RETURN_FALSE; + } - RETURN_STR(result); + RETURN_STR(result); + } + break; +#if HAVE_ARGON2LIB + case PHP_PASSWORD_ARGON2I: + { + size_t out_len = 32; + size_t encoded_len; + int status = 0; + + encoded_len = argon2_encodedlen( + time_cost, + memory_cost, + threads, + (uint32_t)salt_len, + out_len + ); + + zend_string *out = zend_string_alloc(out_len, 0); + zend_string *encoded = zend_string_alloc(encoded_len, 0); + + status = argon2_hash( + time_cost, + memory_cost, + threads, + password, + password_len, + salt, + salt_len, + out->val, + out_len, + encoded->val, + encoded_len, + type, + ARGON2_VERSION_NUMBER + ); + + efree(out); + efree(salt); + + if (status != ARGON2_OK) { + efree(encoded); + php_error_docref(NULL, E_WARNING, argon2_error_message(status)); + RETURN_FALSE; + } + + RETURN_STR(encoded); + } + break; +#endif + default: + RETURN_FALSE; + } } /* }}} */ diff --git a/ext/standard/php_password.h b/ext/standard/php_password.h index fdc72b0258edd..4bc2e5660f290 100644 --- a/ext/standard/php_password.h +++ b/ext/standard/php_password.h @@ -13,6 +13,7 @@ | license@php.net so we can mail you a copy immediately. | +----------------------------------------------------------------------+ | Authors: Anthony Ferrara | + | Charles R. Portwood II | +----------------------------------------------------------------------+ */ @@ -28,13 +29,21 @@ PHP_FUNCTION(password_get_info); PHP_MINIT_FUNCTION(password); -#define PHP_PASSWORD_DEFAULT PHP_PASSWORD_BCRYPT - +#define PHP_PASSWORD_DEFAULT PHP_PASSWORD_BCRYPT #define PHP_PASSWORD_BCRYPT_COST 10 +#if HAVE_ARGON2LIB +#define PHP_PASSWORD_ARGON2_MEMORY_COST 1<<10 +#define PHP_PASSWORD_ARGON2_TIME_COST 2 +#define PHP_PASSWORD_ARGON2_THREADS 2 +#endif + typedef enum { - PHP_PASSWORD_UNKNOWN, - PHP_PASSWORD_BCRYPT + PHP_PASSWORD_UNKNOWN, + PHP_PASSWORD_BCRYPT, +#if HAVE_ARGON2LIB + PHP_PASSWORD_ARGON2I, +#endif } php_password_algo; #endif diff --git a/ext/standard/tests/password/password_get_info_argon2.phpt b/ext/standard/tests/password/password_get_info_argon2.phpt new file mode 100644 index 0000000000000..903f9faca5287 --- /dev/null +++ b/ext/standard/tests/password/password_get_info_argon2.phpt @@ -0,0 +1,29 @@ +--TEST-- +Test normal operation of password_get_info() with Argon2 +--SKIPIF-- + +--FILE-- + +--EXPECT-- +array(3) { + ["algo"]=> + int(2) + ["algoName"]=> + string(7) "argon2i" + ["options"]=> + array(3) { + ["memory_cost"]=> + int(65536) + ["time_cost"]=> + int(3) + ["threads"]=> + int(1) + } +} +OK! \ No newline at end of file diff --git a/ext/standard/tests/password/password_hash_argon2.phpt b/ext/standard/tests/password/password_hash_argon2.phpt new file mode 100644 index 0000000000000..03fee3978e28b --- /dev/null +++ b/ext/standard/tests/password/password_hash_argon2.phpt @@ -0,0 +1,18 @@ +--TEST-- +Test normal operation of password_hash() with argon2 +--SKIPIF-- + +--EXPECT-- +bool(true) +OK! \ No newline at end of file diff --git a/ext/standard/tests/password/password_hash_error_argon2.phpt b/ext/standard/tests/password/password_hash_error_argon2.phpt new file mode 100644 index 0000000000000..39a0c4722c3c4 --- /dev/null +++ b/ext/standard/tests/password/password_hash_error_argon2.phpt @@ -0,0 +1,21 @@ +--TEST-- +Test error operation of password_hash() with argon2 +--SKIPIF-- + +--FILE-- + 0])); +var_dump(password_hash('test', PASSWORD_ARGON2I, ['time_cost' => 0])); +var_dump(password_hash('test', PASSWORD_ARGON2I, ['threads' => 0])); +?> +--EXPECTF-- +Warning: password_hash(): Memory cost is outside of allowed memory range in %s on line %d +NULL + +Warning: password_hash(): Time cost is outside of allowed time range in %s on line %d +NULL + +Warning: password_hash(): Invalid number of threads in %s on line %d +NULL \ No newline at end of file diff --git a/ext/standard/tests/password/password_needs_rehash_argon2.phpt b/ext/standard/tests/password/password_needs_rehash_argon2.phpt new file mode 100644 index 0000000000000..1fec77d3bdf07 --- /dev/null +++ b/ext/standard/tests/password/password_needs_rehash_argon2.phpt @@ -0,0 +1,22 @@ +--TEST-- +Test normal operation of password_needs_rehash() with argon2 +--SKIPIF-- + +--FILE-- + 1<<17])); +var_dump(password_needs_rehash($hash, PASSWORD_ARGON2I, ['time_cost' => 2])); +var_dump(password_needs_rehash($hash, PASSWORD_ARGON2I, ['threads' => 2])); +echo "OK!"; +?> +--EXPECT-- +bool(false) +bool(true) +bool(true) +bool(true) +OK! diff --git a/ext/standard/tests/password/password_verify_argon2.phpt b/ext/standard/tests/password/password_verify_argon2.phpt new file mode 100644 index 0000000000000..99917a7a28861 --- /dev/null +++ b/ext/standard/tests/password/password_verify_argon2.phpt @@ -0,0 +1,18 @@ +--TEST-- +Test normal operation of password_verify() with argon2 +--SKIPIF-- + +--FILE-- + +--EXPECT-- +bool(true) +bool(false) +OK! \ No newline at end of file