Skip to content

PR for Password Hash RFC #191

Merged
merged 48 commits into from Oct 16, 2012
Select commit
+932 −97
View
2 NEWS
@@ -3,6 +3,8 @@ PHP NEWS
?? ??? 201?, PHP 5.5.0
- General improvements:
+ . Add simplified password hashing API
+ (https://wiki.php.net/rfc/password_hash). (Anthony Ferrara)
. Support list in foreach (https://wiki.php.net/rfc/foreachlist). (Laruence)
. Implemented 'finally' keyword (https://wiki.php.net/rfc/finally). (Laruence)
. Drop Windows XP and 2003 support. (Pierre)
View
24 ext/standard/basic_functions.c
@@ -1854,6 +1854,25 @@ ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO(arginfo_getlastmod, 0)
ZEND_END_ARG_INFO()
/* }}} */
+/* {{{ password.c */
+ZEND_BEGIN_ARG_INFO_EX(arginfo_password_hash, 0, 0, 2)
+ ZEND_ARG_INFO(0, password)
+ ZEND_ARG_INFO(0, algo)
+ ZEND_ARG_INFO(0, options)
+ZEND_END_ARG_INFO()
+ZEND_BEGIN_ARG_INFO_EX(arginfo_password_get_info, 0, 0, 1)
+ ZEND_ARG_INFO(0, hash)
+ZEND_END_ARG_INFO()
+ZEND_BEGIN_ARG_INFO_EX(arginfo_password_needs_rehash, 0, 0, 2)
+ ZEND_ARG_INFO(0, hash)
+ ZEND_ARG_INFO(0, algo)
+ ZEND_ARG_INFO(0, options)
+ZEND_END_ARG_INFO()
+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()
+/* }}} */
/* {{{ proc_open.c */
#ifdef PHP_CAN_SUPPORT_PROC_OPEN
ZEND_BEGIN_ARG_INFO_EX(arginfo_proc_terminate, 0, 0, 1)
@@ -2864,6 +2883,10 @@ const zend_function_entry basic_functions[] = { /* {{{ */
PHP_FE(base64_decode, arginfo_base64_decode)
PHP_FE(base64_encode, arginfo_base64_encode)
+ PHP_FE(password_hash, arginfo_password_hash)
+ 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(convert_uuencode, arginfo_convert_uuencode)
PHP_FE(convert_uudecode, arginfo_convert_uudecode)
@@ -3614,6 +3637,7 @@ PHP_MINIT_FUNCTION(basic) /* {{{ */
BASIC_MINIT_SUBMODULE(browscap)
BASIC_MINIT_SUBMODULE(standard_filters)
BASIC_MINIT_SUBMODULE(user_filters)
+ BASIC_MINIT_SUBMODULE(password)
#if defined(HAVE_LOCALECONV) && defined(ZTS)
BASIC_MINIT_SUBMODULE(localeconv)
View
2 ext/standard/config.m4
@@ -580,7 +580,7 @@ PHP_NEW_EXTENSION(standard, array.c base64.c basic_functions.c browscap.c crc32.
incomplete_class.c url_scanner_ex.c ftp_fopen_wrapper.c \
http_fopen_wrapper.c php_fopen_wrapper.c credits.c css.c \
var_unserializer.c ftok.c sha1.c user_filters.c uuencode.c \
- filters.c proc_open.c streamsfuncs.c http.c)
+ filters.c proc_open.c streamsfuncs.c http.c password.c)
PHP_ADD_MAKEFILE_FRAGMENT
PHP_INSTALL_HEADERS([ext/standard/])
View
2 ext/standard/config.w32
@@ -19,7 +19,7 @@ EXTENSION("standard", "array.c base64.c basic_functions.c browscap.c \
versioning.c assert.c strnatcmp.c levenshtein.c incomplete_class.c \
url_scanner_ex.c ftp_fopen_wrapper.c http_fopen_wrapper.c \
php_fopen_wrapper.c credits.c css.c var_unserializer.c ftok.c sha1.c \
- user_filters.c uuencode.c filters.c proc_open.c \
+ user_filters.c uuencode.c filters.c proc_open.c password.c \
streamsfuncs.c http.c flock_compat.c", false /* never shared */);
PHP_INSTALL_HEADERS("", "ext/standard");
if (PHP_MBREGEX != "no") {
View
185 ext/standard/crypt.c
@@ -145,100 +145,54 @@ static void php_to64(char *s, long v, int n) /* {{{ */
}
/* }}} */
-/* {{{ proto string crypt(string str [, string salt])
- Hash a string */
-PHP_FUNCTION(crypt)
+PHPAPI int php_crypt(const char *password, const int pass_len, const char *salt, int salt_len, char **result)
{
- char salt[PHP_MAX_SALT_LEN + 1];
- char *str, *salt_in = NULL;
- int str_len, salt_in_len = 0;
char *crypt_res;
- salt[0] = salt[PHP_MAX_SALT_LEN] = '\0';
-
- /* This will produce suitable results if people depend on DES-encryption
- * available (passing always 2-character salt). At least for glibc6.1 */
- memset(&salt[1], '$', PHP_MAX_SALT_LEN - 1);
-
- if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|s", &str, &str_len, &salt_in, &salt_in_len) == FAILURE) {
- return;
- }
-
- if (salt_in) {
- memcpy(salt, salt_in, MIN(PHP_MAX_SALT_LEN, salt_in_len));
- }
-
- /* The automatic salt generation covers standard DES, md5-crypt and Blowfish (simple) */
- if (!*salt) {
-#if PHP_MD5_CRYPT
- strncpy(salt, "$1$", PHP_MAX_SALT_LEN);
- php_to64(&salt[3], PHP_CRYPT_RAND, 4);
- php_to64(&salt[7], PHP_CRYPT_RAND, 4);
- strncpy(&salt[11], "$", PHP_MAX_SALT_LEN - 11);
-#elif PHP_STD_DES_CRYPT
- php_to64(&salt[0], PHP_CRYPT_RAND, 2);
- salt[2] = '\0';
-#endif
- salt_in_len = strlen(salt);
- } else {
- salt_in_len = MIN(PHP_MAX_SALT_LEN, salt_in_len);
- }
-
/* Windows (win32/crypt) has a stripped down version of libxcrypt and
a CryptoApi md5_crypt implementation */
#if PHP_USE_PHP_CRYPT_R
{
struct php_crypt_extended_data buffer;
if (salt[0]=='$' && salt[1]=='1' && salt[2]=='$') {
- char output[MD5_HASH_MAX_LEN];
+ char output[MD5_HASH_MAX_LEN], *out;
- RETURN_STRING(php_md5_crypt_r(str, salt, output), 1);
+ out = php_md5_crypt_r(password, salt, output);
+ if (out) {
+ *result = estrdup(out);
+ return SUCCESS;
+ }
+ return FAILURE;
} else if (salt[0]=='$' && salt[1]=='6' && salt[2]=='$') {
- const char sha512_salt_prefix[] = "$6$";
- const char sha512_rounds_prefix[] = "rounds=";
char *output;
- int needed = (sizeof(sha512_salt_prefix) - 1
- + sizeof(sha512_rounds_prefix) + 9 + 1
- + salt_in_len + 1 + 86 + 1);
- output = emalloc(needed);
- salt[salt_in_len] = '\0';
+ output = emalloc(PHP_MAX_SALT_LEN);
- crypt_res = php_sha512_crypt_r(str, salt, output, needed);
+ crypt_res = php_sha512_crypt_r(password, salt, output, PHP_MAX_SALT_LEN);
if (!crypt_res) {
- if (salt[0]=='*' && salt[1]=='0') {
- RETVAL_STRING("*1", 1);
- } else {
- RETVAL_STRING("*0", 1);
- }
+ memset(output, 0, PHP_MAX_SALT_LEN);
+ efree(output);
+ return FAILURE;
} else {
- RETVAL_STRING(output, 1);
+ *result = estrdup(output);
+ memset(output, 0, PHP_MAX_SALT_LEN);
+ efree(output);
+ return SUCCESS;
}
-
- memset(output, 0, needed);
- efree(output);
} else if (salt[0]=='$' && salt[1]=='5' && salt[2]=='$') {
- const char sha256_salt_prefix[] = "$5$";
- const char sha256_rounds_prefix[] = "rounds=";
char *output;
- int needed = (sizeof(sha256_salt_prefix) - 1
- + sizeof(sha256_rounds_prefix) + 9 + 1
- + salt_in_len + 1 + 43 + 1);
- output = emalloc(needed);
- salt[salt_in_len] = '\0';
+ output = emalloc(PHP_MAX_SALT_LEN);
- crypt_res = php_sha256_crypt_r(str, salt, output, needed);
+ crypt_res = php_sha256_crypt_r(password, salt, output, PHP_MAX_SALT_LEN);
if (!crypt_res) {
- if (salt[0]=='*' && salt[1]=='0') {
- RETVAL_STRING("*1", 1);
- } else {
- RETVAL_STRING("*0", 1);
- }
+ memset(output, 0, PHP_MAX_SALT_LEN);
+ efree(output);
+ return FAILURE;
} else {
- RETVAL_STRING(output, 1);
+ *result = estrdup(output);
+ memset(output, 0, PHP_MAX_SALT_LEN);
+ efree(output);
+ return SUCCESS;
}
-
- memset(output, 0, needed);
- efree(output);
} else if (
salt[0] == '$' &&
salt[1] == '2' &&
@@ -251,31 +205,25 @@ PHP_FUNCTION(crypt)
memset(output, 0, PHP_MAX_SALT_LEN + 1);
- crypt_res = php_crypt_blowfish_rn(str, salt, output, sizeof(output));
+ crypt_res = php_crypt_blowfish_rn(password, salt, output, sizeof(output));
if (!crypt_res) {
- if (salt[0]=='*' && salt[1]=='0') {
- RETVAL_STRING("*1", 1);
- } else {
- RETVAL_STRING("*0", 1);
- }
+ memset(output, 0, PHP_MAX_SALT_LEN + 1);
+ return FAILURE;
} else {
- RETVAL_STRING(output, 1);
+ *result = estrdup(output);
+ memset(output, 0, PHP_MAX_SALT_LEN + 1);
+ return SUCCESS;
}
-
- memset(output, 0, PHP_MAX_SALT_LEN + 1);
} else {
memset(&buffer, 0, sizeof(buffer));
_crypt_extended_init_r();
- crypt_res = _crypt_extended_r(str, salt, &buffer);
+ crypt_res = _crypt_extended_r(password, salt, &buffer);
if (!crypt_res) {
- if (salt[0]=='*' && salt[1]=='0') {
- RETURN_STRING("*1", 1);
- } else {
- RETURN_STRING("*0", 1);
- }
+ return FAILURE;
} else {
- RETURN_STRING(crypt_res, 1);
+ *result = estrdup(crypt_res);
+ return SUCCESS;
}
}
}
@@ -291,21 +239,68 @@ PHP_FUNCTION(crypt)
# else
# error Data struct used by crypt_r() is unknown. Please report.
# endif
- crypt_res = crypt_r(str, salt, &buffer);
+ crypt_res = crypt_r(password, salt, &buffer);
if (!crypt_res) {
- if (salt[0]=='*' && salt[1]=='0') {
- RETURN_STRING("*1", 1);
- } else {
- RETURN_STRING("*0", 1);
- }
+ return FAILURE;
} else {
- RETURN_STRING(crypt_res, 1);
+ *result = estrdup(crypt_res);
+ return SUCCESS;
}
}
# endif
#endif
}
/* }}} */
+
+
+/* {{{ proto string crypt(string str [, string salt])
+ Hash a string */
+PHP_FUNCTION(crypt)
+{
+ char salt[PHP_MAX_SALT_LEN + 1];
+ char *str, *salt_in = NULL, *result = NULL;
+ int str_len, salt_in_len = 0;
+ salt[0] = salt[PHP_MAX_SALT_LEN] = '\0';
+
+ /* This will produce suitable results if people depend on DES-encryption
+ * available (passing always 2-character salt). At least for glibc6.1 */
+ memset(&salt[1], '$', PHP_MAX_SALT_LEN - 1);
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|s", &str, &str_len, &salt_in, &salt_in_len) == FAILURE) {
+ return;
+ }
+
+ if (salt_in) {
+ memcpy(salt, salt_in, MIN(PHP_MAX_SALT_LEN, salt_in_len));
+ }
+
+ /* The automatic salt generation covers standard DES, md5-crypt and Blowfish (simple) */
+ if (!*salt) {
+#if PHP_MD5_CRYPT
@lstrojny
lstrojny added a note Sep 12, 2012

Does that mean that my salt will change depending on the platform?

@ircmaxell
ircmaxell added a note Sep 12, 2012

That's core crypt() code that exists right now (as of 5.3.0). The reason for the change here is that I refactored it into two functions so I could call the implementation from C.

But now, the salt will not change (assuming that you're comment is ment for this line)...

@lstrojny
lstrojny added a note Sep 12, 2012

Ah, alright. Never mind :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ strncpy(salt, "$1$", PHP_MAX_SALT_LEN);
+ php_to64(&salt[3], PHP_CRYPT_RAND, 4);
+ php_to64(&salt[7], PHP_CRYPT_RAND, 4);
+ strncpy(&salt[11], "$", PHP_MAX_SALT_LEN - 11);
+#elif PHP_STD_DES_CRYPT
+ php_to64(&salt[0], PHP_CRYPT_RAND, 2);
+ salt[2] = '\0';
+#endif
+ salt_in_len = strlen(salt);
+ } else {
+ salt_in_len = MIN(PHP_MAX_SALT_LEN, salt_in_len);
+ }
+ salt[salt_in_len] = '\0';
+
+ if (php_crypt(str, str_len, salt, salt_in_len, &result) == FAILURE) {
+ if (salt[0] == '*' && salt[1] == '0') {
+ RETURN_STRING("*1", 1);
+ } else {
+ RETURN_STRING("*0", 1);
+ }
+ }
+ RETURN_STRING(result, 0);
+}
+/* }}} */
#endif
/*
View
460 ext/standard/password.c
@@ -0,0 +1,460 @@
+/*
+ +----------------------------------------------------------------------+
+ | PHP Version 5 |
+ +----------------------------------------------------------------------+
+ | Copyright (c) 1997-2012 The PHP Group |
+ +----------------------------------------------------------------------+
+ | This source file is subject to version 3.01 of the PHP license, |
+ | that is bundled with this package in the file LICENSE, and is |
+ | available through the world-wide-web at the following url: |
+ | http://www.php.net/license/3_01.txt |
+ | If you did not receive a copy of the PHP license and are unable to |
+ | obtain it through the world-wide-web, please send a note to |
+ | license@php.net so we can mail you a copy immediately. |
+ +----------------------------------------------------------------------+
+ | Authors: Anthony Ferrara <ircmaxell@php.net> |
+ +----------------------------------------------------------------------+
+*/
+
+/* $Id$ */
+
+#include <stdlib.h>
+
+#include "php.h"
+#if HAVE_CRYPT
+
+#include "fcntl.h"
+#include "php_password.h"
+#include "php_rand.h"
+#include "php_crypt.h"
+#include "base64.h"
+#include "zend_interfaces.h"
+#include "info.h"
+
+#if PHP_WIN32
+#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);
+
+ REGISTER_LONG_CONSTANT("PASSWORD_BCRYPT_DEFAULT_COST", PHP_PASSWORD_BCRYPT_COST, CONST_CS | CONST_PERSISTENT);
+
+ return SUCCESS;
+}
+/* }}} */
+
+static char* php_password_get_algo_name(const php_password_algo algo)
+{
+ switch (algo) {
+ case PHP_PASSWORD_BCRYPT:
+ return "bcrypt";
+ case PHP_PASSWORD_UNKNOWN:
+ default:
+ return "unknown";
+ }
+}
+
+static php_password_algo php_password_determine_algo(const char *hash, const size_t len)
+{
+ if (len > 3 && hash[0] == '$' && hash[1] == '2' && hash[2] == 'y' && len == 60) {
+ return PHP_PASSWORD_BCRYPT;
+ }
+
+ return PHP_PASSWORD_UNKNOWN;
+}
+
+static zend_bool php_password_salt_is_alphabet(const char *str, const size_t len) /* {{{ */
+{
+ size_t i = 0;
+
+ for (i = 0; i < len; i++) {
+ if (!((str[i] >= 'A' && str[i] <= 'Z') || (str[i] >= 'a' && str[i] <= 'z') || (str[i] >= '0' && str[i] <= '9') || str[i] == '.' || str[i] == '/')) {
+ return 0;
+ }
+ }
+ return 1;
+}
+/* }}} */
@nikic
nikic added a note Sep 12, 2012

I'd make this function return a zend_bool. Not that it matters much, but would make the semantics of the return value more clear.

@ircmaxell
ircmaxell added a note Sep 13, 2012

Done. It now returns a zend_bool...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+
+static zend_bool php_password_salt_to64(const char *str, const size_t str_len, const size_t out_len, char *ret) /* {{{ */
+{
+ size_t pos = 0;
+ size_t ret_len = 0;
+ unsigned char *buffer;
+ if ((int) str_len < 0) {
+ return FAILURE;
+ }
+ buffer = php_base64_encode((unsigned char*) str, (int) str_len, (int*) &ret_len);
+ if (ret_len < out_len) {
+ /* Too short of an encoded string generated */
+ efree(buffer);
+ return FAILURE;
+ }
+ for (pos = 0; pos < out_len; pos++) {
+ if (buffer[pos] == '+') {
+ ret[pos] = '.';
+ } else if (buffer[pos] == '=') {
+ efree(buffer);
+ return FAILURE;
+ } else {
+ ret[pos] = buffer[pos];
+ }
+ }
+ efree(buffer);
+ return SUCCESS;
+}
+/* }}} */
+
+static zend_bool php_password_make_salt(size_t length, char *ret TSRMLS_DC) /* {{{ */
+{
+ int buffer_valid = 0;
+ size_t i, raw_length;
+ char *buffer;
+ char *result;
+
+ if (length > (INT_MAX / 3)) {
+ php_error_docref(NULL TSRMLS_CC, E_WARNING, "Length is too large to safely generate");
+ return FAILURE;
+ }
+
+ raw_length = length * 3 / 4 + 1;
+
+ buffer = (char *) safe_emalloc(raw_length, 1, 1);
+
+#if PHP_WIN32
+ {
+ BYTE *iv_b = (BYTE *) buffer;
+ if (php_win32_get_random_bytes(iv_b, raw_length) == SUCCESS) {
+ buffer_valid = 1;
+ }
+ }
+#else
+ {
+ int fd, n;
+ size_t read_bytes = 0;
+ fd = open("/dev/urandom", O_RDONLY);
+ if (fd >= 0) {
+ while (read_bytes < raw_length) {
+ n = read(fd, buffer + read_bytes, raw_length - read_bytes);
+ if (n < 0) {
+ break;
+ }
+ read_bytes += (size_t) n;
+ }
+ close(fd);
+ }
+ if (read_bytes >= raw_length) {
+ buffer_valid = 1;
+ }
+ }
+#endif
+ if (!buffer_valid) {
+ for (i = 0; i < raw_length; i++) {
+ buffer[i] ^= (char) (255.0 * php_rand(TSRMLS_C) / RAND_MAX);
+ }
+ }
+
+ result = safe_emalloc(length, 1, 1);
+ if (php_password_salt_to64(buffer, raw_length, length, result) == FAILURE) {
+ php_error_docref(NULL TSRMLS_CC, E_WARNING, "Generated salt too short");
+ efree(buffer);
+ efree(result);
+ return FAILURE;
+ }
+ memcpy(ret, result, (int) length);
+ efree(result);
+ efree(buffer);
+ ret[length] = 0;
+ return SUCCESS;
+}
+/* }}} */
+
+PHP_FUNCTION(password_get_info)
@nikic
nikic added a note Sep 17, 2012

password_get_info will return ['algo' => 'unknown', 'options' => []] on failure. Why was this chosen over returning false for example?

@ircmaxell
ircmaxell added a note Sep 17, 2012

Mainly because I wanted a consistent return value. Having it always return an array seemed the better option to requiring yet another branch...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+{
+ php_password_algo algo;
+ int hash_len;
+ char *hash, *algo_name;
+ zval *options;
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &hash, &hash_len) == FAILURE) {
+ return;
+ }
+
+ if (hash_len < 0 || (size_t) hash_len < 0) {
+ php_error_docref(NULL TSRMLS_CC, E_WARNING, "Supplied password hash too long to safely identify");
+ RETURN_FALSE;
+ }
+
+ ALLOC_INIT_ZVAL(options);
+ array_init(options);
+
+ algo = php_password_determine_algo(hash, (size_t) hash_len);
+ algo_name = php_password_get_algo_name(algo);
+
+ switch (algo) {
+ case PHP_PASSWORD_BCRYPT:
+ {
+ long cost = PHP_PASSWORD_BCRYPT_COST;
+ sscanf(hash, "$2y$%ld$", &cost);
+ add_assoc_long(options, "cost", cost);
+ }
+ break;
+ case PHP_PASSWORD_UNKNOWN:
+ default:
+ break;
+ }
+
+ array_init(return_value);
+
+ add_assoc_long(return_value, "algo", algo);
+ add_assoc_string(return_value, "algoName", algo_name, 1);
+ add_assoc_zval(return_value, "options", options);
+}
+
+PHP_FUNCTION(password_needs_rehash)
+{
+ long new_algo = 0;
+ php_password_algo algo;
+ int hash_len;
+ char *hash;
+ HashTable *options = 0;
+ zval **option_buffer;
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sl|H", &hash, &hash_len, &new_algo, &options) == FAILURE) {
+ return;
+ }
+
+ if (hash_len < 0) {
+ php_error_docref(NULL TSRMLS_CC, E_WARNING, "Supplied password hash too long to safely identify");
+ RETURN_FALSE;
+ }
+
+ algo = php_password_determine_algo(hash, (size_t) hash_len);
+
+ if (algo != new_algo) {
+ RETURN_TRUE;
+ }
+
+ switch (algo) {
+ case PHP_PASSWORD_BCRYPT:
+ {
+ long new_cost = PHP_PASSWORD_BCRYPT_COST, cost = 0;
+
+ if (options && zend_symtable_find(options, "cost", sizeof("cost"), (void **) &option_buffer) == SUCCESS) {
+ if (Z_TYPE_PP(option_buffer) != IS_LONG) {
+ zval cast_option_buffer;
+ MAKE_COPY_ZVAL(option_buffer, &cast_option_buffer);
+ convert_to_long(&cast_option_buffer);
+ new_cost = Z_LVAL(cast_option_buffer);
+ zval_dtor(&cast_option_buffer);
+ } else {
+ new_cost = Z_LVAL_PP(option_buffer);
+ }
+ }
+
+ sscanf(hash, "$2y$%ld$", &cost);
+ if (cost != new_cost) {
+ RETURN_TRUE;
+ }
+ }
+ break;
+ case PHP_PASSWORD_UNKNOWN:
+ default:
+ break;
+ }
+ RETURN_FALSE;
+}
+
+/* {{{ proto boolean password_make_salt(string password, string hash)
+Verify a hash created using crypt() or password_hash() */
+PHP_FUNCTION(password_verify)
+{
+ int status = 0, i;
+ int password_len, hash_len;
+ char *ret, *password, *hash;
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss", &password, &password_len, &hash, &hash_len) == FAILURE) {
+ RETURN_FALSE;
+ }
+ if (php_crypt(password, password_len, hash, hash_len, &ret) == FAILURE) {
+ RETURN_FALSE;
+ }
+
+ if (strlen(ret) != hash_len || hash_len < 13) {
+ efree(ret);
+ RETURN_FALSE;
+ }
+
+ /* We're using this method instead of == in order to provide
+ * resistence 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 |= (ret[i] ^ hash[i]);
+ }
+
+ efree(ret);
+
+ RETURN_BOOL(status == 0);
+
+}
+/* }}} */
+
+/* {{{ proto string password_hash(string password, int algo, array options = array())
+Hash a password */
+PHP_FUNCTION(password_hash)
+{
+ char *hash_format, *hash, *salt, *password, *result;
+ long algo = 0;
+ int password_len = 0, hash_len;
+ size_t salt_len = 0, required_salt_len = 0, hash_format_len;
+ HashTable *options = 0;
+ zval **option_buffer;
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sl|H", &password, &password_len, &algo, &options) == FAILURE) {
+ return;
+ }
+
+ switch (algo) {
+ case PHP_PASSWORD_BCRYPT:
+ {
+ long cost = PHP_PASSWORD_BCRYPT_COST;
+
+ if (options && zend_symtable_find(options, "cost", 5, (void **) &option_buffer) == SUCCESS) {
+ if (Z_TYPE_PP(option_buffer) != IS_LONG) {
+ zval cast_option_buffer;
+ MAKE_COPY_ZVAL(option_buffer, &cast_option_buffer);
+ convert_to_long(&cast_option_buffer);
+ cost = Z_LVAL(cast_option_buffer);
+ zval_dtor(&cast_option_buffer);
+ } else {
+ cost = Z_LVAL_PP(option_buffer);
+ }
+ }
+
+ if (cost < 4 || cost > 31) {
+ php_error_docref(NULL TSRMLS_CC, E_WARNING, "Invalid bcrypt cost parameter specified: %ld", cost);
+ RETURN_NULL();
+ }
+
+ required_salt_len = 22;
+ hash_format = emalloc(8);
+ sprintf(hash_format, "$2y$%02ld$", cost);
+ hash_format_len = 7;
+ }
+ break;
+ case PHP_PASSWORD_UNKNOWN:
+ default:
+ php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unknown password hashing algorithm: %ld", algo);
+ RETURN_NULL();
+ }
+
+ if (options && zend_symtable_find(options, "salt", 5, (void**) &option_buffer) == SUCCESS) {
+ char *buffer;
+ int buffer_len_int = 0;
+ size_t buffer_len;
+ switch (Z_TYPE_PP(option_buffer)) {
+ case IS_STRING:
+ buffer = estrndup(Z_STRVAL_PP(option_buffer), Z_STRLEN_PP(option_buffer));
+ buffer_len_int = Z_STRLEN_PP(option_buffer);
+ break;
+ case IS_LONG:
+ case IS_DOUBLE:
+ case IS_OBJECT: {
+ zval cast_option_buffer;
+ MAKE_COPY_ZVAL(option_buffer, &cast_option_buffer);
+ convert_to_string(&cast_option_buffer);
+ if (Z_TYPE(cast_option_buffer) == IS_STRING) {
+ buffer = estrndup(Z_STRVAL(cast_option_buffer), Z_STRLEN(cast_option_buffer));
+ buffer_len_int = Z_STRLEN(cast_option_buffer);
+ zval_dtor(&cast_option_buffer);
+ break;
+ }
+ zval_dtor(&cast_option_buffer);
+ }
+ case IS_BOOL:
+ case IS_NULL:
+ case IS_RESOURCE:
+ case IS_ARRAY:
+ default:
+ efree(hash_format);
+ php_error_docref(NULL TSRMLS_CC, E_WARNING, "Non-string salt parameter supplied");
+ RETURN_NULL();
+ }
+ if (buffer_len_int < 0) {
+ efree(hash_format);
+ efree(buffer);
+ php_error_docref(NULL TSRMLS_CC, E_WARNING, "Supplied salt is too long");
+ }
+ buffer_len = (size_t) buffer_len_int;
+ if (buffer_len < required_salt_len) {
+ efree(hash_format);
+ efree(buffer);
+ php_error_docref(NULL TSRMLS_CC, E_WARNING, "Provided salt is too short: %lu expecting %lu", (unsigned long) buffer_len, (unsigned long) required_salt_len);
+ RETURN_NULL();
+ } else if (0 == php_password_salt_is_alphabet(buffer, buffer_len)) {
+ salt = safe_emalloc(required_salt_len, 1, 1);
+ if (php_password_salt_to64(buffer, buffer_len, required_salt_len, salt) == FAILURE) {
+ efree(hash_format);
+ efree(buffer);
+ efree(salt);
+ php_error_docref(NULL TSRMLS_CC, E_WARNING, "Provided salt is too short: %lu", (unsigned long) buffer_len);
+ RETURN_NULL();
+ }
+ salt_len = required_salt_len;
+ } else {
+ salt = safe_emalloc(required_salt_len, 1, 1);
+ memcpy(salt, buffer, (int) required_salt_len);
+ salt_len = required_salt_len;
+ }
+ efree(buffer);
+ } else {
+ salt = safe_emalloc(required_salt_len, 1, 1);
+ if (php_password_make_salt(required_salt_len, salt TSRMLS_CC) == FAILURE) {
+ efree(hash_format);
+ efree(salt);
+ RETURN_FALSE;
+ }
+ salt_len = required_salt_len;
+ }
+
+ 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;
+
+ efree(hash_format);
+ 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);
+
+ if (php_crypt(password, password_len, hash, hash_len, &result) == FAILURE) {
+ efree(hash);
+ RETURN_FALSE;
+ }
+
+ efree(hash);
+
+ if (strlen(result) < 13) {
+ efree(result);
+ RETURN_FALSE;
+ }
+
+ RETURN_STRING(result, 0);
+}
+/* }}} */
+
+#endif /* HAVE_CRYPT */
+/*
+ * Local variables:
+ * tab-width: 4
+ * c-basic-offset: 4
+ * End:
+ * vim600: sw=4 ts=4 fdm=marker
+ * vim<600: sw=4 ts=4
+ */
View
1 ext/standard/php_crypt.h
@@ -23,6 +23,7 @@
#ifndef PHP_CRYPT_H
#define PHP_CRYPT_H
+PHPAPI int php_crypt(const char *password, const int pass_len, const char *salt, int salt_len, char **result);
PHP_FUNCTION(crypt);
#if HAVE_CRYPT
PHP_MINIT_FUNCTION(crypt);
View
48 ext/standard/php_password.h
@@ -0,0 +1,48 @@
+/*
+ +----------------------------------------------------------------------+
+ | PHP Version 5 |
+ +----------------------------------------------------------------------+
+ | Copyright (c) 1997-2012 The PHP Group |
+ +----------------------------------------------------------------------+
+ | This source file is subject to version 3.01 of the PHP license, |
+ | that is bundled with this package in the file LICENSE, and is |
+ | available through the world-wide-web at the following url: |
+ | http://www.php.net/license/3_01.txt |
+ | If you did not receive a copy of the PHP license and are unable to |
+ | obtain it through the world-wide-web, please send a note to |
+ | license@php.net so we can mail you a copy immediately. |
+ +----------------------------------------------------------------------+
+ | Authors: Anthony Ferrara <ircmaxell@php.net> |
+ +----------------------------------------------------------------------+
+*/
+
+/* $Id$ */
+
+#ifndef PHP_PASSWORD_H
+#define PHP_PASSWORD_H
+
+PHP_FUNCTION(password_hash);
+PHP_FUNCTION(password_verify);
+PHP_FUNCTION(password_needs_rehash);
+PHP_FUNCTION(password_get_info);
+
+PHP_MINIT_FUNCTION(password);
+
+#define PHP_PASSWORD_DEFAULT PHP_PASSWORD_BCRYPT
+
+#define PHP_PASSWORD_BCRYPT_COST 10
+
+typedef enum {
+ PHP_PASSWORD_UNKNOWN,
+ PHP_PASSWORD_BCRYPT
+} php_password_algo;
+
+#endif
+
+
+/*
+ * Local variables:
+ * tab-width: 4
+ * c-basic-offset: 4
+ * End:
+ */
View
1 ext/standard/php_standard.h
@@ -58,6 +58,7 @@
#include "php_versioning.h"
#include "php_ftok.h"
#include "php_type.h"
+#include "php_password.h"
#define phpext_standard_ptr basic_functions_module_ptr
PHP_MINIT_FUNCTION(standard_filters);
View
39 ext/standard/tests/password/password_bcrypt_errors.phpt
@@ -0,0 +1,39 @@
+--TEST--
+Test error operation of password_hash() with bcrypt hashing
+--FILE--
+<?php
+//-=-=-=-
+
+var_dump(password_hash("foo", PASSWORD_BCRYPT, array("cost" => 3)));
+
+var_dump(password_hash("foo", PASSWORD_BCRYPT, array("cost" => 32)));
+
+var_dump(password_hash("foo", PASSWORD_BCRYPT, array("salt" => "foo")));
+
+var_dump(password_hash("foo", PASSWORD_BCRYPT, array("salt" => "123456789012345678901")));
+
+var_dump(password_hash("foo", PASSWORD_BCRYPT, array("salt" => 123)));
+
+var_dump(password_hash("foo", PASSWORD_BCRYPT, array("cost" => "foo")));
+
+?>
+--EXPECTF--
+Warning: password_hash(): Invalid bcrypt cost parameter specified: 3 in %s on line %d
+NULL
+
+Warning: password_hash(): Invalid bcrypt cost parameter specified: 32 in %s on line %d
+NULL
+
+Warning: password_hash(): Provided salt is too short: 3 expecting 22 in %s on line %d
+NULL
+
+Warning: password_hash(): Provided salt is too short: 21 expecting 22 in %s on line %d
+NULL
+
+Warning: password_hash(): Provided salt is too short: 3 expecting 22 in %s on line %d
+NULL
+
+Warning: password_hash(): Invalid bcrypt cost parameter specified: 0 in %s on line %d
+NULL
+
+
View
58 ext/standard/tests/password/password_get_info.phpt
@@ -0,0 +1,58 @@
+--TEST--
+Test normal operation of password_get_info()
+--FILE--
+<?php
+//-=-=-=-
+// Test Bcrypt
+var_dump(password_get_info('$2y$10$MTIzNDU2Nzg5MDEyMzQ1Nej0NmcAWSLR.oP7XOR9HD/vjUuOj100y'));
+// Test Bcrypt Cost
+var_dump(password_get_info('$2y$11$MTIzNDU2Nzg5MDEyMzQ1Nej0NmcAWSLR.oP7XOR9HD/vjUuOj100y'));
+// Test Bcrypt Invalid Length
+var_dump(password_get_info('$2y$11$MTIzNDU2Nzg5MDEyMzQ1Nej0NmcAWSLR.oP7XOR9HD/vjUuOj100'));
+// Test Non-Bcrypt
+var_dump(password_get_info('$1$rasmusle$rISCgZzpwk3UhDidwXvin0'));
+
+echo "OK!";
+?>
+--EXPECT--
+array(3) {
+ ["algo"]=>
+ int(1)
+ ["algoName"]=>
+ string(6) "bcrypt"
+ ["options"]=>
+ array(1) {
+ ["cost"]=>
+ int(10)
+ }
+}
+array(3) {
+ ["algo"]=>
+ int(1)
+ ["algoName"]=>
+ string(6) "bcrypt"
+ ["options"]=>
+ array(1) {
+ ["cost"]=>
+ int(11)
+ }
+}
+array(3) {
+ ["algo"]=>
+ int(0)
+ ["algoName"]=>
+ string(7) "unknown"
+ ["options"]=>
+ array(0) {
+ }
+}
+array(3) {
+ ["algo"]=>
+ int(0)
+ ["algoName"]=>
+ string(7) "unknown"
+ ["options"]=>
+ array(0) {
+ }
+}
+OK!
View
17 ext/standard/tests/password/password_get_info_error.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Test error operation of password_get_info()
+--FILE--
+<?php
+//-=-=-=-
+var_dump(password_get_info());
+var_dump(password_get_info(array()));
+
+echo "OK!";
+?>
+--EXPECTF--
+Warning: password_get_info() expects exactly 1 parameter, 0 given in %s on line %d
+NULL
+
+Warning: password_get_info() expects parameter 1 to be string, array given in %s on line %d
+NULL
+OK!
View
25 ext/standard/tests/password/password_hash.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Test normal operation of password_hash()
+--FILE--
+<?php
+//-=-=-=-
+
+var_dump(strlen(password_hash("foo", PASSWORD_BCRYPT)));
+
+$hash = password_hash("foo", PASSWORD_BCRYPT);
+
+var_dump($hash === crypt("foo", $hash));
+
+var_dump(password_hash("rasmuslerdorf", PASSWORD_BCRYPT, array("cost" => 7, "salt" => "usesomesillystringforsalt")));
+
+var_dump(password_hash("test", PASSWORD_BCRYPT, array("salt" => "123456789012345678901" . chr(0))));
+
+echo "OK!";
+?>
+--EXPECT--
+int(60)
+bool(true)
+string(60) "$2y$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi"
+string(60) "$2y$10$MTIzNDU2Nzg5MDEyMzQ1Nej0NmcAWSLR.oP7XOR9HD/vjUuOj100y"
+OK!
+
View
48 ext/standard/tests/password/password_hash_error.phpt
@@ -0,0 +1,48 @@
+--TEST--
+Test error operation of password_hash()
+--FILE--
+<?php
+//-=-=-=-
+
+var_dump(password_hash());
+
+var_dump(password_hash("foo"));
+
+var_dump(password_hash("foo", array()));
+
+var_dump(password_hash("foo", 19, new StdClass));
+
+var_dump(password_hash("foo", PASSWORD_BCRYPT, "baz"));
+
+var_dump(password_hash(array(), PASSWORD_BCRYPT));
+
+var_dump(password_hash("123", PASSWORD_BCRYPT, array("salt" => array())));
+
+/* Non-string salt, checking for memory leaks */
+var_dump(password_hash('123', PASSWORD_BCRYPT, array('salt' => 1234)));
+
+?>
+--EXPECTF--
+Warning: password_hash() expects at least 2 parameters, 0 given in %s on line %d
+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 long, array given in %s on line %d
+NULL
+
+Warning: password_hash(): Unknown password hashing algorithm: 19 in %s on line %d
+NULL
+
+Warning: password_hash() expects parameter 3 to be array, string given in %s on line %d
+NULL
+
+Warning: password_hash() expects parameter 1 to be string, array given in %s on line %d
+NULL
+
+Warning: password_hash(): Non-string salt parameter supplied in %s on line %d
+NULL
+
+Warning: password_hash(): Provided salt is too short: 4 expecting 22 in %s on line %d
+NULL
View
45 ext/standard/tests/password/password_needs_rehash.phpt
@@ -0,0 +1,45 @@
+--TEST--
+Test normal operation of password_needs_rehash()
+--FILE--
+<?php
+//-=-=-=-
+
+// Invalid Hash, always rehash
+var_dump(password_needs_rehash('', PASSWORD_BCRYPT));
+
+// Valid, as it's an unknown algorithm
+var_dump(password_needs_rehash('', 0));
+
+// Valid with cost the same
+var_dump(password_needs_rehash('$2y$10$MTIzNDU2Nzg5MDEyMzQ1Nej0NmcAWSLR.oP7XOR9HD/vjUuOj100y', PASSWORD_BCRYPT, array('cost' => 10)));
+
+// Valid with cost the same, additional params
+var_dump(password_needs_rehash('$2y$10$MTIzNDU2Nzg5MDEyMzQ1Nej0NmcAWSLR.oP7XOR9HD/vjUuOj100y', PASSWORD_BCRYPT, array('cost' => 10, 'foo' => 3)));
+
+// Invalid, different (lower) cost
+var_dump(password_needs_rehash('$2y$10$MTIzNDU2Nzg5MDEyMzQ1Nej0NmcAWSLR.oP7XOR9HD/vjUuOj100y', PASSWORD_BCRYPT, array('cost' => 09)));
+
+// Invalid, different (higher) cost
+var_dump(password_needs_rehash('$2y$10$MTIzNDU2Nzg5MDEyMzQ1Nej0NmcAWSLR.oP7XOR9HD/vjUuOj100y', PASSWORD_BCRYPT, array('cost' => 11)));
+
+// Valid with cost the default
+$cost = str_pad(PASSWORD_BCRYPT_DEFAULT_COST, 2, '0', STR_PAD_LEFT);
+var_dump(password_needs_rehash('$2y$'.$cost.'$MTIzNDU2Nzg5MDEyMzQ1Nej0NmcAWSLR.oP7XOR9HD/vjUuOj100y', PASSWORD_BCRYPT));
+
+// Should Issue Needs Rehash, Since Foo is cast to 0...
+var_dump(password_needs_rehash('$2y$10$MTIzNDU2Nzg5MDEyMzQ1Nej0NmcAWSLR.oP7XOR9HD/vjUuOj100y', PASSWORD_BCRYPT, array('cost' => 'foo')));
+
+
+
+echo "OK!";
+?>
+--EXPECT--
+bool(true)
+bool(false)
+bool(false)
+bool(false)
+bool(true)
+bool(true)
+bool(false)
+bool(true)
+OK!
View
33 ext/standard/tests/password/password_needs_rehash_error.phpt
@@ -0,0 +1,33 @@
+--TEST--
+Test error operation of password_needs_rehash()
+--FILE--
+<?php
+//-=-=-=-
+var_dump(password_needs_rehash());
+
+var_dump(password_needs_rehash(''));
+
+var_dump(password_needs_rehash('', "foo"));
+
+var_dump(password_needs_rehash(array(), 1));
+
+var_dump(password_needs_rehash("", 1, "foo"));
+
+echo "OK!";
+?>
+--EXPECTF--
+Warning: password_needs_rehash() expects at least 2 parameters, 0 given in %s on line %d
+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 long, string given in %s on line %d
+NULL
+
+Warning: password_needs_rehash() expects parameter 1 to be string, array given in %s on line %d
+NULL
+
+Warning: password_needs_rehash() expects parameter 3 to be array, string given in %s on line %d
+NULL
+OK!
View
21 ext/standard/tests/password/password_verify.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Test normal operation of password_verify)
+--FILE--
+<?php
+//-=-=-=-
+
+var_dump(password_verify(123, 123));
+
+var_dump(password_verify("foo", '$2a$07$usesomesillystringforsalt$'));
+
+var_dump(password_verify('rasmusler', '$2a$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi'));
+
+var_dump(password_verify('rasmuslerdorf', '$2a$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi'));
+echo "OK!";
+?>
+--EXPECT--
+bool(false)
+bool(false)
+bool(false)
+bool(true)
+OK!
View
18 ext/standard/tests/password/password_verify_error.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Test error operation of password_verify()
+--FILE--
+<?php
+//-=-=-=-
+
+var_dump(password_verify());
+
+var_dump(password_verify("foo"));
+
+?>
+--EXPECTF--
+Warning: password_verify() expects exactly 2 parameters, 0 given in %s on line %d
+bool(false)
+
+Warning: password_verify() expects exactly 2 parameters, 1 given in %s on line %d
+bool(false)
+
Something went wrong with that request. Please try again.