Browse Source

Merge pull request #81 from dereuromark/master-pwd

Refactor Passwordable for 2.6+ or PHP5.5+
Mark 11 years ago
parent
commit
7774a41c89

+ 74 - 0
Controller/Component/Auth/ModernPasswordHasher.php

@@ -0,0 +1,74 @@
+<?php
+
+App::uses('AbstractPasswordHasher', 'Controller/Component/Auth');
+
+/**
+ * Modern password hashing class for PHP5.5+.
+ *
+ * This requires either PHP5.5+ or the password_hash() shim from
+ * https://github.com/ircmaxell/password_compat
+ * If you don't use composer, you can also use the class in this repo:
+ *   require CakePlugin::path('Tools') . 'Lib/Bootstrap/Password.php';
+ * Ideally, in your bootstrap.php
+ */
+class ModernPasswordHasher extends AbstractPasswordHasher {
+
+	/**
+	 * Constructor
+	 *
+	 * @param array $config Array of config.
+	 */
+	public function __construct($config = array()) {
+		if (!function_exists('password_hash')) {
+			throw new CakeException('password_hash() is not available.');
+		}
+		parent::__construct($config);
+	}
+
+	/**
+	 * Default config for this object.
+	 *
+	 * @var array
+	 */
+	protected $_config = array(
+		'salt' => null,
+		'cost' => 10,
+		'hashType' => PASSWORD_BCRYPT
+	);
+
+	/**
+	 * Generates password hash.
+	 *
+	 * @param string $password Plain text password to hash.
+	 * @return string Password hash
+	 * @link http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#using-bcrypt-for-passwords
+	 */
+	public function hash($password) {
+		$options = array('cost' => $this->_config['cost'], 'salt' => $this->_config['salt']);
+		$options = array_filter($options);
+		return password_hash($password, $this->_config['hashType'], $options);
+	}
+
+	/**
+	 * Check hash. Generate hash for user provided password and check against existing hash.
+	 *
+	 * @param string $password Plain text password to hash.
+	 * @param string Existing hashed password.
+	 * @return boolean True if hashes match else false.
+	 */
+	public function check($password, $hashedPassword) {
+		return password_verify($password, $hashedPassword);
+	}
+
+	/**
+	 * Returns true if the password need to be rehashed, due to the password being
+	 * created with anything else than the passwords generated by this class.
+	 *
+	 * @param string $password The password to verify
+	 * @return bool
+	 */
+	public function needsRehash($password) {
+		return password_needs_rehash($password, $this->_config['hashType']);
+	}
+
+}

+ 282 - 0
Lib/Bootstrap/Password.php

@@ -0,0 +1,282 @@
+<?php
+/**
+ * A Compatibility library with PHP 5.5's simplified password hashing API.
+ *
+ * Include it via require:
+ *   require CakePlugin::path('Tools') . 'Lib/Bootstrap/Password.php';
+ *
+ * @author Anthony Ferrara <ircmaxell@php.net>
+ * @license http://www.opensource.org/licenses/mit-license.html MIT License
+ * @copyright 2012 The Authors
+ */
+
+namespace {
+
+if (!defined('PASSWORD_DEFAULT')) {
+
+    define('PASSWORD_BCRYPT', 1);
+    define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
+
+    /**
+     * 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
+     *
+     * @return string|false The hashed password, or false on error.
+     */
+    function password_hash($password, $algo, array $options = array()) {
+        if (!function_exists('crypt')) {
+            trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
+            return null;
+        }
+        if (!is_string($password)) {
+            trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
+            return null;
+        }
+        if (!is_int($algo)) {
+            trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
+            return null;
+        }
+        $resultLength = 0;
+        switch ($algo) {
+            case PASSWORD_BCRYPT:
+                // 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("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
+                        return null;
+                    }
+                }
+                // The length of salt to generate
+                $raw_salt_len = 16;
+                // The length required in the final serialization
+                $required_salt_len = 22;
+                $hash_format = sprintf("$2y$%02d$", $cost);
+                // The expected length of the final crypt() output
+                $resultLength = 60;
+                break;
+            default:
+                trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
+                return null;
+        }
+        $salt_requires_encoding = false;
+        if (isset($options['salt'])) {
+            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 (PasswordCompat\binary\_strlen($salt) < $required_salt_len) {
+                trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", PasswordCompat\binary\_strlen($salt), $required_salt_len), E_USER_WARNING);
+                return null;
+            } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
+                $salt_requires_encoding = true;
+            }
+        } else {
+            $buffer = '';
+            $buffer_valid = false;
+            if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
+                $buffer = mcrypt_create_iv($raw_salt_len, MCRYPT_DEV_URANDOM);
+                if ($buffer) {
+                    $buffer_valid = true;
+                }
+            }
+            if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
+                $buffer = openssl_random_pseudo_bytes($raw_salt_len);
+                if ($buffer) {
+                    $buffer_valid = true;
+                }
+            }
+            if (!$buffer_valid && @is_readable('/dev/urandom')) {
+                $f = fopen('/dev/urandom', 'r');
+                $read = PasswordCompat\binary\_strlen($buffer);
+                while ($read < $raw_salt_len) {
+                    $buffer .= fread($f, $raw_salt_len - $read);
+                    $read = PasswordCompat\binary\_strlen($buffer);
+                }
+                fclose($f);
+                if ($read >= $raw_salt_len) {
+                    $buffer_valid = true;
+                }
+            }
+            if (!$buffer_valid || PasswordCompat\binary\_strlen($buffer) < $raw_salt_len) {
+                $bl = PasswordCompat\binary\_strlen($buffer);
+                for ($i = 0; $i < $raw_salt_len; $i++) {
+                    if ($i < $bl) {
+                        $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
+                    } else {
+                        $buffer .= chr(mt_rand(0, 255));
+                    }
+                }
+            }
+            $salt = $buffer;
+            $salt_requires_encoding = true;
+        }
+        if ($salt_requires_encoding) {
+            // encode string with the Base64 variant used by crypt
+            $base64_digits =
+                'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+            $bcrypt64_digits =
+                './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+
+            $base64_string = base64_encode($salt);
+            $salt = strtr(rtrim($base64_string, '='), $base64_digits, $bcrypt64_digits);
+        }
+        $salt = PasswordCompat\binary\_substr($salt, 0, $required_salt_len);
+
+        $hash = $hash_format . $salt;
+
+        $ret = crypt($password, $hash);
+
+        if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != $resultLength) {
+            return false;
+        }
+
+        return $ret;
+    }
+
+    /**
+     * 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 (PasswordCompat\binary\_substr($hash, 0, 4) == '$2y$' && PasswordCompat\binary\_strlen($hash) == 60) {
+            $return['algo'] = PASSWORD_BCRYPT;
+            $return['algoName'] = 'bcrypt';
+            list($cost) = sscanf($hash, "$2y$%d$");
+            $return['options']['cost'] = $cost;
+        }
+        return $return;
+    }
+
+    /**
+     * 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;
+    }
+
+    /**
+     * 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_verify to function", E_USER_WARNING);
+            return false;
+        }
+        $ret = crypt($password, $hash);
+        if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != PasswordCompat\binary\_strlen($hash) || PasswordCompat\binary\_strlen($ret) <= 13) {
+            return false;
+        }
+
+        $status = 0;
+        for ($i = 0; $i < PasswordCompat\binary\_strlen($ret); $i++) {
+            $status |= (ord($ret[$i]) ^ ord($hash[$i]));
+        }
+
+        return $status === 0;
+    }
+}
+
+}
+
+namespace PasswordCompat\binary {
+    /**
+     * Count the number of bytes in a string
+     *
+     * We cannot simply use strlen() for this, because it might be overwritten by the mbstring extension.
+     * In this case, strlen() will count the number of *characters* based on the internal encoding. A
+     * sequence of bytes might be regarded as a single multibyte character.
+     *
+     * @param string $binary_string The input string
+     *
+     * @internal
+     * @return int The number of bytes
+     */
+    function _strlen($binary_string) {
+           if (function_exists('mb_strlen')) {
+               return mb_strlen($binary_string, '8bit');
+           }
+           return strlen($binary_string);
+    }
+
+    /**
+     * Get a substring based on byte limits
+     *
+     * @see _strlen()
+     *
+     * @param string $binary_string The input string
+     * @param int    $start
+     * @param int    $length
+     *
+     * @internal
+     * @return string The substring
+     */
+    function _substr($binary_string, $start, $length) {
+       if (function_exists('mb_substr')) {
+           return mb_substr($binary_string, $start, $length, '8bit');
+       }
+       return substr($binary_string, $start, $length);
+   }
+
+}

+ 100 - 34
Model/Behavior/PasswordableBehavior.php

@@ -1,8 +1,5 @@
 <?php
 App::uses('ModelBehavior', 'Model');
-App::uses('Router', 'Routing');
-App::uses('CakeRequest', 'Network');
-App::uses('CakeResponse', 'Network');
 App::uses('Security', 'Utility');
 
 // @deprecated Use Configure settings instead.
@@ -32,11 +29,13 @@ if (!defined('PWD_MAX_LENGTH')) {
  * keeps the code clean and lean.
  *
  * Now also is capable of:
- * - require current password prior to altering it (current=>true)
- * - don't allow the same password it was before (allowSame=>false)
- * - supporting different auth types and password hashing algorythms
+ * - Require current password prior to altering it (current=>true)
+ * - Don't allow the same password it was before (allowSame=>false)
+ * - Support different auth types and password hashing algorythms
+ * - PasswordHasher support
+ * - Tools.Modern PasswordHasher and password_hash()/password_verify() support
  *
- * @version 1.7 (Now CakePHP2.4/2.5 ready - with passwordHasher support)
+ * @version 1.8 (Now supports Tools.Modern PasswordHasher and password_hash() method)
  * @author Mark Scherer
  * @link http://www.dereuromark.de/2011/08/25/working-with-passwords-in-cakephp
  * @license MIT
@@ -56,11 +55,11 @@ class PasswordableBehavior extends ModelBehavior {
 		'formFieldRepeat' => 'pwd_repeat',
 		'formFieldCurrent' => 'pwd_current',
 		'userModel' => null, // Defaults to User
-		'hashType' => null, // Only for authType Form [cake2.3]
-		'hashSalt' => true, // Only for authType Form [cake2.3]
+		'hashType' => null, // Only for authType Form [Cake2.3]
+		'hashSalt' => true, // Only for authType Form [Cake2.3]
 		'auth' => null, // Which component (defaults to AuthComponent),
-		'authType' => 'Form', // Which type of authenticate (Form, Blowfish, ...) [cake2.4]
-		'passwordHasher' => null, // If a custom pwd hasher is been used [cake2.4]
+		'authType' => 'Form', // Which type of authenticate (Form, Blowfish, ...) [Cake2.4+]
+		'passwordHasher' => null, // If a custom pwd hasher is been used [Cake2.4+]
 		'allowSame' => true, // Don't allow the old password on change
 		'minLength' => PWD_MIN_LENGTH,
 		'maxLength' => PWD_MAX_LENGTH
@@ -143,34 +142,15 @@ class PasswordableBehavior extends ModelBehavior {
 			return false;
 		}
 
-		$auth = 'Auth';
-		if (empty($this->settings[$Model->alias]['auth']) && class_exists('AuthExtComponent')) {
-			$auth = 'AuthExt';
-		} elseif ($this->settings[$Model->alias]['auth']) {
-			$auth = $this->settings[$Model->alias]['auth'];
-		}
-		$authClass = $auth . 'Component';
-		if (!class_exists($authClass)) {
-			throw new CakeException('No Authentication class found (' . $authClass . ')');
-		}
+		return $this->_validateSameHash($Model, $pwd);
 
-		$this->Auth = new $authClass(new ComponentCollection());
 
-		// Easiest authenticate method via form and (id + pwd)
-		$authConfig = array(
-			'fields' => array('username' => 'id', 'password' => $this->settings[$Model->alias]['field']),
-			'userModel' => $this->settings[$Model->alias]['userModel'] ? $this->settings[$Model->alias]['userModel'] : $Model->alias
-		);
 		if (!empty($this->settings[$Model->alias]['passwordHasher'])) {
 			$authConfig['passwordHasher'] = $this->settings[$Model->alias]['passwordHasher'];
 		}
 		$this->Auth->authenticate = array(
 			$this->settings[$Model->alias]['authType'] => $authConfig
 		);
-		$request = Router::getRequest();
-		$request->data[$Model->alias] = array('id' => $uid, 'password' => $pwd);
-		$response = new CakeResponse();
-		return (bool)$this->Auth->identify($request, $response);
 	}
 
 	/**
@@ -218,16 +198,92 @@ class PasswordableBehavior extends ModelBehavior {
 		if (!isset($Model->data[$Model->alias][$Model->primaryKey])) {
 			return true;
 		}
+
 		$primaryKey = $Model->data[$Model->alias][$Model->primaryKey];
-		$value = Security::hash($Model->data[$Model->alias][$formField], $type, $salt);
+		if ($type === 'blowfish' && function_exists('password_hash') && !empty($this->settings[$Model->alias]['passwordHasher'])) {
+			$value = $Model->data[$Model->alias][$formField];
+		} else {
+			$value = Security::hash($Model->data[$Model->alias][$formField], $type, $salt);
+		}
+
 		$dbValue = $Model->field($field, array($Model->primaryKey => $primaryKey));
 		if (!$dbValue) {
 			return true;
 		}
+
+		if ($type === 'blowfish' && function_exists('password_hash') && !empty($this->settings[$Model->alias]['passwordHasher'])) {
+			$PasswordHasher = $this->_getPasswordHasher($this->settings[$Model->alias]['passwordHasher']);
+			return !$PasswordHasher->check($value, $dbValue);
+		}
 		return ($value !== $dbValue);
 	}
 
 	/**
+	 * PasswordableBehavior::_validateSameHash()
+	 *
+	 * @param Model $Model
+	 * @param string $pwd
+	 * @return bool Success
+	 */
+	protected function _validateSameHash(Model $Model, $pwd) {
+		$field = $this->settings[$Model->alias]['field'];
+		$type = $this->settings[$Model->alias]['hashType'];
+		$salt = $this->settings[$Model->alias]['hashSalt'];
+		if ($this->settings[$Model->alias]['authType'] === 'Blowfish') {
+			$type = 'blowfish';
+			$salt = false;
+		}
+
+		$primaryKey = $Model->data[$Model->alias][$Model->primaryKey];
+		$dbValue = $Model->field($field, array($Model->primaryKey => $primaryKey));
+		if (!$dbValue && $pwd) {
+			return false;
+		}
+
+		if ($type === 'blowfish' && function_exists('password_hash') && !empty($this->settings[$Model->alias]['passwordHasher'])) {
+			$value = $pwd;
+		} else {
+			if ($type === 'blowfish') {
+				$salt = $dbValue;
+			}
+			$value = Security::hash($pwd, $type, $salt);
+		}
+
+		if ($type === 'blowfish' && function_exists('password_hash') && !empty($this->settings[$Model->alias]['passwordHasher'])) {
+			$PasswordHasher = $this->_getPasswordHasher($this->settings[$Model->alias]['passwordHasher']);
+			return $PasswordHasher->check($value, $dbValue);
+		}
+		return $value === $dbValue;
+	}
+
+	/**
+	 * PasswordableBehavior::_getPasswordHasher()
+	 *
+	 * @param mixed $hasher Name or options array.
+	 * @return PasswordHasher
+	 */
+	protected function _getPasswordHasher($hasher) {
+		$class = $hasher;
+		$config = array();
+		if (is_array($hasher)) {
+			$class = $hasher['className'];
+			unset($hasher['className']);
+			$config = $hasher;
+		}
+
+		list($plugin, $class) = pluginSplit($class, true);
+		$className = $class . 'PasswordHasher';
+		App::uses($className, $plugin . 'Controller/Component/Auth');
+		if (!class_exists($className)) {
+			throw new CakeException(__d('cake_dev', 'Password hasher class "%s" was not found.', $class));
+		}
+		if (!is_subclass_of($className, 'AbstractPasswordHasher')) {
+			throw new CakeException(__d('cake_dev', 'Password hasher must extend AbstractPasswordHasher class.'));
+		}
+		return new $className($config);
+	}
+
+	/**
 	 * Adding validation rules
 	 * also adds and merges config settings (direct + configure)
 	 *
@@ -376,7 +432,18 @@ class PasswordableBehavior extends ModelBehavior {
 		}
 
 		if (isset($Model->data[$Model->alias][$formField])) {
-			$Model->data[$Model->alias][$field] = Security::hash($Model->data[$Model->alias][$formField], $type, $salt);
+			if ($type === 'blowfish' && function_exists('password_hash') && !empty($this->settings[$Model->alias]['passwordHasher'])) {
+				$cost = !empty($this->settings[$Model->alias]['hashCost']) ? $this->settings[$Model->alias]['hashCost'] : 10;
+				$options = array('cost' => $cost);
+				$PasswordHasher = $this->_getPasswordHasher($this->settings[$Model->alias]['passwordHasher']);
+				$Model->data[$Model->alias][$field] = $PasswordHasher->hash($Model->data[$Model->alias][$formField], $options);
+			} else {
+				$Model->data[$Model->alias][$field] = Security::hash($Model->data[$Model->alias][$formField], $type, $salt);
+			}
+			if (!$Model->data[$Model->alias][$field]) {
+				return false;
+			}
+
 			unset($Model->data[$Model->alias][$formField]);
 			if ($this->settings[$Model->alias]['confirm']) {
 				$formFieldRepeat = $this->settings[$Model->alias]['formFieldRepeat'];
@@ -391,7 +458,6 @@ class PasswordableBehavior extends ModelBehavior {
 
 		// Update whitelist
 		$this->_modifyWhitelist($Model, true);
-
 		return true;
 	}
 

+ 135 - 2
Test/Case/Model/Behavior/PasswordableBehaviorTest.php

@@ -3,6 +3,11 @@ App::uses('ComponentCollection', 'Controller');
 App::uses('AuthComponent', 'Controller/Component');
 App::uses('SimplePasswordHasher', 'Controller/Component/Auth');
 
+if (!function_exists('password_hash')) {
+	require_once CakePlugin::path('Tools') . 'Lib/Bootstrap/Password.php';
+}
+
+
 class PasswordableBehaviorTest extends CakeTestCase {
 
 	public $fixtures = array(
@@ -231,7 +236,6 @@ class PasswordableBehaviorTest extends CakeTestCase {
 			'formFieldCurrent' => 'passw_current',
 			'allowSame' => false,
 			'current' => true,
-			//'userModel' => 'ToolsUser'
 		));
 		$this->User->create();
 		$data = array(
@@ -503,6 +507,7 @@ class PasswordableBehaviorTest extends CakeTestCase {
 			'current' => false,
 			'authType' => 'Blowfish',
 		));
+
 		$this->User->create();
 		$data = array(
 			'pwd' => 'somepwd',
@@ -513,8 +518,9 @@ class PasswordableBehaviorTest extends CakeTestCase {
 		$this->assertTrue((bool)$result);
 		$uid = (string)$this->User->id;
 
-		// Without the current password it will not continue
 		$this->User->Behaviors->load('Tools.Passwordable', array('current' => true));
+
+		// Without the current password it will not continue
 		$this->User->create();
 		$data = array(
 			'id' => $uid,
@@ -552,6 +558,133 @@ class PasswordableBehaviorTest extends CakeTestCase {
 	}
 
 	/**
+	 * Tests that passwords prior to PHP5.5 and/or password_hash() are still working
+	 * if Tools.Modern is being used.
+	 *
+	 * @return void
+	 */
+	public function testBlowfishWithBC() {
+		$this->skipIf(!function_exists('password_hash'), 'password_hash() is not available.');
+
+		$oldHash = Security::hash('foobar', 'blowfish', false);
+		$newHash = password_hash('foobar', PASSWORD_BCRYPT);
+
+		$this->User->Behaviors->load('Tools.Passwordable', array(
+			'allowSame' => false,
+			'current' => false,
+			'authType' => 'Blowfish',
+			'passwordHasher' => 'Tools.Modern'
+		));
+		$this->User->create();
+		$data = array(
+			'pwd' => 'somepwd',
+			'pwd_repeat' => 'somepwd'
+		);
+		$this->User->set($data);
+		$result = $this->User->save();
+		$this->assertTrue((bool)$result);
+		$uid = (string)$this->User->id;
+
+		// Same pwd is not allowed
+		$this->User->create();
+		$data = array(
+			'id' => $uid,
+			'pwd' => 'somepwd',
+			'pwd_repeat' => 'somepwd'
+		);
+		$this->User->set($data);
+		$result = $this->User->save();
+		$this->assertFalse($result);
+
+		$this->User->Behaviors->load('Tools.Passwordable', array('current' => true));
+
+		// Without the correct current password it will not continue
+		$this->User->create();
+		$data = array(
+			'id' => $uid,
+			'pwd_current' => 'somepwdxyz',
+			'pwd' => '123456',
+			'pwd_repeat' => '123456'
+		);
+		$this->User->set($data);
+		$result = $this->User->save();
+		$this->assertFalse($result);
+
+		// Now it will
+		$this->User->create();
+		$data = array(
+			'id' => $uid,
+			'pwd_current' => 'somepwd',
+			'pwd' => '123456',
+			'pwd_repeat' => '123456'
+		);
+		$this->User->set($data);
+		$result = $this->User->save();
+		$this->assertTrue((bool)$result);
+
+		// Lets set a BC password (without password_hash() method but Security class)
+		$data = array(
+			'id' => $uid,
+			'password' => $oldHash,
+		);
+		$result = $this->User->save($data, array('validate' => false));
+		$this->assertTrue((bool)$result);
+
+		// Now it will still work
+		$this->User->create();
+		$data = array(
+			'id' => $uid,
+			'pwd_current' => 'foobar',
+			'pwd' => '123456',
+			'pwd_repeat' => '123456'
+		);
+		$this->User->set($data);
+		$result = $this->User->save();
+		$this->assertTrue((bool)$result);
+
+		// Lets set an invalid BC password (without password_hash() method but Security class)
+		$data = array(
+			'id' => $uid,
+			'password' => $oldHash . 'x',
+		);
+		$result = $this->User->save($data, array('validate' => false));
+		$this->assertTrue((bool)$result);
+
+		// Now it will still work
+		$this->User->create();
+		$data = array(
+			'id' => $uid,
+			'pwd_current' => 'foobar',
+			'pwd' => '123456',
+			'pwd_repeat' => '123456'
+		);
+		$this->User->set($data);
+		$result = $this->User->save();
+		$this->assertFalse($result);
+
+		// Lets set a valid BC password (without password_hash() method but Security class)
+		// But the provided pwd is incorrect
+		$data = array(
+			'id' => $uid,
+			'password' => $oldHash,
+		);
+		$result = $this->User->save($data, array('validate' => false));
+		$this->assertTrue((bool)$result);
+
+		// Now it will still work
+		$this->User->create();
+		$data = array(
+			'id' => $uid,
+			'pwd_current' => 'foobarx',
+			'pwd' => '123456',
+			'pwd_repeat' => '123456'
+		);
+		$this->User->set($data);
+		$result = $this->User->save();
+		$this->assertFalse($result);
+	}
+
+	/**
 	 * PasswordableBehaviorTest::testSettings()
 	 *
 	 * @return void