From 238fc3e8cdc92ff48700b7049cdaf41ff458d494 Mon Sep 17 00:00:00 2001 From: Sebastian Kehr <kehr@ub.uni-leipzig.de> Date: Thu, 14 Jun 2018 16:37:54 +0200 Subject: [PATCH] Secure encrypted sessions (#1200) --- config/vufind/config.ini | 2 + .../src/VuFind/Session/AbstractBase.php | 16 +- .../src/VuFind/Session/HandlerInterface.php | 71 +++++++++ .../src/VuFind/Session/PluginManager.php | 18 ++- .../src/VuFind/Session/SecureDelegator.php | 146 ++++++++++++++++++ .../VuFind/Session/SecureDelegatorFactory.php | 104 +++++++++++++ .../VuFindTest/Session/PluginManagerTest.php | 2 +- 7 files changed, 349 insertions(+), 10 deletions(-) create mode 100644 module/VuFind/src/VuFind/Session/HandlerInterface.php create mode 100644 module/VuFind/src/VuFind/Session/SecureDelegator.php create mode 100644 module/VuFind/src/VuFind/Session/SecureDelegatorFactory.php diff --git a/config/vufind/config.ini b/config/vufind/config.ini index d4e9eb94104..0d5b2c16020 100644 --- a/config/vufind/config.ini +++ b/config/vufind/config.ini @@ -155,6 +155,8 @@ generator = "VuFind 4.1.3" [Session] type = File lifetime = 3600 ; Session lasts for 1 hour +; Should stored session data be encrypted? +secure = false ; Keep-alive interval in seconds. When set to a positive value, the session is kept ; alive with a JavaScript call as long as a VuFind page is open in the browser. ; Default is 0 (disabled). When keep-alive is enabled, session lifetime above can be diff --git a/module/VuFind/src/VuFind/Session/AbstractBase.php b/module/VuFind/src/VuFind/Session/AbstractBase.php index cf4654f544f..1e5c24847ec 100644 --- a/module/VuFind/src/VuFind/Session/AbstractBase.php +++ b/module/VuFind/src/VuFind/Session/AbstractBase.php @@ -4,7 +4,8 @@ * * PHP version 7 * - * Copyright (C) Villanova University 2010. + * Copyright (C) Villanova University 2010, + * Leipzig University Library <info@ub.uni-leipzig.de> 2018. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -22,12 +23,13 @@ * @category VuFind * @package Session_Handlers * @author Demian Katz <demian.katz@villanova.edu> + * @author Sebastian Kehr <kehr@ub.uni-leipzig.de> * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development:plugins:session_handlers Wiki */ namespace VuFind\Session; -use Zend\Session\SaveHandler\SaveHandlerInterface; +use Zend\Config\Config; /** * Base class for session handling @@ -35,11 +37,11 @@ use Zend\Session\SaveHandler\SaveHandlerInterface; * @category VuFind * @package Session_Handlers * @author Demian Katz <demian.katz@villanova.edu> + * @author Sebastian Kehr <kehr@ub.uni-leipzig.de> * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development:plugins:session_handlers Wiki */ -abstract class AbstractBase implements SaveHandlerInterface, - \VuFind\Db\Table\DbTableAwareInterface +abstract class AbstractBase implements HandlerInterface { use \VuFind\Db\Table\DbTableAwareTrait { getDbTable as getTable; @@ -55,7 +57,7 @@ abstract class AbstractBase implements SaveHandlerInterface, /** * Session configuration settings * - * @var \Zend\Config\Config + * @var Config */ protected $config = null; @@ -90,12 +92,12 @@ abstract class AbstractBase implements SaveHandlerInterface, /** * Set configuration. * - * @param \Zend\Config\Config $config Session configuration ([Session] section of + * @param Config $config Session configuration ([Session] section of * config.ini) * * @return void */ - public function setConfig($config) + public function setConfig(Config $config) { if (isset($config->lifetime)) { $this->lifetime = $config->lifetime; diff --git a/module/VuFind/src/VuFind/Session/HandlerInterface.php b/module/VuFind/src/VuFind/Session/HandlerInterface.php new file mode 100644 index 00000000000..bd69dc3452a --- /dev/null +++ b/module/VuFind/src/VuFind/Session/HandlerInterface.php @@ -0,0 +1,71 @@ +<?php +/** + * Session handler interface + * + * Copyright (C) Villanova University 2018, + * Leipzig University Library <info@ub.uni-leipzig.de> 2018. + * + * PHP version 7 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Session_Handlers + * @author Demian Katz <demian.katz@villanova.edu> + * @author Sebastian Kehr <kehr@ub.uni-leipzig.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:session_handlers Wiki + */ +namespace VuFind\Session; + +use VuFind\Db\Table\DbTableAwareInterface; +use Zend\Config\Config; +use Zend\Session\SaveHandler\SaveHandlerInterface; + +/** + * Session handler interface + * + * @category VuFind + * @package Session_Handlers + * @author Demian Katz <demian.katz@villanova.edu> + * @author Sebastian Kehr <kehr@ub.uni-leipzig.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:session_handlers Wiki + */ +interface HandlerInterface extends SaveHandlerInterface, DbTableAwareInterface +{ + /** + * Enable session writing (default) + * + * @return void + */ + public function enableWrites(); + + /** + * Disable session writing, i.e. make it read-only + * + * @return void + */ + public function disableWrites(); + + /** + * Set configuration. + * + * @param Config $config Session configuration ([Session] section of + * config.ini) + * + * @return void + */ + public function setConfig(Config $config); +} diff --git a/module/VuFind/src/VuFind/Session/PluginManager.php b/module/VuFind/src/VuFind/Session/PluginManager.php index 6579c49319e..61ad67cc59a 100644 --- a/module/VuFind/src/VuFind/Session/PluginManager.php +++ b/module/VuFind/src/VuFind/Session/PluginManager.php @@ -4,7 +4,8 @@ * * PHP version 7 * - * Copyright (C) Villanova University 2010. + * Copyright (C) Villanova University 2010, + * Leipzig University Library <info@ub.uni-leipzig.de> 2018. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -22,6 +23,7 @@ * @category VuFind * @package Session_Handlers * @author Demian Katz <demian.katz@villanova.edu> + * @author Sebastian Kehr <kehr@ub.uni-leipzig.de> * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development:plugins:session_handlers Wiki */ @@ -33,6 +35,7 @@ namespace VuFind\Session; * @category VuFind * @package Session_Handlers * @author Demian Katz <demian.katz@villanova.edu> + * @author Sebastian Kehr <kehr@ub.uni-leipzig.de> * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development:plugins:session_handlers Wiki */ @@ -64,6 +67,17 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager 'VuFind\Session\Memcache' => 'Zend\ServiceManager\Factory\InvokableFactory', ]; + /** + * Default delegator factories. + * + * @var string[][]|\Zend\ServiceManager\Factory\DelegatorFactoryInterface[][] + */ + protected $delegators = [ + 'VuFind\Session\Database' => ['VuFind\Session\SecureDelegatorFactory'], + 'VuFind\Session\File' => ['VuFind\Session\SecureDelegatorFactory'], + 'VuFind\Session\Memcache' => ['VuFind\Session\SecureDelegatorFactory'], + ]; + /** * Constructor * @@ -88,6 +102,6 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager */ protected function getExpectedInterface() { - return 'Zend\Session\SaveHandler\SaveHandlerInterface'; + return 'VuFind\Session\HandlerInterface'; } } diff --git a/module/VuFind/src/VuFind/Session/SecureDelegator.php b/module/VuFind/src/VuFind/Session/SecureDelegator.php new file mode 100644 index 00000000000..8425c42fba1 --- /dev/null +++ b/module/VuFind/src/VuFind/Session/SecureDelegator.php @@ -0,0 +1,146 @@ +<?php +/** + * Secure session delegator + * + * Copyright (C) Villanova University 2018, + * Leipzig University Library <info@ub.uni-leipzig.de> 2018. + * + * PHP version 7 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Session_Handlers + * @author Demian Katz <demian.katz@villanova.edu> + * @author Sebastian Kehr <kehr@ub.uni-leipzig.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:session_handlers Wiki + */ +namespace VuFind\Session; + +use VuFind\Cookie\CookieManager; +use Zend\Crypt\BlockCipher; +use Zend\Math\Rand; + +/** + * Secure session delegator + * + * @category VuFind + * @package Session_Handlers + * @author Demian Katz <demian.katz@villanova.edu> + * @author Sebastian Kehr <kehr@ub.uni-leipzig.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:session_handlers Wiki + */ +class SecureDelegator +{ + /** + * The block cipher for en/decrypting session data. + * + * @var BlockCipher + */ + protected $cipher; + + /** + * VuFind cookie manager service. + * + * @var CookieManager + */ + protected $cookieManager; + + /** + * The wrapped session handler. + * + * @var HandlerInterface + */ + protected $handler; + + /** + * SecureDelegator constructor. + * + * @param CookieManager $cookieManager {@see $cookieHandler} + * @param HandlerInterface $handler {@see $handler} + */ + public function __construct( + CookieManager $cookieManager, HandlerInterface $handler + ) { + $this->handler = $handler; + $this->cookieManager = $cookieManager; + $this->cipher = BlockCipher::factory('openssl'); + } + + /** + * Opens a session. + * + * @param string $save_path Session save path + * @param string $name Session name + * + * @return bool + */ + public function open($save_path, $name) + { + $cookieName = "{$name}_KEY"; + $cipherKey = ($cookieValue = $this->cookieManager->get($cookieName)) + ?? base64_encode(Rand::getBytes(64)); + + if (!$cookieValue) { + $lifetime = session_get_cookie_params()['lifetime']; + $expire = $lifetime ? $lifetime + time() : 0; + $this->cookieManager->set($cookieName, $cipherKey, $expire); + } + + $this->cipher->setKey(base64_decode($cipherKey)); + return $this->handler->open($save_path, $name); + } + + /** + * Read a sessions data. + * + * @param string $session_id Session id + * + * @return bool|string + */ + public function read($session_id) + { + $data = $this->handler->read($session_id); + return $data ? $this->cipher->decrypt($data) : $data; + } + + /** + * Writes session data. + * + * @param string $session_id Session id + * @param string $session_data Session data + * + * @return bool + */ + public function write($session_id, $session_data) + { + $data = $this->cipher->encrypt($session_data); + return $this->handler->write($session_id, $data); + } + + /** + * Pass calls to non-existing methods to the wrapped Handler + * + * @param string $name Name of the method being called + * @param array $arguments Passed Arguments + * + * @return mixed + */ + public function __call($name, $arguments) + { + return $this->handler->{$name}(...$arguments); + } +} diff --git a/module/VuFind/src/VuFind/Session/SecureDelegatorFactory.php b/module/VuFind/src/VuFind/Session/SecureDelegatorFactory.php new file mode 100644 index 00000000000..2f5222749f7 --- /dev/null +++ b/module/VuFind/src/VuFind/Session/SecureDelegatorFactory.php @@ -0,0 +1,104 @@ +<?php +/** + * Secure session delegator factory + * + * Copyright (C) Villanova University 2018, + * Leipzig University Library <info@ub.uni-leipzig.de> 2018. + * + * PHP version 7 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Session_Handlers + * @author Demian Katz <demian.katz@villanova.edu> + * @author Sebastian Kehr <kehr@ub.uni-leipzig.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:session_handlers Wiki + */ +namespace VuFind\Session; + +use Interop\Container\ContainerInterface; +use ProxyManager\Factory\LazyLoadingValueHolderFactory; +use Zend\ServiceManager\Factory\DelegatorFactoryInterface; + +/** + * Secure session delegator factory + * + * @category VuFind + * @package Session_Handlers + * @author Demian Katz <demian.katz@villanova.edu> + * @author Sebastian Kehr <kehr@ub.uni-leipzig.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:session_handlers Wiki + */ +class SecureDelegatorFactory implements DelegatorFactoryInterface +{ + /** + * Invokes this factory. + * + * @param ContainerInterface $container Service container + * @param string $name Service name + * @param callable $callback Service callback + * @param array|null $options Service options + * + * @return SecureDelegator + */ + public function __invoke( + ContainerInterface $container, $name, callable $callback, + array $options = null + ): HandlerInterface { + /** + * The wrapped session handler. + * + * @var HandlerInterface $handler + */ + $handler = call_user_func($callback); + $config = $container->get('VuFind\Config\PluginManager'); + $secure = $config->get('config')->Session->secure ?? false; + return $secure ? $this->delegate($container, $handler) : $handler; + } + + /** + * Creates the delegating session handler + * + * @param ContainerInterface $container Service Container + * @param HandlerInterface $handler Wrapped session handler + * + * @return HandlerInterface + */ + protected function delegate( + ContainerInterface $container, HandlerInterface $handler + ): HandlerInterface { + $cookieManager = $container->get('VuFind\Cookie\CookieManager'); + $config = $container->get('ProxyManager\Configuration'); + $factory = new LazyLoadingValueHolderFactory($config); + $delegator = new SecureDelegator($cookieManager, $handler); + /** + * The handler proxy. + * + * @var HandlerInterface $handler + */ + $handler = $factory->createProxy( + HandlerInterface::class, function ( + &$target, $proxy, $method, array $params, &$init + ) use ($delegator) { + $init = null; + $target = $delegator; + return true; + } + ); + return $handler; + } +} diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Session/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Session/PluginManagerTest.php index 62e31a9846a..e6633f6b39b 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Session/PluginManagerTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Session/PluginManagerTest.php @@ -59,7 +59,7 @@ class PluginManagerTest extends \VuFindTest\Unit\TestCase * @return void * * @expectedException Zend\ServiceManager\Exception\InvalidServiceException - * @expectedExceptionMessage Plugin ArrayObject does not belong to Zend\Session\SaveHandler\SaveHandlerInterface + * @expectedExceptionMessage Plugin ArrayObject does not belong to VuFind\Session\HandlerInterface */ public function testExpectedInterface() { -- GitLab