diff --git a/NEWS b/NEWS index e76c564c34cd8..37e443d25e9f5 100644 --- a/NEWS +++ b/NEWS @@ -12,8 +12,6 @@ PHP NEWS (Nikita Popov) - Core: - . Fixed bug #62443 (Crypt SHA256/512 Segfaults With Malformed - Salt). (Anthony Ferrara) . Added boolval(). (Jille Timmermans). . Fixed bug #61681 (Malformed grammar). (Nikita Popov, Etienne, Laruence). . Fixed bug #61038 (unpack("a5", "str\0\0") does not work as expected). @@ -44,6 +42,9 @@ PHP NEWS still exists for backward compatibility but is doing nothing). (Pierrick) . Fixed bug #54995 (Missing CURLINFO_RESPONSE_CODE support). (Pierrick) +- Hash + . Added support for PBKDF2 via hash_pbkdf2(). (Anthony Ferrara) + - MySQLi . Dropped support for LOAD DATA LOCAL INFILE handlers when using libmysql. Known for stability problems. (Andrey) diff --git a/ext/hash/hash.c b/ext/hash/hash.c index 895d64da33fbd..957575d47276c 100644 --- a/ext/hash/hash.c +++ b/ext/hash/hash.c @@ -23,6 +23,7 @@ #include "config.h" #endif +#include #include "php_hash.h" #include "ext/standard/info.h" #include "ext/standard/file.h" @@ -202,10 +203,45 @@ PHP_FUNCTION(hash_file) } /* }}} */ +static inline void php_hash_string_xor_char(unsigned char *out, const unsigned char *in, const unsigned char xor_with, const int length) { + int i; + for (i=0; i < length; i++) { + out[i] = in[i] ^ xor_with; + } +} + +static inline void php_hash_string_xor(unsigned char *out, const unsigned char *in, const unsigned char *xor_with, const int length) { + int i; + for (i=0; i < length; i++) { + out[i] = in[i] ^ xor_with[i]; + } +} + +static inline void php_hash_hmac_prep_key(unsigned char *K, const php_hash_ops *ops, void *context, const unsigned char *key, const int key_len) { + memset(K, 0, ops->block_size); + if (key_len > ops->block_size) { + /* Reduce the key first */ + ops->hash_init(context); + ops->hash_update(context, key, key_len); + ops->hash_final(K, context); + } else { + memcpy(K, key, key_len); + } + /* XOR the key with 0x36 to get the ipad) */ + php_hash_string_xor_char(K, K, 0x36, ops->block_size); +} + +static inline void php_hash_hmac_round(unsigned char *final, const php_hash_ops *ops, void *context, const unsigned char *key, const unsigned char *data, const long data_size) { + ops->hash_init(context); + ops->hash_update(context, key, ops->block_size); + ops->hash_update(context, data, data_size); + ops->hash_final(final, context); +} + static void php_hash_do_hash_hmac(INTERNAL_FUNCTION_PARAMETERS, int isfilename, zend_bool raw_output_default) /* {{{ */ { char *algo, *data, *digest, *key, *K; - int algo_len, data_len, key_len, i; + int algo_len, data_len, key_len; zend_bool raw_output = raw_output_default; const php_hash_ops *ops; void *context; @@ -230,52 +266,29 @@ static void php_hash_do_hash_hmac(INTERNAL_FUNCTION_PARAMETERS, int isfilename, } context = emalloc(ops->context_size); - ops->hash_init(context); K = emalloc(ops->block_size); - memset(K, 0, ops->block_size); + digest = emalloc(ops->digest_size + 1); - if (key_len > ops->block_size) { - /* Reduce the key first */ - ops->hash_update(context, (unsigned char *) key, key_len); - ops->hash_final((unsigned char *) K, context); - /* Make the context ready to start over */ - ops->hash_init(context); - } else { - memcpy(K, key, key_len); - } - - /* XOR ipad */ - for(i=0; i < ops->block_size; i++) { - K[i] ^= 0x36; - } - ops->hash_update(context, (unsigned char *) K, ops->block_size); + php_hash_hmac_prep_key((unsigned char *) K, ops, context, (unsigned char *) key, key_len); if (isfilename) { char buf[1024]; int n; - + ops->hash_init(context); + ops->hash_update(context, (unsigned char *) K, ops->block_size); while ((n = php_stream_read(stream, buf, sizeof(buf))) > 0) { ops->hash_update(context, (unsigned char *) buf, n); } php_stream_close(stream); + ops->hash_final((unsigned char *) digest, context); } else { - ops->hash_update(context, (unsigned char *) data, data_len); + php_hash_hmac_round((unsigned char *) digest, ops, context, (unsigned char *) K, (unsigned char *) data, data_len); } - digest = emalloc(ops->digest_size + 1); - ops->hash_final((unsigned char *) digest, context); - - /* Convert K to opad -- 0x6A = 0x36 ^ 0x5C */ - for(i=0; i < ops->block_size; i++) { - K[i] ^= 0x6A; - } + php_hash_string_xor_char((unsigned char *) K, (unsigned char *) K, 0x6A, ops->block_size); - /* Feed this result into the outter hash */ - ops->hash_init(context); - ops->hash_update(context, (unsigned char *) K, ops->block_size); - ops->hash_update(context, (unsigned char *) digest, ops->digest_size); - ops->hash_final((unsigned char *) digest, context); + php_hash_hmac_round((unsigned char *) digest, ops, context, (unsigned char *) K, (unsigned char *) digest, ops->digest_size); /* Zero the key */ memset(K, 0, ops->block_size); @@ -591,6 +604,128 @@ PHP_FUNCTION(hash_algos) } /* }}} */ +/* {{{ proto string hash_pbkdf2(string algo, string password, string salt, int iterations [, int length = 0, bool raw_output = false]) +Generate a PBKDF2 hash of the given password and salt +Returns lowercase hexits by default */ +PHP_FUNCTION(hash_pbkdf2) +{ + char *returnval, *algo, *salt, *pass = NULL; + unsigned char *computed_salt, *digest, *temp, *result, *K1, *K2 = NULL; + long loops, i, j, algo_len, pass_len, iterations, length, digest_length = 0; + int argc, salt_len = 0; + zend_bool raw_output = 0; + const php_hash_ops *ops; + void *context; + + argc = ZEND_NUM_ARGS(); + if (zend_parse_parameters(argc TSRMLS_CC, "sssl|lb", &algo, &algo_len, &pass, &pass_len, &salt, &salt_len, &iterations, &length, &raw_output) == FAILURE) { + return; + } + + ops = php_hash_fetch_ops(algo, algo_len); + if (!ops) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unknown hashing algorithm: %s", algo); + RETURN_FALSE; + } + + if (iterations <= 0) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Iterations must be a positive integer: %ld", iterations); + RETURN_FALSE; + } + + if (length < 0) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Length must be greater than or equal to 0: %ld", length); + RETURN_FALSE; + } + + if (salt_len > INT_MAX - 4) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Supplied salt is too long, max of INT_MAX - 4 bytes: %d supplied", salt_len); + RETURN_FALSE; + } + + context = emalloc(ops->context_size); + ops->hash_init(context); + + K1 = emalloc(ops->block_size); + K2 = emalloc(ops->block_size); + digest = emalloc(ops->digest_size); + temp = emalloc(ops->digest_size); + + /* Setup Keys that will be used for all hmac rounds */ + php_hash_hmac_prep_key(K1, ops, context, (unsigned char *) pass, pass_len); + /* Convert K1 to opad -- 0x6A = 0x36 ^ 0x5C */ + php_hash_string_xor_char(K2, K1, 0x6A, ops->block_size); + + /* Setup Main Loop to build a long enough result */ + if (length == 0) { + length = ops->digest_size; + } + digest_length = length; + if (!raw_output) { + digest_length = (long) ceil((float) length / 2.0); + } + + loops = (long) ceil((float) digest_length / (float) ops->digest_size); + + result = safe_emalloc(loops, ops->digest_size, 0); + + computed_salt = safe_emalloc(salt_len, 1, 4); + memcpy(computed_salt, (unsigned char *) salt, salt_len); + + for (i = 1; i <= loops; i++) { + /* digest = hash_hmac(salt + pack('N', i), password) { */ + + /* pack("N", i) */ + computed_salt[salt_len] = (unsigned char) (i >> 24); + computed_salt[salt_len + 1] = (unsigned char) ((i & 0xFF0000) >> 16); + computed_salt[salt_len + 2] = (unsigned char) ((i & 0xFF00) >> 8); + computed_salt[salt_len + 3] = (unsigned char) (i & 0xFF); + + php_hash_hmac_round(digest, ops, context, K1, computed_salt, (long) salt_len + 4); + php_hash_hmac_round(digest, ops, context, K2, digest, ops->digest_size); + /* } */ + + /* temp = digest */ + memcpy(temp, digest, ops->digest_size); + + /* + * Note that the loop starting at 1 is intentional, since we've already done + * the first round of the algorithm. + */ + for (j = 1; j < iterations; j++) { + /* digest = hash_hmac(digest, password) { */ + php_hash_hmac_round(digest, ops, context, K1, digest, ops->digest_size); + php_hash_hmac_round(digest, ops, context, K2, digest, ops->digest_size); + /* } */ + /* temp ^= digest */ + php_hash_string_xor(temp, temp, digest, ops->digest_size); + } + /* result += temp */ + memcpy(result + ((i - 1) * ops->digest_size), temp, ops->digest_size); + } + /* Zero potentially sensitive variables */ + memset(K1, 0, ops->block_size); + memset(K2, 0, ops->block_size); + memset(computed_salt, 0, salt_len + 4); + efree(K1); + efree(K2); + efree(computed_salt); + efree(context); + efree(digest); + efree(temp); + + returnval = safe_emalloc(length, 1, 1); + if (raw_output) { + memcpy(returnval, result, length); + } else { + php_hash_bin2hex(returnval, result, digest_length); + } + returnval[length] = 0; + efree(result); + RETURN_STRINGL(returnval, length, 0); +} +/* }}} */ + /* Module Housekeeping */ static void php_hash_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC) /* {{{ */ @@ -1003,6 +1138,15 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_hash_algos, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_INFO_EX(arginfo_hash_pbkdf2, 0, 0, 4) + ZEND_ARG_INFO(0, algo) + ZEND_ARG_INFO(0, password) + ZEND_ARG_INFO(0, salt) + ZEND_ARG_INFO(0, iterations) + ZEND_ARG_INFO(0, length) + ZEND_ARG_INFO(0, raw_output) +ZEND_END_ARG_INFO() + /* BC Land */ #ifdef PHP_MHASH_BC ZEND_BEGIN_ARG_INFO(arginfo_mhash_get_block_size, 0) @@ -1049,6 +1193,7 @@ const zend_function_entry hash_functions[] = { PHP_FE(hash_copy, arginfo_hash_copy) PHP_FE(hash_algos, arginfo_hash_algos) + PHP_FE(hash_pbkdf2, arginfo_hash_pbkdf2) /* BC Land */ #ifdef PHP_HASH_MD5_NOT_IN_CORE @@ -1105,3 +1250,4 @@ ZEND_GET_MODULE(hash) * vim600: noet sw=4 ts=4 fdm=marker * vim<600: noet sw=4 ts=4 */ + diff --git a/ext/hash/php_hash.h b/ext/hash/php_hash.h index 87050cb8e52af..7bc72a2bcb140 100644 --- a/ext/hash/php_hash.h +++ b/ext/hash/php_hash.h @@ -127,6 +127,7 @@ PHP_FUNCTION(hash_update_stream); PHP_FUNCTION(hash_update_file); PHP_FUNCTION(hash_final); PHP_FUNCTION(hash_algos); +PHP_FUNCTION(hash_pbkdf2); PHP_HASH_API const php_hash_ops *php_hash_fetch_ops(const char *algo, int algo_len); PHP_HASH_API void php_hash_register_algo(const char *algo, const php_hash_ops *ops); diff --git a/ext/hash/tests/hash_pbkdf2_basic.phpt b/ext/hash/tests/hash_pbkdf2_basic.phpt new file mode 100644 index 0000000000000..fdccc4b6ea460 --- /dev/null +++ b/ext/hash/tests/hash_pbkdf2_basic.phpt @@ -0,0 +1,37 @@ +--TEST-- +Test hash_pbkdf2() function : basic functionality +--SKIPIF-- + +--FILE-- + +===Done=== +--EXPECT-- +*** Testing hash_pbkdf2() : basic functionality *** +sha1: 0c60c80f961f0e71f3a9 +sha1(raw): 0c60c80f961f0e71f3a9b524af6012062fe037a6 +sha1(rounds): 3d2eec4fe41c849b80c8d8366 +sha1(rounds)(raw): 3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038 +sha256: 120fb6cffcf8b32c43e7 +sha256(raw): 120fb6cffcf8b32c43e7225256c4f837a86548c9 +sha256(rounds): 348c89dbcbd32b2f32d814b8116e84cf2b17347e +sha256(rounds)(raw): 348c89dbcbd32b2f32d814b8116e84cf2b17347ebc1800181c4e2a1fb8dd53e1c635518c7dac47e9 +===Done=== diff --git a/ext/hash/tests/hash_pbkdf2_error.phpt b/ext/hash/tests/hash_pbkdf2_error.phpt new file mode 100644 index 0000000000000..fd70cca581ea8 --- /dev/null +++ b/ext/hash/tests/hash_pbkdf2_error.phpt @@ -0,0 +1,78 @@ +--TEST-- +Test hash_pbkdf2() function : error functionality +--SKIPIF-- + +--FILE-- + +===Done=== +--EXPECT-- +*** Testing hash_pbkdf2() : error conditions *** + +-- Testing hash_pbkdf2() function with less than expected no. of arguments -- +NULL +hash_pbkdf2() expects at least 4 parameters, 0 given +NULL +hash_pbkdf2() expects at least 4 parameters, 1 given +NULL +hash_pbkdf2() expects at least 4 parameters, 2 given +NULL +hash_pbkdf2() expects at least 4 parameters, 3 given + +-- Testing hash_pbkdf2() function with more than expected no. of arguments -- +NULL +hash_pbkdf2() expects at most 6 parameters, 7 given + +-- Testing hash_pbkdf2() function with invalid hash algorithm -- +bool(false) +hash_pbkdf2(): Unknown hashing algorithm: foo + +-- Testing hash_pbkdf2() function with invalid iterations -- +bool(false) +hash_pbkdf2(): Iterations must be a positive integer: 0 +bool(false) +hash_pbkdf2(): Iterations must be a positive integer: -1 + +-- Testing hash_pbkdf2() function with invalid length -- +bool(false) +hash_pbkdf2(): Length must be greater than or equal to 0: -1 + +===Done===