Difference between #6 and #8 of Authenticating against phpass hashes with Yii

unchanged
Title
Authenticating against phpass hashes with Yii
unchanged
Category
How-tos
unchanged
Tags
Authentication, security
changed
Content
Preface
-------
The [Portable PHP password hashing framework](http://www.openwall.com/phpass/
"phpass") 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](http://www.openwall.com/phpass/)
(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:
~~~
[php]
'import'=>array(
  ...,
  'application.extensions.PasswordHash',
),
~~~
Then, locate the `params` stanza and add a new configuration hash for phpass:
~~~
[php]
// 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](http://en.wikipedia.org/wiki/Key_stretching). A setting of 8 means
the hash algorithm will be applied 2^<sup>8</sup> = 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](http://www.yiiframework.com/doc/guide/1.1/en/topics.auth#defining-identity-class
"The Definite Guide to Yii sec. 8.3.1").

~~~
[php]
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:
~~~
[php]
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);$this->password=$ph->HashPassword($this->password);
    }
    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:
~~~
[php]
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
~~~
[php]
else if(!$ph->CheckPassword($this->password, $record->password))
~~~
in `UserIdentity` with:
~~~
[php]
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:
~~~
[php]
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](http://www.openwall.com/articles/PHP-Users-Passwords)
- [Wikipedia (en): bcrypt](http://en.wikipedia.org/wiki/Bcrypt
"[Wikipedia (en)] bcrypt")
- [USENIX99](http://www.usenix.org/events/usenix99/provos.html "USENIX99: A
Future-Adaptable Password Scheme") - Original bcrypt proposal