Note: This guide is outdated as of Yii v1.1.14 which introduced the [CPasswordHelper] class. Please use that instead.
Preface ¶
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.
Installation ¶
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
.
Preparing Yii Configuration ¶
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:
- iteration_count_log2 controls the number of iterations for key stretching. A setting of 8 means the hash algorithm will be applied 2^8 = 256 times. This setting should be kept between 4 and 31.
- portable_hashes controls whether portable hashes should be used or not. Portable hashes are salted MD5 hashes prefixed by
$P$
.
Preparing UserIdentity ¶
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.
Preparing the User model ¶
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'),
);
}
Caveats ¶
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.
Using existing 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;
}
Links ¶
- How to manage a PHP application's users and passwords
- Wikipedia (en): bcrypt
- USENIX99 - Original bcrypt proposal
I like this
This is a good option..... I gonna test
Problem...
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.
Check hash success
There is an easy way to ensure the hash was successful. Here is a sample method we use...
public function securePassword($password) { //Instantiate object with configuration $passwordHash = new PasswordHash(Yii::app()->params['phpass']['iteration_count)_log2'],Yii::app()->params['phpass']['portable_hashes']); //Hash the passed password $securedPassword = $passwordHash->HashPassword($password); unset($passwordHash); If (strlen($securedPassword < 20)) //smallest hash is 20 characters { return false; } else { return $securedPassword; } }
RE: Check hash success
Hm, interesting. How often does hashing fail? I've always thought of phpass to be quite failsafe.
RE: Check hash success
It is simply a good practice in our development model to catch the "black swan" unexpected events especially when it comes to security
RE: Check hash success
I see. It would still be interesting to know how often that happens.
open_basedir on /dev/urandom call
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.
RE: open_basedir on /dev/urandom call
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.
Bug in the beforeSave function?
Shouldn't the following line in your beforeSave example be updated from this:
$this->password=$ph->HashPassword($this->password);
to this?
$this->password=$ph->HashPassword($this->password1); // note the 1
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?
RE: Bug in the beforeSave function?
Yes, indeed. Thanks for the pointer. That must have slipped in during clean-up.
Thanks for nice extension
//Is this a vanilla hash? if($record->password{0}!=='$') { $record->password=$ph->HashPassword($this->password); $record->save(); }
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
//Is this a vanilla hash? if($record->password{0}!=='$') { $record->password=$ph->HashPassword($this->password); $record->save(false); }
Though thanks for your effort.
RE: Thanks for nice extension
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.Will check
Will check and still in dev
Login to production issues
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.
password1 & password2 safe?
Hi,
I was wondering, do I need to add this in rule, in addition to compare line?
public function rules() { // NOTE: you should only define rules for those attributes that // will receive user inputs. return array( ... array('password2', 'compare', 'compareAttribute'=>'password1'), array('password1, password2', 'safe'), ... ); }
Without declaring as safe, I cannot go inside the if body although meet the condition.
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(); }
Thank you for the great and helpful wiki!
Daniel
RE: password1 & password2 safe?
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 thepassword1
attribute as safe?Also had to declare password1, password2 safe
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/)
Thanks for the extension
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(); }
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.