From 434d3a71377b52edf0af94d13e31594a785eac20 Mon Sep 17 00:00:00 2001 From: Heath Nail Date: Sat, 21 Jul 2012 12:48:14 -0400 Subject: [PATCH] Add bcrypt support to Security::hash() --- lib/Cake/Test/Case/Utility/SecurityTest.php | 56 ++++++++++ lib/Cake/Utility/Security.php | 111 ++++++++++++++++++-- 2 files changed, 159 insertions(+), 8 deletions(-) diff --git a/lib/Cake/Test/Case/Utility/SecurityTest.php b/lib/Cake/Test/Case/Utility/SecurityTest.php index cb323d9dd..e7ddced30 100644 --- a/lib/Cake/Test/Case/Utility/SecurityTest.php +++ b/lib/Cake/Test/Case/Utility/SecurityTest.php @@ -67,6 +67,46 @@ class SecurityTest extends CakeTestCase { $this->assertTrue(Security::validateAuthKey($authKey)); } +/** + * testHashInvalidSalt method + * + * @expectedException PHPUnit_Framework_Error + * @return void + */ + public function testHashInvalidSalt() { + $result = Security::hash('someKey', 'blowfish', true); + } + +/** + * testHashAnotherInvalidSalt + * + * @expectedException PHPUnit_Framework_Error + * @return void + */ + public function testHashAnotherInvalidSalt() { + $result = Security::hash('someKey', 'blowfish', '$1$lksdjoijfaoijs'); + } + +/** + * testHashYetAnotherInvalidSalt + * + * @expectedException PHPUnit_Framework_Error + * @return void + */ + public function testHashYetAnotherInvalidSalt() { + $result = Security::hash('someKey', 'blowfish', '$2a$10$123'); + } + +/** + * testHashInvalidCost method + * + * @expectedException PHPUnit_Framework_Error + * @return void + */ + public function testHashInvalidCost() { + Security::setCost(1000); + $result = Security::hash('somekey', 'blowfish', false); + } /** * testHash method * @@ -112,6 +152,22 @@ class SecurityTest extends CakeTestCase { $this->assertSame(strlen(Security::hash($key, 'sha256', true)), 64); } + $hashType = 'blowfish'; + Security::setHash($hashType); + Security::setCost(10); // ensure default cost + $this->assertSame(Security::$hashType, $hashType); + $this->assertSame(strlen(Security::hash($key, null, false)), 60); + + $password = $submittedPassword = $key; + $storedPassword = Security::hash($password); + + $hashedPassword = Security::hash($submittedPassword, null, $storedPassword); + $this->assertSame($storedPassword, $hashedPassword); + + $submittedPassword = 'someOtherKey'; + $hashedPassword = Security::hash($submittedPassword, null, $storedPassword); + $this->assertNotSame($storedPassword, $hashedPassword); + Security::setHash($_hashType); } diff --git a/lib/Cake/Utility/Security.php b/lib/Cake/Utility/Security.php index 6438338a2..c9f41008d 100644 --- a/lib/Cake/Utility/Security.php +++ b/lib/Cake/Utility/Security.php @@ -33,6 +33,13 @@ class Security { */ public static $hashType = null; +/** + * Default cost + * + * @var string + */ + public static $hashCost = '10'; + /** * Get allowed minutes of inactivity based on security level. * @@ -76,14 +83,26 @@ class Security { /** * Create a hash from string using given method. * Fallback on next available method. + * If you are using blowfish, for comparisons simply pass the originally hashed + * string as the salt (the salt is prepended to the hash and php handles the + * parsing automagically. Do NOT use a constant salt for blowfish. * * @param string $string String to hash * @param string $type Method to use (sha1/sha256/md5) - * @param boolean $salt If true, automatically appends the application's salt - * value to $string (Security.salt) + * @param mixed $salt If true, automatically appends the application's salt + * value to $string (Security.salt). If you are using blowfish the salt + * must be false or a previously generated salt. * @return string Hash */ public static function hash($string, $type = null, $salt = false) { + if (empty($type)) { + $type = self::$hashType; + } + $type = strtolower($type); + + if ($type === 'blowfish') { + return self::_crypt($string, $type, $salt); + } if ($salt) { if (is_string($salt)) { $string = $salt . $string; @@ -92,11 +111,6 @@ class Security { } } - if (empty($type)) { - $type = self::$hashType; - } - $type = strtolower($type); - if ($type == 'sha1' || $type == null) { if (function_exists('sha1')) { $return = sha1($string); @@ -119,7 +133,7 @@ class Security { * Sets the default hash method for the Security object. This affects all objects using * Security::hash(). * - * @param string $hash Method to use (sha1/sha256/md5) + * @param string $hash Method to use (sha1/sha256/md5/blowfish) * @return void * @see Security::hash() */ @@ -127,6 +141,16 @@ class Security { self::$hashType = $hash; } +/** + * Sets the cost for they blowfish hash method. + * + * @param integer $cost Valid values are 4-31 + * @return void + */ + public static function setCost($cost) { + self::$hashCost = $cost; + } + /** * Encrypts/Decrypts a text using the given key. * @@ -189,4 +213,75 @@ class Security { return $out; } +/** + * Generates a pseudo random salt suitable for use with php's crypt() function. + * The salt length should not exceed 27. The salt will be composed of + * [./0-9A-Za-z]{$length}. + * + * @param integer $length The length of the returned salt + * @return string The generated salt + */ + public static function salt($length = 22) { + return substr(str_replace('+', '.', base64_encode(sha1(uniqid(Configure::read('Security.salt'), true), true))), 0, $length); + } + +/** + * One way encryption using php's crypt() function. + * + * @param string $password The string to be encrypted. + * @param string $type The encryption method to use (blowfish) + * @param mixed $salt false to generate a new salt or an existing salt. + */ + protected static function _crypt($password, $type = null, $salt = false) { + $options = array( + 'saltFormat' => array( + 'blowfish' => '$2a$%s$%s', + ), + 'saltLength' => array( + 'blowfish' => 22, + ), + 'costLimits' => array( + 'blowfish' => array(4, 31), + ) + ); + extract($options); + if ($type === null) { + $hashType = self::$hashType; + } else { + $hashType = $type; + } + $cost = self::$hashCost; + if ($salt === false) { + if (isset($costLimits[$hashType]) && ($cost < $costLimits[$hashType][0] || $cost > $costLimits[$hashType][1])) { + trigger_error(__d( + 'cake_dev', + 'When using %s you must specify a cost between %s and %s', + array( + $hashType, + $costLimits[$hashType][0], + $costLimits[$hashType][1] + ) + ), E_USER_WARNING); + return ''; + } + $vspArgs = array(); + $salt = self::salt($saltLength[$hashType]); + if ($hashType === 'blowfish') { + $bfCost = chr(ord('0') + $cost / 10); + $bfCost .= chr(ord('0') + $cost % 10); + $vspArgs[] = $bfCost; + } + $vspArgs[] = $salt; + $salt = vsprintf($saltFormat[$hashType], $vspArgs); + } elseif ($salt === true || strpos($salt, '$2a$') !== 0 || strlen($salt) < 29) { + trigger_error(__d( + 'cake_dev', + 'Invalid salt: %s for %s Please visit http://www.php.net/crypt and read the appropriate section for building %s salts.', + array($salt, $hashType, $hashType) + ), E_USER_WARNING); + return ''; + } + return crypt($password, $salt); + } + }