Skip to content

Commit

Permalink
Bring password back into compliance with patch, add unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ircmaxell committed Sep 7, 2012
1 parent f35795a commit f1aff4a
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 53 deletions.
207 changes: 154 additions & 53 deletions lib/password.php
@@ -1,56 +1,78 @@
<?php

defined('PASSWORD_BCRYPT') or define('PASSWORD_BCRYPT', '2y');
defined('PASSWORD_BCRYPT') or define('PASSWORD_BCRYPT', 1);

defined('PASSWORD_DEFAULT') or define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);

defined('PASSWORD_BCRYPT_COST') or define('PASSWORD_BCRYPT_COST', 10);

if (!function_exists('password_hash')) {
function password_hash($password, $algo = PASSWORD_DEFAULT, $options = array()) {
/**
* Hash the password using the specified algorithm
*
* @param string $password The password to hash
* @param int $algo The algorithm to use (Defined by PASSWORD_* constants)
* @param array $options The options for the algorithm to use
*
* @returns string|false The hashed password, or false on error.
*/
function password_hash($password, $algo, $options = array()) {
if (!function_exists('crypt')) {
trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
return false;
return null;
}
if (!is_string($password)) {
trigger_error("Password must be a string", E_USER_WARNING);
return false;
trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
return null;
}
if (!$algo) {
$algo = PASSWORD_DEFAULT;
if (!is_int($algo)) {
trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
return null;
}
switch ($algo) {
case PASSWORD_BCRYPT:
$cost = PASSWORD_BCRYPT_COST;
// Note that this is a C constant, but not exposed to PHP, so we don't define it here.
$cost = 10;
if (isset($options['cost'])) {
$cost = $options['cost'];
if ($cost < 4 || $cost > 31) {
trigger_error(sprintf("Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
return false;
trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
return null;
}
}
$required_salt_len = 22;
$hash_format = sprintf("$2y$%02d$", $cost);
break;
default:
trigger_error(sprintf("Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
return false;
trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
return null;
}
if (isset($options['salt'])) {
if (is_string($options['salt'])) {
$salt = $options['salt'];
} else {
trigger_error('Non-string salt parameter supplied', E_USER_WARNING);
return false;
switch (gettype($options['salt'])) {
case 'NULL':
case 'boolean':
case 'integer':
case 'double':
case 'string':
$salt = (string) $options['salt'];
break;
case 'object':
if (method_exists($options['salt'], '__tostring')) {
$salt = (string) $options['salt'];
break;
}
case 'array':
case 'resource':
default:
trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
return null;
}
if (strlen($salt) < $required_salt_len) {
trigger_error(sprintf("Provided salt is too short: %d expecting %d", strlen($salt), $required_salt_len), E_USER_WARNING);
return false;
trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", strlen($salt), $required_salt_len), E_USER_WARNING);
return null;
} elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
$salt = str_replace('+', '.', base64_encode($salt));
}
} else {
$salt = password_make_salt($required_salt_len);
$salt = __password_make_salt($required_salt_len);
}
$salt = substr($salt, 0, $required_salt_len);

Expand All @@ -66,9 +88,77 @@ function password_hash($password, $algo = PASSWORD_DEFAULT, $options = array())
}
}

if (!function_exists('password_get_info')) {
/**
* Get information about the password hash. Returns an array of the information
* that was used to generate the password hash.
*
* array(
* 'algo' => 1,
* 'algoName' => 'bcrypt',
* 'options' => array(
* 'cost' => 10,
* ),
* )
*
* @param string $hash The password hash to extract info from
*
* @return array The array of information about the hash.
*/
function password_get_info($hash) {
$return = array(
'algo' => 0,
'algoName' => 'unknown',
'options' => array(),
);
if (substr($hash, 0, 4) == '$2y$' && strlen($hash) == 60) {
$return['algo'] = PASSWORD_BCRYPT;
$return['algoName'] = 'bcrypt';
list($cost) = sscanf($hash, "$2y$%d$");
$return['options']['cost'] = $cost;
}
return $return;
}
}

if (!function_exists('password_needs_rehash')) {
/**
* Determine if the password hash needs to be rehashed according to the options provided
*
* If the answer is true, after validating the password using password_verify, rehash it.
*
* @param string $hash The hash to test
* @param int $algo The algorithm used for new password hashes
* @param array $options The options array passed to password_hash
*
* @return boolean True if the password needs to be rehashed.
*/
function password_needs_rehash($hash, $algo, array $options = array()) {
$info = password_get_info($hash);
if ($info['algo'] != $algo) {
return true;
}
switch ($algo) {
case PASSWORD_BCRYPT:
$cost = isset($options['cost']) ? $options['cost'] : 10;
if ($cost != $info['options']['cost']) {
return true;
}
break;
}
return false;
}
}

if (!function_exists('password_verify')) {
/**
* Verify a password against a hash using a timing attack resistant approach
*
* @param string $password The password to verify
* @param string $hash The hash to verify against
*
* @return boolean If the password matches the hash
*/
function password_verify($password, $hash) {
if (!function_exists('crypt')) {
trigger_error("Crypt must be loaded for password_create to function", E_USER_WARNING);
Expand All @@ -88,42 +178,53 @@ function password_verify($password, $hash) {
}
}

if (!function_exists('password_make_salt')) {
function password_make_salt($length, $raw_output = false) {
if ($length <= 0) {
trigger_error(sprintf("Length cannot be less than or equal zero: %d", $length), E_USER_WARNING);
return false;
}

if ($raw_output) {
$raw_length = $length;
} else {
$raw_length = (int) ($length * 3 / 4 + 1);
/**
* Function to make a salt
*
* DO NOT USE THIS FUNCTION DIRECTLY
*
* @internal
*/
function __password_make_salt($length) {
if ($length <= 0) {
trigger_error(sprintf("Length cannot be less than or equal zero: %d", $length), E_USER_WARNING);
return false;
}
$buffer = '';
$raw_length = (int) ($length * 3 / 4 + 1);
$buffer_valid = false;
if (function_exists('mcrypt_create_iv')) {
$buffer = mcrypt_create_iv($raw_length, MCRYPT_DEV_URANDOM);
if ($buffer) {
$buffer_valid = true;
}

$buffer_valid = false;

if (function_exists('mcrypt_create_iv')) {
$buffer = mcrypt_create_iv($raw_length, MCRYPT_DEV_URANDOM);
if ($buffer) {
$buffer_valid = true;
}
}
if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
$buffer = openssl_random_pseudo_bytes($raw_length);
if ($buffer) {
$buffer_valid = true;
}
if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
$buffer = openssl_random_pseudo_bytes($raw_length);
if ($buffer) {
$buffer_valid = true;
}
if (!$buffer_valid && file_exists('/dev/urandom')) {
$f = @fopen('/dev/urandom', 'r');
if ($f) {
$read = strlen($buffer);
while ($read < $raw_length) {
$buffer .= fread($f, $raw_length - $read);
$read = strlen($buffer);
}
}
if (!$buffer_valid) {
for ($i = 0; $i < $raw_length; $i++) {
$buffer .= chr(mt_rand(0, 255));
fclose($f);
if ($read >= $raw_length) {
$buffer_valid = true;
}
}

if (!$raw_output) {
$buffer = str_replace('+', '.', base64_encode($buffer));
}
if (!$buffer_valid) {
for ($i = 0; $i < $raw_length; $i++) {
$buffer .= chr(mt_rand(0, 255));
}
return substr($buffer, 0, $length);
}
}
$buffer = str_replace('+', '.', base64_encode($buffer));
return substr($buffer, 0, $length);
}
26 changes: 26 additions & 0 deletions test/Unit/PasswordGetInfoTest.php
@@ -0,0 +1,26 @@
<?php

class PasswordGetInfoTest extends PHPUnit_Framework_TestCase {

public static function provideInfo() {
return array(
array('foo', array('algo' => 0, 'algoName' => 'unknown', 'options' => array())),
array('$2y$', array('algo' => 0, 'algoName' => 'unknown', 'options' => array())),
array('$2y$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi', array('algo' => PASSWORD_BCRYPT, 'algoName' => 'bcrypt', 'options' => array('cost' => 7))),
array('$2y$10$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi', array('algo' => PASSWORD_BCRYPT, 'algoName' => 'bcrypt', 'options' => array('cost' => 10))),

);
}

public function testFuncExists() {
$this->assertTrue(function_exists('password_get_info'));
}

/**
* @dataProvider provideInfo
*/
public function testInfo($hash, $info) {
$this->assertEquals($info, password_get_info($hash));
}

}
84 changes: 84 additions & 0 deletions test/Unit/PasswordHashTest.php
@@ -0,0 +1,84 @@
<?php

class PasswordHashTest extends PHPUnit_Framework_TestCase {

public function testFuncExists() {
$this->assertTrue(function_exists('password_hash'));
}

public function testStringLength() {
$this->assertEquals(60, strlen(password_hash('foo', PASSWORD_BCRYPT)));
}

public function testHash() {
$hash = password_hash('foo', PASSWORD_BCRYPT);
$this->assertEquals($hash, crypt('foo', $hash));
}

public function testKnownSalt() {
$hash = password_hash("rasmuslerdorf", PASSWORD_BCRYPT, array("cost" => 7, "salt" => "usesomesillystringforsalt"));
$this->assertEquals('$2y$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi', $hash);
}

public function testRawSalt() {
$hash = password_hash("test", PASSWORD_BCRYPT, array("salt" => "123456789012345678901" . chr(0)));
$this->assertEquals('$2y$10$MTIzNDU2Nzg5MDEyMzQ1Nej0NmcAWSLR.oP7XOR9HD/vjUuOj100y', $hash);
}

/**
* @expectedException PHPUnit_Framework_Error
*/
public function testInvalidAlgo() {
password_hash('foo', array());
}

/**
* @expectedException PHPUnit_Framework_Error
*/
public function testInvalidAlgo2() {
password_hash('foo', 2);
}

/**
* @expectedException PHPUnit_Framework_Error
*/
public function testInvalidPassword() {
password_hash(array(), 1);
}

/**
* @expectedException PHPUnit_Framework_Error
*/
public function testInvalidSalt() {
password_hash('foo', PASSWORD_BCRYPT, array('salt' => array()));
}

/**
* @expectedException PHPUnit_Framework_Error
*/
public function testInvalidBcryptCostLow() {
password_hash('foo', PASSWORD_BCRYPT, array('cost' => 3));
}

/**
* @expectedException PHPUnit_Framework_Error
*/
public function testInvalidBcryptCostHigh() {
password_hash('foo', PASSWORD_BCRYPT, array('cost' => 32));
}

/**
* @expectedException PHPUnit_Framework_Error
*/
public function testInvalidBcryptCostInvalid() {
password_hash('foo', PASSWORD_BCRYPT, array('cost' => 'foo'));
}

/**
* @expectedException PHPUnit_Framework_Error
*/
public function testInvalidBcryptSaltShort() {
password_hash('foo', PASSWORD_BCRYPT, array('salt' => 'abc'));
}

}

0 comments on commit f1aff4a

Please sign in to comment.