Merge branch 'pull-request/1997'

This commit is contained in:
Christoph M. Becker 2016-09-08 17:00:07 +02:00
commit 1305fdaa3b
9 changed files with 411 additions and 58 deletions

View File

@ -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 <argon2.h> 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

View File

@ -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);

View File

@ -13,6 +13,7 @@
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
| Authors: Anthony Ferrara <ircmaxell@php.net> |
| Charles R. Portwood II <charlesportwoodii@erianna.com> |
+----------------------------------------------------------------------+
*/
@ -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;
algo = php_password_determine_algo(hash, (size_t) hash_len);
switch(algo) {
#if HAVE_ARGON2LIB
case PHP_PASSWORD_ARGON2I:
{
argon2_type type = Argon2_i;
status = argon2_verify(hash, password, password_len, type);
if (status == ARGON2_OK) {
RETURN_TRUE;
}
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);
}
}
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();
}
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);
}
if (cost < 4 || cost > 31) {
php_error_docref(NULL, E_WARNING, "Invalid bcrypt cost parameter specified: " ZEND_LONG_FMT, cost);
RETURN_NULL();
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;
}
required_salt_len = 22;
sprintf(hash_format, "$2y$%02ld$", (long) cost);
hash_format_len = 7;
}
break;
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);
if (ZSTR_LEN(result) < 13) {
zend_string_free(result);
RETURN_FALSE;
}
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;
}
efree(hash);
if (ZSTR_LEN(result) < 13) {
zend_string_free(result);
RETURN_FALSE;
}
RETURN_STR(result);
}
/* }}} */

View File

@ -13,6 +13,7 @@
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
| Authors: Anthony Ferrara <ircmaxell@php.net> |
| Charles R. Portwood II <charlesportwoodii@erianna.com> |
+----------------------------------------------------------------------+
*/
@ -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

View File

@ -0,0 +1,29 @@
--TEST--
Test normal operation of password_get_info() with Argon2
--SKIPIF--
<?php
if (!defined('PASSWORD_ARGON2I')) die('Skipped: password_get_info not built with Argon2');
?>
--FILE--
<?php
var_dump(password_get_info('$argon2i$v=19$m=65536,t=3,p=1$SWhIcG5MT21Pc01PbWdVZw$WagZELICsz7jlqOR2YzoEVTWb2oOX1tYdnhZYXxptbU'));
echo "OK!";
?>
--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!

View File

@ -0,0 +1,18 @@
--TEST--
Test normal operation of password_hash() with argon2
--SKIPIF--
<?php
if (!defined('PASSWORD_ARGON2I')) die('Skipped: password_hash not built with Argon2');
--FILE--
<?php
$password = "the password for testing 12345!";
$hash = password_hash($password, PASSWORD_ARGON2I);
var_dump(password_verify($password, $hash));
echo "OK!";
?>
--EXPECT--
bool(true)
OK!

View File

@ -0,0 +1,21 @@
--TEST--
Test error operation of password_hash() with argon2
--SKIPIF--
<?php
if (!defined('PASSWORD_ARGON2I')) die('Skipped: password_hash not built with Argon2');
?>
--FILE--
<?php
var_dump(password_hash('test', PASSWORD_ARGON2I, ['memory_cost' => 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

View File

@ -0,0 +1,22 @@
--TEST--
Test normal operation of password_needs_rehash() with argon2
--SKIPIF--
<?php
if (!defined('PASSWORD_ARGON2I')) die('Skipped: password_needs_rehash not built with Argon2');
?>
--FILE--
<?php
$hash = '$argon2i$v=19$m=65536,t=3,p=1$YkprUktYN0lHQTd2bWRFeA$79aA+6IvgclpDAJVoezProlqzIPy7do/P0sBDXS9Nn0';
var_dump(password_needs_rehash($hash, PASSWORD_ARGON2I));
var_dump(password_needs_rehash($hash, PASSWORD_ARGON2I, ['memory_cost' => 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!

View File

@ -0,0 +1,18 @@
--TEST--
Test normal operation of password_verify() with argon2
--SKIPIF--
<?php
if (!defined('PASSWORD_ARGON2I')) die('Skipped: password_verify not built with Argon2');
?>
--FILE--
<?php
var_dump(password_verify('test', '$argon2i$v=19$m=65536,t=3,p=1$OEVjWWs2Z3YvWlNZQ0ZmNw$JKin7ahjmh8JYvMyFcXri0Ss/Uvd3uYpD7MG6C/5Cy0'));
var_dump(password_verify('argon2', '$argon2i$v=19$m=65536,t=3,p=1$OEVjWWs2Z3YvWlNZQ0ZmNw$JKin7ahjmh8JYvMyFcXri0Ss/Uvd3uYpD7MG6C/5Cy0'));
echo "OK!";
?>
--EXPECT--
bool(true)
bool(false)
OK!