You must be logged in to view profiles.

Cipher Behavior

By Xemle (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.

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

Behavior Class:

Download code <?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:

Download code <?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:

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

In the controller you save your submitted data:

Controller Class:

Download code <?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:

Download code 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:

Download code <?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:

Download code <?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:

Download code <?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:

Download code <?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!

Download code  /**
  * 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:

Download code <?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:

Download code <?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:

Download code <?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:

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

Encrypt now all values with the new Security.salt.

Model Class:

Download code <?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:

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

 

Comments 775

CakePHP Team Comments Author Comments
 

Comment

1 Problems with PHP5

This has some problems with PHP5. Is it possible for the author to fix this?
Posted Oct 28, 2008 by Anders Holst
 

Comment

2 Recursive decrypt

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 Dec 2, 2008 by Andraž
 

Bug

3 Salt value which begins or ends in zero is trimmed.

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 Jun 13, 2009 by J Smithson