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