Thursday, January 30, 2014

Saving and Retrieving Encrypted Data in Yii

A few people have commented about using md5 or the PASSWORD() capabilities and how to retrieve their data from their MySQL database when updating user passwords and other encrypted data, so I thought it would be a good idea to spend a little time talking about how you can use the CSecurityManager to handle your encrypted data in a way that keeps it completely secure, but allows you to retrieve the original data.

This is not a new feature, it has been around since Yii 1.0, but it's easy to overlook it you don't handle a lot of encryption or need to worry about much other than the occasional casual user field.

The CSecurityManager works as an application component, so to access it you simply call
// $key is optional, it will default to NULL if you don't pass one
Yii::app()->securityManager->encrypt( $string, $key );

and

Yii::app()->securityManager->decrypt( $string, $key );

The most important thing to remember is that your key has to be the same when encrypting and decrypting! This may seem like a no-brainer, but if you're using different keys for various applications, components, or modules, you can quickly run into complications. I like to store the key as a constant that I can access with the model, so that the model can then intelligently report the data if needed. Of course, you need to consider the security of your models before implementing anything that will allow decrypted access.

Back in this old post about saving encrypted passwords I demonstrated the more traditional MySQL PASSWORD() approach. Using the CSecurityManager to handle your encryption will use essentially the same approach, only you'll update the encryptPassword function as follows:

const KEY_VALUE = 'someRandom123'; // <-- make this good

public function encryptPassword()
{
  // Nothing to encrypt
  if ( $this->userpass == '' )
    return;

  $this->userpass = Yii::app()->securityManager->encrypt( 
    $this->userpass, 
    self::KEY_VALUE 
  );
}

Note: I'm using the password field here since it's the previous example and the one thing most people actually make an effort to encrypt. You can encrypt any data that you save, so please don't feel that this example applies only to password data.

Now, if your administrators need to be able to access the encrypted data so that they can answer questions for people, you need a way for the model to see that data cleanly - hello decrypt! You should be VERY cautious about using this. If you handle the decrypted data lightly you're losing the point of encrypting it in the first place.

/**
 * Return the decrypted value of the field (does NOT assign
 * the decrypted value back to the attribute)
 * @return string
 */
public function decryptPassword()
{
  // Nothing to decrypt
  if ( $this->userpass == '' )
    return '';

  return Yii::app()->securityManager->decrypt( 
     $this->userpass, 
     self::KEY_VALUE
  );
    
}

Remember, as in the original example, you're not going to re-encrypt it before every save, only when the data has been updated or the record is new, unless you're decrypting it by default afterFind, which I do not recommend (see above note about the wisdom of handling decrypted secure data).

If you really want to get fancy with your data storage, you could run an encryption filter on all the attributes before saving, and then decrypt them afterFind so that it's impossible to read anything in the database directly. There may be some merit to that idea if you're on a shared database server, but if you trust your database security then that's probably a lot more complicated than you need to make your data storage for any data that isn't truly sensitive.


So, now looking back to the original post about authentication via database, we should update our authentication a bit:

public function authenticate()
{
  $record=User::model()->findByAttributes( array('username'=>$this->username) );
  if($record===null)
    $this->errorCode=self::ERROR_USERNAME_INVALID;
  else if( $this->password != $record->decryptPassword())  // <-- Note the change
    $this->errorCode=self::ERROR_PASSWORD_INVALID;
  else {
    $this->_id=$record->id;
    $this->setState('title', $record->title);
    $this->errorCode=self::ERROR_NONE;
  }
  return !$this->errorCode;
}