Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Bring password back into compliance with patch, add unit tests

  • Loading branch information...
commit f1aff4a0fcbf42a156a986a9a474cf01dd0be326 1 parent f35795a
@ircmaxell authored
View
207 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);
@@ -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);
@@ -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);
+}
View
26 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));
+ }
+
+}
View
84 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'));
+ }
+
+}
View
26 test/Unit/PasswordNeedsRehashTest.php
@@ -0,0 +1,26 @@
+<?php
+
+class PasswordNeedsRehashTest extends PHPUnit_Framework_TestCase {
+
+ public static function provideCases() {
+ return array(
+ array('foo', 0, array(), false),
+ array('foo', 1, array(), true),
+ array('$2y$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi', PASSWORD_BCRYPT, array(), true),
+ array('$2y$07$usesomesillystringfore2udlvp1ii2e./u9c8sbjqp8i90dh6hi', PASSWORD_BCRYPT, array('cost' => 7), false),
+ array('$2y$07$usesomesillystringfore2udlvp1ii2e./u9c8sbjqp8i90dh6hi', PASSWORD_BCRYPT, array('cost' => 5), true),
+ );
+ }
+
+ public function testFuncExists() {
+ $this->assertTrue(function_exists('password_needs_rehash'));
+ }
+
+ /**
+ * @dataProvider provideCases
+ */
+ public function testCases($hash, $algo, $options, $valid) {
+ $this->assertEquals($valid, password_needs_rehash($hash, $algo, $options));
+ }
+
+}
View
29 test/Unit/PasswordVerifyTest.php
@@ -0,0 +1,29 @@
+<?php
+
+class PasswordVerifyTest extends PHPUnit_Framework_TestCase {
+
+ public function testFuncExists() {
+ $this->assertTrue(function_exists('password_verify'));
+ }
+
+ public function testFailedType() {
+ $this->assertFalse(password_verify(123, 123));
+ }
+
+ public function testSaltOnly() {
+ $this->assertFalse(password_verify('foo', '$2a$07$usesomesillystringforsalt$'));
+ }
+
+ public function testInvalidPassword() {
+ $this->assertFalse(password_verify('rasmusler', '$2a$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi'));
+ }
+
+ public function testValidPassword() {
+ $this->assertTrue(password_verify('rasmuslerdorf', '$2a$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi'));
+ }
+
+ public function testInValidHash() {
+ $this->assertFalse(password_verify('rasmuslerdorf', '$2a$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hj'));
+ }
+
+}
View
3  test/bootstrap.php
@@ -0,0 +1,3 @@
+<?php
+
+require_once '../lib/password.php';
View
29 test/phpunit.xml.dist
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit backupGlobals="true"
+ backupStaticAttributes="false"
+ bootstrap="./bootstrap.php"
+ colors="false"
+ convertErrorsToExceptions="true"
+ convertNoticesToExceptions="true"
+ convertWarningsToExceptions="true"
+ forceCoversAnnotation="false"
+ mapTestClassNameToCoveredClassName="false"
+ processIsolation="false"
+ stopOnError="false"
+ stopOnFailure="false"
+ stopOnIncomplete="false"
+ stopOnSkipped="false"
+ testSuiteLoaderClass="PHPUnit_Runner_StandardTestSuiteLoader"
+ strict="false"
+ verbose="false">
+ <testsuites>
+ <testsuite name="Unit">
+ <directory>./Unit</directory>
+ </testsuite>
+ </testsuites>
+ <filter>
+ <whitelist>
+ <directory suffix=".php">../lib/</directory>
+ </whitelist>
+ </filter>
+</phpunit>
Please sign in to comment.
Something went wrong with that request. Please try again.