Skip to content

Commit

Permalink
Initial version of the cookie encryption strategy.
Browse files Browse the repository at this point in the history
  • Loading branch information
daschl committed Sep 12, 2011
1 parent edaae76 commit 8f20cb5
Show file tree
Hide file tree
Showing 2 changed files with 299 additions and 0 deletions.
200 changes: 200 additions & 0 deletions storage/session/strategy/Encrypt.php
@@ -0,0 +1,200 @@
<?php
/**
* Lithium: the most rad php framework
*
* @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/

namespace lithium\storage\session\strategy;

use lithium\core\ConfigException;

/**
* This strategy allows you to encrypt your `Session` and / or `Cookie` data so that it
* is not stored in cleartext on the client side.
*
* Example configuration:
*
* {{{
* Session::config(array('default' => array(
* 'adapter' => 'Cookie',
* 'strategies' => array('Encrypt' => array('secret' => 'foobar'))
* )));
* }}}
*
* By default, this strategy uses the AES algorithm in the CBC mode. You can override this
* defaults by passing a different `cipher` and/or `mode` to the config like this:
*
* {{{
* Session::config(array('default' => array(
* 'adapter' => 'Cookie',
* 'strategies' => array('Encrypt' => array(
* 'cipher' => MCRYPT_RIJNDAEL_128,
* 'mode' => MCRYPT_MODE_ECB,
* 'secret' => 'foobar'
* ))
* )));
* }}}
*
* @link http://www.php.net/manual/en/mcrypt.ciphers.php List of supported ciphers.
* @link http://www.php.net/manual/en/mcrypt.constants.php List of supported modes.
*/
class Encrypt extends \lithium\core\Object {

/**
* Holds the initialization vector.
*/
protected static $_vector = null;

/**
* Constructor.
*
* @param array $config Configuration array. You can override the default cipher and mode.
*/
public function __construct(array $config = array()) {
if (!isset($config['secret'])) {
throw new ConfigException("Encrypt strategy requires a secret key.");
}
$defaults = array(
'cipher' => MCRYPT_RIJNDAEL_256,
'mode' => MCRYPT_MODE_CBC
);
parent::__construct($config + $defaults);
$this->_config['vector'] = static::_vector($this->_config['cipher'], $this->_config['mode']);
}

/**
* Read encryption method.
*
* @param
* @param
* @return
*/
public function read($data, array $options = array()) {
$class = $options['class'];

$encrypted = $class::read(null, array('strategies' => false));

if (!isset($encrypted['__encrypted']) || !$encrypted['__encrypted']) {
return isset($encrypted[$data]) ? $encrypted[$data] : null;
}

$current = $this->_decrypt($encrypted['__encrypted']);

if($data) {
return isset($current[$data]) ? $current[$data] : null;
} else {
return $current;
}
}

/**
* Write encryption method.
*
* @param
* @param
* @return
*/
public function write($data, array $options = array()) {
$class = $options['class'];

$futureData = $this->read(null, $options) ?: array();
$futureData = array($options['key'] => $data) + $futureData;

$payload = empty($futureData) ? null : $this->_encrypt($futureData);

$class::write('__encrypted', $payload, array('strategies' => false) + $options);
return $data;
}

/**
* Delete encryption method.
*
* @param
* @param
* @return
*/
public function delete($data, array $options = array()) {
$class = $options['class'];

$futureData = $this->read(null, $options) ?: array();
unset($futureData[$options['key']]);

$payload = empty($futureData) ? null : $this->_encrypt($futureData);

$class::write('__encrypted', $payload, array('strategies' => false) + $options);
return $data;
}

/**
* Determines if the Mcrypt extension has been installed.
*
* @return boolean `true` if enabled, `false` otherwise
*/
public static function enabled() {
return extension_loaded('mcrypt');
}

/**
* Serialize and encrypt a given data array.
*
* @param
* @return
*/
protected function _encrypt($decrypted = array()) {
extract($this->_config);

$encrypted = mcrypt_encrypt($cipher, $secret, serialize($decrypted), $mode, $vector);
$data = base64_encode($encrypted) . base64_encode($vector);

return $data;
}

/**
* Decrypt and unserialize a previously encrypted string.
*
* @param
* @return
*/
protected function _decrypt($encrypted) {
extract($this->_config);

$vectorSize = strlen(base64_encode(str_repeat(" ", static::_vectorSize($cipher, $mode))));
$vector = base64_decode(substr($encrypted, -$vectorSize));
$data = base64_decode(substr($encrypted, 0, -$vectorSize));

$decrypted = mcrypt_decrypt($cipher, $secret, $data, $mode, $vector);
$data = unserialize(trim($decrypted));

return $data;
}

/**
* Generates an initialization vector.
*
* @param
* @param
* @return string Returns an initialization vector.
* @link http://www.php.net/manual/en/function.mcrypt-create-iv.php
*/
protected static function _vector($cipher, $mode) {
if(static::$_vector) {
return static::$_vector;
}

$size = static::_vectorSize($cipher, $mode);
return static::$_vector = mcrypt_create_iv($size, MCRYPT_DEV_URANDOM);
}

/**
* Returns the vector size vor a given cipher and mode.
*
* @param
* @param
* @return
*/
protected static function _vectorSize($cipher, $mode) {
return mcrypt_get_iv_size($cipher, $mode);
}
}
99 changes: 99 additions & 0 deletions tests/cases/storage/session/strategy/EncryptTest.php
@@ -0,0 +1,99 @@
<?php
/**
* Lithium: the most rad php framework
*
* @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/

namespace lithium\tests\cases\storage\session\strategy;

use lithium\storage\session\strategy\Encrypt;
use lithium\tests\mocks\storage\session\strategy\MockCookieSession;

class EncryptTest extends \lithium\test\Unit {

public $secret = 'foobar';

/**
* Skip the test if Mcrypt extension is unavailable.
*
* @return void
*/
public function skip() {
$this->skipIf(!Encrypt::enabled(), 'The Mcrypt extension is not installed or enabled.');
}

public function setUp() {
$this->mock = 'lithium\tests\mocks\storage\session\strategy\MockCookieSession';
MockCookieSession::reset();
}

public function testConstructException() {
$this->expectException('/Encrypt strategy requires a secret key./');
$encrypt = new Encrypt();
}

public function testEnabled() {
$this->assertTrue(Encrypt::enabled());
}

public function testConstruct() {
$encrypt = new Encrypt(array('secret' => $this->secret));
$this->assertTrue($encrypt instanceof Encrypt);
}

public function testWrite() {
$encrypt = new Encrypt(array('secret' => $this->secret));

$key = 'fookey';
$value = 'barvalue';

$result = $encrypt->write($value, array('class' => $this->mock, 'key' => $key));
$cookie = MockCookieSession::data();

$this->assertTrue($result);
$this->assertTrue($cookie['__encrypted']);
$this->assertTrue(is_string($cookie['__encrypted']));
$this->assertNotEqual($cookie['__encrypted'], $value);
}

public function testRead() {
$encrypt = new Encrypt(array('secret' => $this->secret));

$key = 'fookey';
$value = 'barvalue';

$result = $encrypt->write($value, array('class' => $this->mock, 'key' => $key));
$this->assertTrue($result);

$cookie = MockCookieSession::data();
$result = $encrypt->read($key, array('class' => $this->mock));

$this->assertEqual($value, $result);
$this->assertNotEqual($cookie['__encrypted'], $result);
}

public function testDelete() {
$encrypt = new Encrypt(array('secret' => $this->secret));

$key = 'fookey';
$value = 'barvalue';

$result = $encrypt->write($value, array('class' => $this->mock, 'key' => $key));
$this->assertTrue($result);

$cookie = MockCookieSession::data();
$result = $encrypt->read($key, array('class' => $this->mock));

$this->assertEqual($value, $result);

$result = $encrypt->delete($key, array('class' => $this->mock, 'key' => $key));

$cookie = MockCookieSession::data();
$this->assertTrue(empty($cookie['__encrypted']));

$result = $encrypt->read($key, array('class' => $this->mock));
$this->assertFalse($result);
}
}

4 comments on commit 8f20cb5

@kamui545
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi @daschl, great work ! It looks awesome.
Do you think we can re-use this Encrypt class to encrypt/decrypt data like an email field stored in a database ?

@daschl
Copy link
Contributor Author

@daschl daschl commented on 8f20cb5 Sep 12, 2011

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kamul545 Thanks!

Actually we thought about doing refactoring it into a more general class later if there's the need for it. As it looks like it is, so let's get this class on track first and then open an enhancement request that discusses this - ok?

Regards

@jperras
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work. Looks good to me, on a cursory inspection.

I wouldn't worry too much about refactoring to be more general - that's something that can be done at a later date without BC break, if done correctly.

@kamui545
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@daschl Of course no problem, that was not a request, just a simple question by curiosity :P

Please sign in to comment.