Permalink
Browse files

Initial version of the cookie encryption strategy.

  • Loading branch information...
1 parent edaae76 commit 8f20cb5262e680482cead1637779b43a81b7d734 @daschl daschl committed Sep 12, 2011
Showing with 299 additions and 0 deletions.
  1. +200 −0 storage/session/strategy/Encrypt.php
  2. +99 −0 tests/cases/storage/session/strategy/EncryptTest.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);
+ }
+}
@@ -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
Contributor

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
Member

@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
Member

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
Contributor

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

Please sign in to comment.