The Portable PHP password hashing framework allows advanced password hashing offering increased security over simple MD5- or SHA1-hashed passwords. phpass is already in use in some larger projects such as WordPress (since v2.5), Drupal 7 and phpBB 3.
Fetch the latest version of phpass from here (At the time of this writing, that is v0.3), and extract the contained PasswordHash.php to application.extensions.
Open your application/config/main.php and locate the import stanza. Add another entry like this:
'import'=>array( ..., 'application.extensions.PasswordHash', ),
Then, locate the params stanza and add a new configuration hash for phpass:
// application-level parameters that can be accessed // using Yii::app()->params['paramName'] 'params'=>array( ..., 'phpass'=>array( 'iteration_count_log2'=>8, 'portable_hashes'=>false, ), ),
This will allow to change the phpass configuration rapidly. For reference:
$P$.The following snippet is a modified UserIdentity class taken from the Definite Guide to Yii section 8.3: Authentication and Authorization - Defining Identity Class.
class UserIdentity extends CUserIdentity { private $_id; public function authenticate() { $record=User::model()->findByAttributes(array('username'=>$this->username)); $ph=new PasswordHash(Yii::app()->params['phpass']['iteration_count_log2'], Yii::app()->params['phpass']['portable_hashes']); if($record===null) $this->errorCode=self::ERROR_USERNAME_INVALID; else if(!$ph->CheckPassword($this->password, $record->password)) $this->errorCode=self::ERROR_PASSWORD_INVALID; else { $this->_id=$record->id; $this->errorCode=self::ERROR_NONE; } return !$this->errorCode; } public function getId() { return $this->_id; } }
This will check submitted passwords against a hash stored in database. Now we just need to create those hashes.
We need to overwrite the CActiveRecord::beforeSave() method so we can hash the password before it gets sent to the database:
class User extends CActiveRecord { public $password1; public $password2; ... public function beforeSave() { if(!empty($this->password1) && $this->password1==$this->password2) { $ph=new PasswordHash(Yii::app()->params['phpass']['iteration_count_log2'], Yii::app()->params['phpass']['portable_hashes']); $this->password=$ph->HashPassword($this->password1); } return parent::beforeSave(); } }
In the example above, User.password1 and User.password2 are expected to be filled by a form updating the user's password. This is in an effort to keep existing users from being crippled by updates that do not involve password changes. In addition, you might want to add the following validation rule:
public function rules() { return array( ..., array('password2', 'compare', 'compareAttribute'=>'password1'), ); }
If you are storing your users in a database, see to it that the password field has sufficient length (at least 60 characters). Otherwise you'll be risking truncated, invalid hashes.
phpass is using salted hashes exclusively and is using a custom base64 scheme. So there is no way to "upgrade" existing hashes to phpass hashes. However, since the output of phpass looks entirely different than the output of md5(), sha1(), etc., it is possible to use both schemes in parallel. Just replace the line
else if(!$ph->CheckPassword($this->password, $record->password))
in UserIdentity with:
else if(md5($this->password)!==$record->password && !$ph->CheckPassword($this->password, $record->password))
Of course, you can also try to set a seamless upgrade mechanism in place by creating a new hash every time a user logs in with his correct credentials that are still using the old scheme. Just replace the UserIdentity::authenticate() method with this:
public function authenticate() { $record=User::model()->findByAttributes(array('username'=>$this->username)); $ph=new PasswordHash(Yii::app()->params['phpass']['iteration_count_log2'], Yii::app()->params['phpass']['portable_hashes']); if($record===null) $this->errorCode=self::ERROR_USERNAME_INVALID; else if(md5($this->password)!==$record->password && !$ph->CheckPassword($this->password, $record->password)) $this->errorCode=self::ERROR_PASSWORD_INVALID; else { //Is this a vanilla hash? if($record->password{0}!=='$') { $record->password=$ph->HashPassword($this->password); $record->save(); } $this->_id=$record->id; $this->errorCode=self::ERROR_NONE; } return !$this->errorCode; }
Total 18 comments
I played with a few different extensions but think this makes sense.
Should it update non-encrypted passwords with the new hash? It seems like it hits the "Password Incorrect" Error and stops.
Do I need to apply a hash to all my data first?
It seems like this was meant to deal with unhashed passwords but it doesn't work for me.
I removed the md5 reference as my passwords were unhashed initially, that seems to allow the upgrade to hashed passwords without users knowing. Thanks again! //Is this a vanilla hash? if($record->password{0}!=='$') { $record->password=$ph->HashPassword($this->password); $record->save(); }FWIW I also had to add array('password1, password2', 'safe'), to the rules using yii 1.1.13. Without that line $this->password1 and $this->password2 are always blank in beforeSave. Adding just password1 as safe would not update the password successfully.
Thanks for a great article. Very informative and easy to implement.
*Edit: It may be worth noting that I integrated the above into the yii-user extenstion (http://www.yiiframework.com/extension/yii-user/)
Uhm, well ... attributes are considered safe if they are covered by any rule in the current scenario. Could be that this is what is happening with
password1. Could you try to declare just thepassword1attribute as safe?Hi,
I was wondering, do I need to add this in rule, in addition to compare line?
Without declaring as safe, I cannot go inside the if body although meet the condition.
Thank you for the great and helpful wiki!
Daniel
Need help if anyone has had this problem after configuring phpass on my yii app and tested on my local machine am able to login but moving to production server it wont log me in. Tried my other host and works fine. The prod is running on php5.2 and the local and other hosting are running php5.3. Thanks.
Will check and still in dev
That's mighty strange. You should better check which rule it is that's missfiring (if it is missfiring) in
User.rules(). Just usingsave(false)is no good workaround.Trying to encrypt existing passwords with this method. I had issues with my app until I had to add false in the save method. As in
Though thanks for your effort.
Yes, indeed. Thanks for the pointer. That must have slipped in during clean-up.
Shouldn't the following line in your beforeSave example be updated from this:
to this?
Because you want to test the passwords against themselves and then, if they match, you want to hash the password and then save the hash into the password variable. Correct?
Thanks for reporting this. This hasn't come to my attention either. But it looks like the authors took care of that about 10 months ago. Their fix is identical to yours. Unfortunately, it hasn't made it in any release yet.
Hi,
a customer stumbled across a problem with phpass today: the extension tries to check if /dev/urandom is readable (File PasswordHash.php, Line 51). If the server enforces an open_basedir setting, the call to is_readable will throw an php error (open_basedir restriction in effect - you do not want to add /dev to open_basedir!) which breaks the application.
This slipped through my test environment as it is not using open_basedir :(
Add an @ before the is_readable call to fix this problem.
I will try to contact the original author of this file about this problem.
I see. It would still be interesting to know how often that happens.
It is simply a good practice in our development model to catch the "black swan" unexpected events especially when it comes to security
Hm, interesting. How often does hashing fail? I've always thought of phpass to be quite failsafe.
There is an easy way to ensure the hash was successful. Here is a sample method we use...
In hindsight, the solution with User.beforeSave() isn't that great. If an existing User would be updated, the hashed password might be hashed again, thus making logging in impossible. I'll need to refine that example soon.
Edit: Should be all good now.
This is a good option..... I gonna test
Leave a comment
Please login to leave your comment.