<?php
/**
* PHP versions 5
*
* phTagr : Tag, Browse, and Share Your Photos.
* Copyright 2006-2012, Sebastian Felis (hide@address.com)
*
* Licensed under The GPL-2.0 License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright 2006-2012, Sebastian Felis (hide@address.com)
* @link http://www.phtagr.org phTagr
* @package Phtagr
* @since phTagr 2.2b3
* @license GPL-2.0 (http://www.opensource.org/licenses/GPL-2.0)
*/
require_once("Crypt/Blowfish.php");
class CipherBehavior extends ModelBehavior
{
/** Default values of behavior.
@key Symetric 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 differend
outputs for the same input. Default is 4.
@padding Padding of ciphered value. Default is 4.
@autoDecrypt Decrypt ciphered value automatically. Default is false.
@noEncypt Disables encryption if true. Usefull for revert the encryption. */
var $default = array(
'cipher' => 'password',
'prefix' => '$E$',
'saltLen' => 4,
'padding' => 4,
'autoDecrypt' => false,
'noEncypt' => 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']) && $config['saltLen'] >= 2) {
$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]['noEncypt']) {
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) {
$this->log(print_r($data, true));
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 decrypt {$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(microtime(true)*1000);
$salt = '';
$lenKey = strlen($key);
$lenValue = strlen($value);
$old = rand(0, 255);
for($i = 0; $i < $len; $i++) {
$n = ord($key[$i % $lenKey]);
for ($j = 0; $j < $n; $j++) {
$toss = rand(0, 255);
}
$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 = 4) {
$l = strlen($value) + 2 * 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($envelope, 0, $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 -1, 1));
if ($pad > 32) {
$pad = 0;
}
$value = substr($envelope, $saltLen, $l - (2 * $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($base64Value, 0, $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;
}
}
?>