Encrypted Cookie Component

by jreeves
A Cookie component for Cake PHP 1.1 (which should also work in 1.2 as well). The aim of this component was to make saved data as secure as possible. I have (hopefully) achieved this by utilizing mcrypt (for encrypting and decrypting the data) and checksuming the stored data. Update: SVN build 385 Cookie's now get stored with their expiration timestamp. When __unpacking() we can check to see if the user has tampered with the cookies expiration time and kill the cookie if they have. Update: SVN build 387 Fixed constructor and tidied up comments, please note that the use of __construct() relies upon PHP5, but this could easily be adapted for PHP4 if required.

Component Class:

<?php 
/**
 * Cookie.php
 * John Reeves
 * November 2007
 * SVN build 387
 *
 * Cookie Component for CakePHP 1.1
*/

class CookieComponent extends Object
{
    
/**
     * @var int $default_cookie_life
     * Default time that a cookie will last for if not specified (604800 = 1 week)
    */
    
var $default_cookie_life 604800;        // one week

    /**
     * @var boolean $use_mcrypt
     * Encrypt cookie data using the mcrypt extension, MCrypt will use the
     * CAKE_SESSION_STRING constant as the key.
    */
    
var $use_mcrypt true;    

    
    
/**
     * Constructor.
     * Check that mCrypt is present, if not disable it.
     *
     *    @return Void.
    */
    
function __construct()
    {
        
parent::__construct();
        
        
// Over-ride mcrypt choice if module not avaliable.
        
if ($this->use_mcrypt && !extension_loaded('mcrypt')) {
            
$this->log(__METHOD__." could not load the mcrypt module.");
            
$this->use_mcrypt false;
        }        
    }
    
    
    
/**
     * Gets called immediatley after beforeFilter() by the Controller.
     * empty.
     *
     *    @param object $controller        Reference to Controller.
     *    @return Void
    */
    
function startup(&$controller)
    {
    }
    
    
    
/** 
     * Returns the variable $key from the cookie specified by $cookie.  If
     * you do not specify $key, the whole array will be returned.
     *
     *    @param string $cookie
     *    @param string $key
     *    @return mixed                false on failure.  Array / String on success.
    */
    
function read($cookie$key null)
    {
        if (!
$this->valid($cookie))
            return 
false;
        
        
// Read in the cookie from $_COOKIE
        
$cookie $_COOKIE[$cookie];    
                    
        
// Unpack the stored cookie, error if unpacking fails.
        
if (!$cookie_array $this->__unpackCookie($cookie)) {
            return 
false;
        }
        
        if (!
is_array($cookie_array)) {
            
$error __METHOD__." cookie: {$cookie} was not a serialized array.";
            
$this->log($error);
            
trigger_error($error);
        }
            
        
// Extract key.
        
if ($key) {
            if (!isset(
$cookie_array[$key]))
                return 
false;
                
            return 
$cookie_array[$key];
        }
        
        
// Return whole unserialised array.
        
else {
            return 
$cookie_array;
        }
    }
    
    
    
/**
     * Writes the array specified by $data to the cookie specified by $cookie
     * which will expire after $expires (Which can either be time as a string
     * or in seconds).
     *
     *    @param string $cookie
     *    @param array $data
     *    @param string $expires
     *    @return boolean
    */
    
function write($cookie$data$expires null)
    {
        if (!
is_array($data)) {
            
$error __METHOD__ " expects an array.";
            
$this->log($error);
            
trigger_error($error);
        }
        
        
$expires $this->__getCookieExpires($expires);
        
$contents $this->__packCookie($data$expires);
        
        if (!
setcookie($cookie$contents$expires'/')) {
            
$this->log(__METHOD__." failed to write cookie: {$cookie}");
            return 
false;
        }
        
        return 
true;
    }
    
    
    
/**
     * Deletes the cookie specified by $cookie
     *
     *    @param string $cookie
     *    @return boolean
    */
    
function delete($cookie)
    {
        if (!
$this->valid($cookie))
            return 
false;
        
        if (!
setcookie($cookie''time()-1'/')) {
            
$this->log(__METHOD__." failed to delete cookie {$cookie}");
            return 
false;
        }
        
        return 
true;
    }
    
    
    
/**
     * Delete the cookie specified by $cookie
     *
     *    @param string $cookie
     *    @return boolean
    */
    
function del($cookie)
    {
        return 
$this->delete($cookie);
    }
    
    
    
/**
     * Returns true if the cookie specified by $cookie has been set.
     *
     *    @param string $cookie
     *    @return boolean
    */
    
function valid($cookie)
    {
        return (isset(
$_COOKIE[$cookie]));
    }
    
    
    
/**
     * Private function to get the exiration value when setting a cookie.
     * $value can either be expressed as the number of seconds into the future,
     * or as a string that will be parsed via strtotime.
     *
     *    @param mixed $value
     *    @return mixed            unix_timestamp on success, false on failure.
    */
    
function __getCookieExpires($value null)
    {
        if (!
$value)
            
$expires time() + $this->default_cookie_life;
            
        else if (
is_numeric($value)) {
            
$expires time() + $value;
        }
        
        else {
            
$time strtotime($value);
            if (
$time != false && is_numeric($time) && $time time()) {
                
$expires $time;
            }
            
            else {
                
$this->log(__METHOD__ " failed to parse expires val: {$value}"LOG_DEBUG);
                return 
false;
            }
        }
        
        return 
$expires;        
    }
    
    
    
/**
     * Encodes a mixed value specified by $data into a string value which can
     * be stored safely in a cookie.  This function also adds checksumming, 
     * expiration date validtion (as specified by $expires) and, if mcrypt is
     * enabled, encryption.  Cookie's packed with __packCookie() can be unpacked
     * by calling __unpackCookie() on them.
     *
     *    @param mixed $data        Data to store in the cookie (will be serialized)
     *    @param int $expires        Date (unix timestamp) that the cookie is set to expire.
     *    @return string            base64 encoded, seralized data safe to store in a cookie.
    */
    
function __packCookie($data$expires)
    {
        if (!
is_numeric($expires) || $expires time()) {
            
trigger_error(__METHOD__.' $expires must be a valid timestamp not in the past.');
        }
        
        
$serialized         serialize(array($data$expires));
        
$checksum             md5($serialized CAKE_SESSION_STRING);
        
$cookie_data        serialize(array($serialized$checksum));
        
        if (
$this->use_mcrypt) {
            
$cookie_data $this->__encrypt($cookie_data);
        }
        
        
// base64 encode for safe storage in US-ASCII Cookie format
        
return base64_encode($cookie_data);
    }
    
    
    
/**
     * Unpacks the value of a cookie specified by $cookie into an array.  If
     * the cookie data is not stored as expected it will return false.
     *
     *    @param string $cookie    Serialised cookie data.
     *    @return array            array($data, $checksum);
     *
    */
    
function __unpackCookie($cookie)
    {        
        
// base64 decode.
        
$data base64_decode($cookie);

        
// decrypt the cookie
        
if ($this->use_mcrypt) {
            
$data $this->__decrypt($data);
        }
                
        
$checksum_array unserialize($data);
        
        
// $checksum_array[0] (the data) should match the hash we stored in 
        // $checksum_array[1].
        
if (md5($checksum_array[0] . CAKE_SESSION_STRING) !== $checksum_array[1]) {
            
$msg __METHOD__." cookie failed checksum, possible tampering.";
            
$this->log($msg);
            
debug($msg);
            return 
false;
        }
        
        
// Unserialise the checksumed data, now verify the timestamp.
        
$expires_array unserialize($checksum_array[0]);
        
        if (!
is_numeric($expires_array[1]) || $expires_array[1] < time()) {
            
$msg __METHOD__." cookie should have expired, possible tampering.";
            
$this->log($msg);
            
debug($msg);
            return 
false;
        }
        
        return 
$expires_array[0];
    }
    
    
    
/**
     * Encrypt's a plain text string value specified by $plain_text into an 
     * encrypted string using the mcrypt encryption method specified by 
     * $method.
     *
     *    @param string $plain_text
     *    @param string $method
     *    @return string
     *    @trigger_error                 on missing MCrypt Module.
    */
    
function __encrypt($plain_text$method='rijndael-256')
    {
        
// Check the method requested exists
        
if (!in_array($methodmcrypt_list_algorithms())) {
            
trigger_error(__METHOD__." requested encryption method {$method} is"
                
" not supported by your MCrypt library.");
        }
        
        
$td mcrypt_module_open($method'''ecb''');
        
$iv mcrypt_create_iv(mcrypt_enc_get_iv_size($td), MCRYPT_RAND);
        
$key_length mcrypt_enc_get_key_size($td);
        
        
// Chop the key to the correct size.
        
$key substr(md5(CAKE_SESSION_STRING), 0$key_length);
        
        
mcrypt_generic_init($td$key$iv);
        
$encrypted mcrypt_generic($td$plain_text);
        
        
// Tear down.
        
mcrypt_generic_deinit($td);
        
mcrypt_module_close($td);
        
        return 
$encrypted;
    }
    
    
    
/**
     * Decryptes an encrypted string specified by $encrypted using the mcrypt
     * module specified by $method
     *
     *    @param string $encrypted    
     *    @param string $method
     *    @return string                    Decrypted plain text.
    */
    
function __decrypt($encrypted$method='rijndael-256')
    {
        
$td mcrypt_module_open($method'''ecb''');
        
$iv mcrypt_create_iv(mcrypt_enc_get_iv_size($td), MCRYPT_RAND);
        
$key_length mcrypt_enc_get_key_size($td);
        
        
// Chop the key to the correct size.
        
$key substr(md5(CAKE_SESSION_STRING), 0$key_length);
        
        
mcrypt_generic_init($td$key$iv);
        return 
rtrim(mdecrypt_generic($td$encrypted));
    }
}
?>

Report

More on Components

Advertising

Comments

  • kalpesh.gamit posted on 11/14/08 12:28:58 AM
    Nice Post
  • sir_epal posted on 06/16/08 11:02:38 PM
    Hi....

    Here's my problem... I declared my cookie [$this->Cookie->write('hello', 'HELLO HELLO')] in function index. When i'm trying to read it [$this->Cookie->read('hello')] in function page2, it's value is null....
    Question, is it possible to read the cookie when you declared it in other object?
login to post a comment.