From 6387498823c85e07a549c189ba0ec33cb6e0d90c Mon Sep 17 00:00:00 2001 From: Anthony Ferrara Date: Tue, 12 Jun 2012 09:57:11 -0400 Subject: [PATCH 1/7] Create hash_pbkdf2 function addition --- NEWS | 3 + ext/hash/hash.c | 206 ++++++++++++++++++++++---- ext/hash/php_hash.h | 1 + ext/hash/tests/hash_pbkdf2_basic.phpt | 37 +++++ ext/hash/tests/hash_pbkdf2_error.phpt | 78 ++++++++++ 5 files changed, 293 insertions(+), 32 deletions(-) create mode 100644 ext/hash/tests/hash_pbkdf2_basic.phpt create mode 100644 ext/hash/tests/hash_pbkdf2_error.phpt diff --git a/NEWS b/NEWS index e9e70e9039a30..dc4265050ab96 100644 --- a/NEWS +++ b/NEWS @@ -42,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()F + - 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..7f0d36f6adaec 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, (unsigned char *) key, key_len); + ops->hash_final((unsigned char *) 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,124 @@ 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 */ + memset(K2, 0, ops->block_size); + 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); + 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 potentiall 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 +1134,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 +1189,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 +1246,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..6b827da30d25e --- /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=== From 550253f6529bfa56e494505e6517500f98c7223a Mon Sep 17 00:00:00 2001 From: Anthony Ferrara Date: Tue, 12 Jun 2012 13:51:18 -0400 Subject: [PATCH 2/7] Update NEWS to fix typo, add name --- NEWS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS b/NEWS index dc4265050ab96..37e443d25e9f5 100644 --- a/NEWS +++ b/NEWS @@ -43,7 +43,7 @@ PHP NEWS . Fixed bug #54995 (Missing CURLINFO_RESPONSE_CODE support). (Pierrick) - Hash - . Added support for PBKDF2 via hash_pbkdf2()F + . Added support for PBKDF2 via hash_pbkdf2(). (Anthony Ferrara) - MySQLi . Dropped support for LOAD DATA LOCAL INFILE handlers when using libmysql. From 4918acccc6bd23f907b6712bdd04fcd265a411b0 Mon Sep 17 00:00:00 2001 From: Anthony Ferrara Date: Tue, 12 Jun 2012 14:09:16 -0400 Subject: [PATCH 3/7] refactor away un-necessary casts in hashing routines --- ext/hash/hash.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/hash/hash.c b/ext/hash/hash.c index 7f0d36f6adaec..71f3753dab5b9 100644 --- a/ext/hash/hash.c +++ b/ext/hash/hash.c @@ -222,8 +222,8 @@ static inline void php_hash_hmac_prep_key(unsigned char *K, const php_hash_ops * if (key_len > ops->block_size) { /* Reduce the key first */ ops->hash_init(context); - ops->hash_update(context, (unsigned char *) key, key_len); - ops->hash_final((unsigned char *) K, context); + ops->hash_update(context, key, key_len); + ops->hash_final(K, context); } else { memcpy(K, key, key_len); } From df3d351cad7ecc2b6087e7f26edf6fa8b22cd960 Mon Sep 17 00:00:00 2001 From: Anthony Ferrara Date: Tue, 12 Jun 2012 14:10:35 -0400 Subject: [PATCH 4/7] Update error messages to be more inline with PHP standards --- ext/hash/hash.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/hash/hash.c b/ext/hash/hash.c index 71f3753dab5b9..40023f75b8a15 100644 --- a/ext/hash/hash.c +++ b/ext/hash/hash.c @@ -629,12 +629,12 @@ PHP_FUNCTION(hash_pbkdf2) } if (iterations <= 0) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "Iterations Must Be A Positive Integer: %ld", iterations); + 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); + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Length must be greater than or equal to 0: %ld", length); RETURN_FALSE; } From 43eb8dc04af1480b3caa62d252ede28dcb059c7b Mon Sep 17 00:00:00 2001 From: Anthony Ferrara Date: Tue, 12 Jun 2012 14:32:21 -0400 Subject: [PATCH 5/7] Remove un-needed memset, and replacing stray spaces --- ext/hash/hash.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ext/hash/hash.c b/ext/hash/hash.c index 40023f75b8a15..74c86a8714e54 100644 --- a/ext/hash/hash.c +++ b/ext/hash/hash.c @@ -652,7 +652,6 @@ PHP_FUNCTION(hash_pbkdf2) temp = emalloc(ops->digest_size); /* Setup Keys that will be used for all hmac rounds */ - memset(K2, 0, ops->block_size); 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); @@ -661,7 +660,7 @@ PHP_FUNCTION(hash_pbkdf2) if (length == 0) { length = ops->digest_size; } - digest_length = length; + digest_length = length; if (!raw_output) { digest_length = (long) ceil((float) length / 2.0); } From 2f1cd2cb1377bac9093ab539d936dd6c4a913916 Mon Sep 17 00:00:00 2001 From: Anthony Ferrara Date: Tue, 12 Jun 2012 14:52:43 -0400 Subject: [PATCH 6/7] Fix tests to use proper casing --- ext/hash/tests/hash_pbkdf2_error.phpt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ext/hash/tests/hash_pbkdf2_error.phpt b/ext/hash/tests/hash_pbkdf2_error.phpt index 6b827da30d25e..fd70cca581ea8 100644 --- a/ext/hash/tests/hash_pbkdf2_error.phpt +++ b/ext/hash/tests/hash_pbkdf2_error.phpt @@ -67,12 +67,12 @@ hash_pbkdf2(): Unknown hashing algorithm: foo -- Testing hash_pbkdf2() function with invalid iterations -- bool(false) -hash_pbkdf2(): Iterations Must Be A Positive Integer: 0 +hash_pbkdf2(): Iterations must be a positive integer: 0 bool(false) -hash_pbkdf2(): Iterations Must Be A Positive Integer: -1 +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 +hash_pbkdf2(): Length must be greater than or equal to 0: -1 ===Done=== From 03536e889ad29ed3b6153aafa77b647bdcfe2592 Mon Sep 17 00:00:00 2001 From: Anthony Ferrara Date: Tue, 12 Jun 2012 15:05:44 -0400 Subject: [PATCH 7/7] More cleanup of documentation and comments, as well as code formatting --- ext/hash/hash.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ext/hash/hash.c b/ext/hash/hash.c index 74c86a8714e54..957575d47276c 100644 --- a/ext/hash/hash.c +++ b/ext/hash/hash.c @@ -205,14 +205,14 @@ 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++) { + 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++) { + for (i=0; i < length; i++) { out[i] = in[i] ^ xor_with[i]; } } @@ -687,6 +687,11 @@ PHP_FUNCTION(hash_pbkdf2) /* 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); @@ -698,7 +703,7 @@ PHP_FUNCTION(hash_pbkdf2) /* result += temp */ memcpy(result + ((i - 1) * ops->digest_size), temp, ops->digest_size); } - /* Zero potentiall sensitive variables */ + /* Zero potentially sensitive variables */ memset(K1, 0, ops->block_size); memset(K2, 0, ops->block_size); memset(computed_salt, 0, salt_len + 4);