Cipher Behavior

This article is also available in the following languages:
By xemle
The Cipher Behavior encrypts/decrypts model properties to protect passwords, emails, etc via symmetric encryption BlowFish. This behavior is very useful if your data should not be stored in clear text in the database (e.g. external database connections).
If your want to cipher critical data of your model like passwords or emails, this cipher behavior handles the symmetric encryption and decryption on the fly. For the symmetric encryption the cipher BlowFish is required, but other symmetric encryption algorithms might be used. This behavior is very useful if your data should not be stored in clear text in the database (e.g. external database connections).

The behavior could be configured simple for your needs. By default the behavior ciphers the password Model property. The code is from the behavior of the multi-user image gallery phtagr.org.

This behavior is similar to the crypter-component http://bakery.cakephp.org/articles/view/crypter-component but implements an automatic encryption to the model.

The Behavior


First of all the PEAR package of BlowFish must be installed. This packages comes with an PHP implementation of BlowFish and does not required the MCrypt PHP extension. If the MCrypt extension is installed, it will use it.

pear install Crypt_Blowfish
Download the behavior to your /models/behaviors directory.

Behavior Class:

<?php 
require_once("Crypt/Blowfish.php");

class 
CipherBehavior extends ModelBehavior 
{
  
/** Default values of behavior.
    @key Symmetric key. Default is value of 'Security.salt' configuration.
    @cipher Columns to cipher. Default is 'password'.
    @prefix Prefix of ciphered values. Default is '$E$'.
    @saltLen Length of salt as prefix and suffix. The salt ensures different
    outputs for the same input. Default is 4. 
    @padding Padding of ciphered value. Default is 4.
    @autoDecrypt Decrypt ciphered value automatically. Default is false. */
  
var $default = array(
                    
'cipher' => 'password'
                    
'prefix' => '$E$'
                    
'saltLen' => 4
                    
'padding' => 4
                    
'autoDecrypt' => false,
                    
'noEncrypt' => false
                  
);
  var 
$config = array();

  function 
setup(&$model$config = array()) {
    
$this->config[$model->name] = $this->default;

    if (isset(
$config['key']))
      
$this->config[$model->name]['key'] = $config['key'];
    else
      
$this->config[$model->name]['key'] = Configure::read('Security.salt');

    if (isset(
$config['cipher']))
      
$this->config[$model->name]['cipher'] = $config['cipher'];

    if (isset(
$config['prefix']))
      
$this->config[$model->name]['prefix'] = $config['prefix'];

    if (isset(
$config['saltLen']))
      
$this->config[$model->name]['saltLen'] = $config['saltLen'];

    if (isset(
$config['padding']) && $config['padding'] <= 32)
      
$this->config[$model->name]['padding'] = $config['padding'];

    if (isset(
$config['autoDecrypt']))
      
$this->config[$model->name]['autoDecrypt'] = $config['autoDecrypt'];

    if (isset(
$config['noEncrypt']))
      
$this->config[$model->name]['noEncrypt'] = $config['noEncrypt'];
  }

  
/** Model hook to encrypt model data 
    @param model Current model */
  
function beforeSave(&$model) {
    if (isset(
$this->config[$model->name]) && !$this->config[$model->name]['noEncrypt']) {
      if (!
is_array($this->config[$model->name]['cipher'])) {
        
$cipher = array($this->config[$model->name]['cipher']);
      } else {
        
$cipher $this->config[$model->name]['cipher'];
      }

      
$prefix $this->config[$model->name]['prefix'];
      
$prefixLen strlen($prefix);

      foreach (
$cipher as $column) {
        if (!empty(
$model->data[$model->name][$column]) && 
          
substr($model->data[$model->name][$column], 0$prefixLen) != $prefix) {
          
$encrypt $this->_encryptValue($model->data[$model->name][$column], $this->config[$model->name]);
          if (
$encrypt) {
            
$model->data[$model->name][$column] = $encrypt;
          } else {
            
$this->log(__METHOD__." Could not encrypt {$model->name}::$column: '$model->data[$model->name][$column]'");
          }
        }
      }
    }
  
    return 
true;
  }

  
/** Model hook to decrypt model data if auto decipher is turned on in the
    * model behavior configuration. Only primary model data are decrypted. */
  
function afterFind(&$model$result$primary false) {
    if (!
$result || !isset($this->config[$model->name]['cipher']))
      return 
$result;
    
    if (
$primary && $this->config[$model->name]['autoDecrypt']) {
      
// check for single of multiple model
      
$keys array_keys($result);
      if (!
is_numeric($keys[0])) {
        
$this->decrypt(&$model, &$result);
      } else {
        foreach(
$keys as $index) {
          
$this->decrypt(&$model, &$result[$index]);
        }
      }
    }
    return 
$result;
  }

  
/** Decrypt model value
    @param model Current model
    @param data Current model data. If null, the Model::data is used 
    @return Deciphered model data */
  
function decrypt(&$model, &$data null) {
    if (
$data === null)
      
$data =& $model->data;
    if (isset(
$this->config[$model->name])) {
      if (!
is_array($this->config[$model->name]['cipher'])) {
        
$cipher = array($this->config[$model->name]['cipher']);
      } else {
        
$cipher $this->config[$model->name]['cipher'];
      }

      
$prefix $this->config[$model->name]['prefix'];
      
$prefixLen strlen($prefix);
      foreach (
$cipher as $column) {
        if (!empty(
$data[$model->name][$column]) && 
          
substr($data[$model->name][$column], 0$prefixLen) == $prefix) {
          
$decrypt $this->_decryptValue($data[$model->name][$column], $this->config[$model->name]);
          if (
$decrypt) {
            
$data[$model->name][$column] = $decrypt;
          } else {
            
$this->log(__METHOD__." Could not decrpyt {$model->name}::$column: '{$data[$model->name][$column]}'");
          }
        }
      }
    }
    return 
$data;
  }

  
/** Create salt for cipher's envelope. The salt is an random string which
   * depends on the random generator, the value, the key and on the previous
   * generated character.
    @param value Value to cipher
    @param key Key for encrpytion.
    @param len Length of resulting salt. Default is 4
    @return Randomly generated salt of the given lenth */
  
function _generateSalt($value$key '9nHPrYcxmvTliA'$len 4) {
    
srand(getMicrotime()*1000);
    
$salt '';
    
$lenKey strlen($key);
    
$lenValue strlen($value);
    
$old rand(0255);
    for(
$i 0$i $len$i++) {
      
$n ord($key[$i $lenKey]);
      for (
$j 0$j $n$j++) {
        
$toss rand(0255);
      }
      
$toss ^= $n;
      
$toss ^= ord($value[$i $lenValue]);
      
$toss ^= $old;
      
$salt .= chr($toss);
      
$old $toss;
    }
    return 
$salt;
  }

  
/** Packs a value with a surrounding salt value. Additionaly the resulting
   * envelope could be aligned
    @param value Value to envelope
    @param salt Salt which builds the prefix and suffix of the envelope
    @param padding Alignment size. Default is 4
    @return Envelope with salt 
    @see _unpackValue() */
  
function _packValue($value$salt$padding) {
    
$l strlen($value) + strlen($salt);
    
$lp $l $padding;
    
$pad '';
    if (
$lp) {
      
$pad str_repeat(chr(0), $lp-1).chr($lp);
    }
    return 
$salt.$value.$pad.$salt;
  }

  
/** Unpacks an envelope and returns the packed value
    @param envelope
    @return Value or false on an error 
    @see _packValue() */
  
function _unpackValue($envelope$saltLen) {
    
$l strlen($envelope);
    if (
$l 2*$saltLen) {
      
$this->log(__METHOD__." Value for unpacking is to short");
      return 
false;
    }
    
$salt substr($envelope0$saltLen);
    if (
$salt != substr($envelope$l $saltLen$saltLen)) {
      
$this->log(__METHOD__." Enclosed salt missmatch: '$salt' != '".substr($envelope$l $saltLen$saltLen)."' $l");
      return 
false;
    }
    
$pad ord(substr($envelope$l $saltLen -11));
    if (
$pad 32
      
$pad 0;
    
$value substr($envelope$saltLen$l - ($saltLen) - $pad);
    return 
$value;
  }

  
/** Encrpytes a value using the blowfish cipher. As key the Security.salt
    * value is used 
    @param value Value to cipher
    @return Return of the chiphered value in base64 encoding. To distinguish
    ciphed value, the ciphed value has a prefix of '$E$' i
    @see _decryptValue(), _packValue(), _generateSalt() */  
  
function _encryptValue($value$config) {
    
extract($config);
    
$bf = new Crypt_Blowfish($key);

    
$enclose $this->_packValue($value$this->_generateSalt($value$key$saltLen), $padding);
    
$encrypted $bf->encrypt($enclose);
    if (
PEAR::isError($encrypted)) {
      
$this->log($encrypted->getMessage());
      return 
false;
    }
    return 
$prefix.base64_encode($encrypted);
  }

  
/** Decrpyted the given base64 string using the blowfish cipher
    @param base64Value Base 64 encoded string.
    @see _encryptValue(), _unpackValue() */
  
function _decryptValue($base64Value$config) {
    
extract($config);
    
$prefixLen strlen($prefix);
    if (
substr($base64Value0$prefixLen) != $prefix) {
      
$this->log(__METHOD__." Security prefix is missing: '$base64Value'");
      return 
false;
    }
    
$encrypted  base64_decode(substr($base64Value$prefixLen));
    if (
$encrypted === false) {
      
$this->log(__METHOD__." Could not decode base64 value '$base64Value'");
      return 
false;
    }
    
$bf = new Crypt_Blowfish($key);

    
$envelope trim($bf->decrypt($encrypted), chr(0));
    
$value $this->_unpackValue($envelope$saltLen);
    if (
$value === false) {
      
$this->log(__METHOD__." Could not unpack value from '$envelope'");
      return 
false;
    }

    if (
PEAR::isError($value)) {
      
$this->log($value->getMessage());
      return 
false;
    }
    return 
$value;
  }

}
?>

Usage


As mentioned above, the behavior ciphers the password property/table column by default.

Model Class:

<?php 
class User extends AppModel
{
  var 
$name 'User';

  var 
$actsAs = array('Cipher' => array());

}
?>

Following example saves the User model. Submit your login data via a formular. The $this->data might look like:

Array
(
    [User] => Array
        (
            [id] => 1
            [username] => admin
            [password] => MySecret
        )
)

In the controller you save your submitted data:

Controller Class:

<?php 
$this
->User->save($this->data);
?>

Now every time a User is saved, the password will be ciphered. The behavior only ciphers the properties, if the values do not start with the ciphered prefix $E$.

The ciphered data looks now like:

Array
(
    [User] => Array
        (
            [id] => 1
            [username] => admin
            [password] => $E$fIOGYbF6jQMXOOa5umzgXGWBfo7roAuk
        )
)

By default the behavior does not decrypt the properties and the decryption must be called explicitly:

Controller Class:

<?php 
$user 
$this->User->findByUsername($this->data['User']['username']);
$this->User->decrypt(&$user);
if (
$user['User']['password'] == $this->data['User']['password']) {
  
// successful login
}
?>

Configuration


Automatic Decryption


If you want to decrpyt all data automatically (might cost some CPU cycles and slows down your requests), you can configure the cipher behavior:

Model Class:

<?php 
class User extends AppModel
{
  var 
$name 'User';

  var 
$actsAs = array('Cipher' => array('autoDecypt' => true));
}
?>

Model Properties


By default, the cipher behavior encrypts and decrypts the model property (table column) password. Other fields are also possible.

Model Class:

<?php 
class User extends AppModel
{
  var 
$name 'User';

  var 
$actsAs = array('Cipher' => array('cipher' => array('password''email''creditnumber')));
}
?>

Custom Key


By default, the cipher behavior uses the Security.salt as cipher key. If you require a custom key, you can set in on the configuration:

Model Class:

<?php 
class User extends AppModel
{
  var 
$name 'User';

  var 
$actsAs = array('Cipher' => array('key' => 'MySuperSecureCipherKey'));
}
?>

Note: Since the Security.salt is used from your configuration config/core.php and cipher key, it is very important to change the default value of Security.salt! Otherwise the encryption is not secure!

 /**
  * A random string used in security hashing methods.
  */
      Configure::write('Security.salt', 'NewSecureAndUnknownSecuritySaltForCake');

Salt and Padding


Before a value is encrypted it will be packed and padded. The clear text before the value is ciphered is surrounded by a salt and padded to a specific length block to $salt.$value.$padding.$salt.

The salt is used to avoid same encrypted results of same values. It is also used to discover the correct decryption. The padding is used to hide the original value lengths. By default, the salt and padding have the length of 4. This could be changed in the behavior configuration.

Note: The salt should be at least 2 characters long. Otherwise the successful decryption could not be detected well (apart of the diversity of the ciphered value).

Model Class:

<?php 
class User extends AppModel
{
  var 
$name 'User';

  var 
$actsAs = array('Cipher' => array('saltLen' => 6'padding' => 8));
}
?>

Prefix


To distinguish between ciphered value and a clear text value, the ciphered value has a prefix. The default prefix is $E$ but could be change in the configuration.

Model Class:

<?php 
class User extends AppModel
{
  var 
$name 'User';

  var 
$actsAs = array('Cipher' => array('prefix' => '$ciphered$'));
}
?>

Debug


The behavior dumps log message to the standard log if something goes wrong. Please watch these entries while developing with the cipher behavior.

Changing Security.salt


If you using this behavior and some data is already ciphered but have to change the Security.salt, you need to decrypt all the data with the old Security.salt, save the clear text and encrypt all values with the new Security.salt.

Decrypt all values with the old Security.salt value:

Model Class:

<?php 
class User extends AppModel
{
  var 
$name 'User';

  var 
$actsAs = array('Cipher' => array('noEncrypt' => true'autoDecrypt' => true));

  function 
clearCipher() {
    
$users $this->findAll();
    foreach (
$users as $user) {
      
$this->id $user['User']['id'];
      
$this->save($user);
    }
  }
}
?>

Controller Class:

<?php 
$this
->User->clearCipher();
?>

Encrypt now all values with the new Security.salt.

Model Class:

<?php 
class User extends AppModel
{
  var 
$name 'User';

  var 
$actsAs = array('Cipher' => array());

  function 
cipherAll() {
    
$users $this->findAll();
    foreach (
$users as $user) {
      
$this->id $user['User']['id'];
      
$this->save($user);
    }
  }
}
?>

Controller Class:

<?php 
$this
->User->cipherAll();
?>

Comments

  • Posted 06/13/09 02:06:42 PM
    The following line in method _decryptValue will remove part of the salt from the envelope if the salt begins or ends in zero:

       $envelope = trim($bf->decrypt($encrypted), chr(0)); 
    My fix goes at the end of method _generateSalt :

        # Do NOT Allow salt to begin or end with zero, problem with trim of envelope
        if( $len && !ord($salt[0]) )$salt[0]='J';
        if( $len > 1 && !ord($salt[$len-1]) )$salt[$len-1]='S';
        return $salt;
      }
    You would expect the salt to begin or end in zero 1 in 128 times.
  • Posted 12/02/08 04:25:57 PM
    What i must change, that this function will work also recursive?


    /** Model hook to decrypt model data if auto decipher is turned on in the
        * model behavior configuration. Only primary model data are decrypted. */
      function afterFind(&$model, $result, $primary = false) {
        if (!$result || !isset($this->config[$model->name]['cipher']))
          return $result;
        
        if ($primary && $this->config[$model->name]['autoDecrypt']) {
          // check for single of multiple model
          $keys = array_keys($result);
          if (!is_numeric($keys[0])) {
            $this->decrypt(&$model, &$result);
          } else {
            foreach($keys as $index) {
              $this->decrypt(&$model, &$result[$index]);
            }
          }
        }
        return $result;
      } 
  • Posted 10/28/08 08:26:38 AM
    This has some problems with PHP5. Is it possible for the author to fix this?

Comments are closed for articles over a year old