Subversion Repositories oidplus

Rev

Rev 1116 | Go to most recent revision | Blame | Last modification | View Log | RSS feed

  1. <?php
  2.  
  3. /*
  4.  * OIDplus 2.0
  5.  * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft
  6.  *
  7.  * Licensed under the Apache License, Version 2.0 (the "License");
  8.  * you may not use this file except in compliance with the License.
  9.  * You may obtain a copy of the License at
  10.  *
  11.  *     http://www.apache.org/licenses/LICENSE-2.0
  12.  *
  13.  * Unless required by applicable law or agreed to in writing, software
  14.  * distributed under the License is distributed on an "AS IS" BASIS,
  15.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16.  * See the License for the specific language governing permissions and
  17.  * limitations under the License.
  18.  */
  19.  
  20. namespace ViaThinkSoft\OIDplus;
  21.  
  22. // phpcs:disable PSR1.Files.SideEffects
  23. \defined('INSIDE_OIDPLUS') or die;
  24. // phpcs:enable PSR1.Files.SideEffects
  25.  
  26. class OIDplusSessionHandler extends OIDplusBaseClass implements OIDplusGetterSetterInterface {
  27.  
  28.         private $secret = '';
  29.         protected $sessionLifetime = 0;
  30.  
  31.         /**
  32.          * @throws OIDplusException
  33.          */
  34.         public function __construct() {
  35.                 $this->sessionLifetime = OIDplus::baseConfig()->getValue('SESSION_LIFETIME', 30*60);
  36.                 $this->secret = OIDplus::baseConfig()->getValue('SERVER_SECRET');
  37.  
  38.                 // **PREVENTING SESSION HIJACKING**
  39.                 // Prevents javascript XSS attacks aimed to steal the session ID
  40.                 @ini_set('session.cookie_httponly', '1');
  41.  
  42.                 // **PREVENTING SESSION FIXATION**
  43.                 // Session ID cannot be passed through URLs
  44.                 @ini_set('session.use_only_cookies', '1');
  45.  
  46.                 @ini_set('session.use_trans_sid', '0');
  47.  
  48.                 // Uses a secure connection (HTTPS) if possible
  49.                 @ini_set('session.cookie_secure', OIDplus::isSslAvailable() ? '1' : '0');
  50.  
  51.                 $path = OIDplus::webpath(null,OIDplus::PATH_RELATIVE);
  52.                 if (empty($path)) $path = '/';
  53.                 @ini_set('session.cookie_path', $path);
  54.  
  55.                 @ini_set('session.cookie_samesite', OIDplus::baseConfig()->getValue('COOKIE_SAMESITE_POLICY', 'Strict'));
  56.  
  57.                 @ini_set('session.use_strict_mode', '1');
  58.  
  59.                 @ini_set('session.gc_maxlifetime', $this->sessionLifetime);
  60.         }
  61.  
  62.         /**
  63.          * @return void
  64.          * @throws OIDplusException
  65.          */
  66.         protected function sessionSafeStart() {
  67.                 if (!isset($_SESSION)) {
  68.                         // TODO: session_name() makes some problems. Leave it away for now.
  69.                         //session_name('OIDplus_SESHDLR');
  70.                         if (!session_start()) {
  71.                                 throw new OIDplusException(_L('Session could not be started'));
  72.                         }
  73.                 }
  74.  
  75.                 if (!isset($_SESSION['ip'])) {
  76.                         if (!isset($_SERVER['REMOTE_ADDR'])) return;
  77.  
  78.                         // Remember the IP address of the user
  79.                         $_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
  80.                 } else {
  81.                         if ($_SERVER['REMOTE_ADDR'] != $_SESSION['ip']) {
  82.                                 // Was the session hijacked?! Get out of here!
  83.  
  84.                                 // We don't use $this->destroySession(), because this calls sessionSafeStart() again
  85.                                 $_SESSION = array();
  86.                                 session_destroy();
  87.                                 session_write_close();
  88.                                 OIDplus::cookieUtils()->unsetcookie(session_name()); // remove cookie, so GDPR people are happy
  89.                         }
  90.                 }
  91.         }
  92.  
  93.         /**
  94.          * @return void
  95.          */
  96.         function __destruct() {
  97.                 session_write_close();
  98.         }
  99.  
  100.         private $cacheSetValues = array(); // Important if you do a setValue() followed by an getValue()
  101.  
  102.         /**
  103.          * @param string $name
  104.          * @param mixed $value
  105.          * @return void
  106.          * @throws OIDplusException
  107.          */
  108.         public function setValue(string $name, $value) {
  109.                 $enc_data = self::encrypt($value, $this->secret);
  110.  
  111.                 $this->cacheSetValues[$name] = $enc_data;
  112.  
  113.                 $this->sessionSafeStart();
  114.                 OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime);
  115.  
  116.                 $_SESSION[$name] = $enc_data;
  117.         }
  118.  
  119.         /**
  120.          * @param string $name
  121.          * @param mixed|null $default
  122.          * @return mixed|null
  123.          * @throws OIDplusException
  124.          */
  125.         public function getValue(string $name, $default = NULL) {
  126.                 if (isset($this->cacheSetValues[$name])) return self::decrypt($this->cacheSetValues[$name], $this->secret);
  127.  
  128.                 if (!$this->isActive()) return $default; // GDPR: Only start a session when we really need one
  129.                 $this->sessionSafeStart();
  130.                 OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime);
  131.  
  132.                 if (!isset($_SESSION[$name])) return $default;
  133.                 return self::decrypt($_SESSION[$name], $this->secret);
  134.         }
  135.  
  136.         /**
  137.          * @param string $name
  138.          * @return bool
  139.          * @throws OIDplusException
  140.          */
  141.         public function exists(string $name): bool {
  142.                 if (isset($this->cacheSetValues[$name])) return true;
  143.  
  144.                 if (!$this->isActive()) return false; // GDPR: Only start a session when we really need one
  145.                 $this->sessionSafeStart();
  146.                 OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime);
  147.  
  148.                 return isset($_SESSION[$name]);
  149.         }
  150.  
  151.         /**
  152.          * @param string $name
  153.          * @return void
  154.          * @throws OIDplusException
  155.          */
  156.         public function delete(string $name) {
  157.                 if (isset($this->cacheSetValues[$name])) unset($this->cacheSetValues[$name]);
  158.  
  159.                 if (!$this->isActive()) return; // GDPR: Only start a session when we really need one
  160.                 $this->sessionSafeStart();
  161.                 OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime);
  162.  
  163.                 unset($_SESSION[$name]);
  164.         }
  165.  
  166.         /**
  167.          * @return void
  168.          * @throws OIDplusException
  169.          */
  170.         public function destroySession() {
  171.                 if (!$this->isActive()) return;
  172.  
  173.                 $this->sessionSafeStart();
  174.                 OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime);
  175.  
  176.                 $_SESSION = array();
  177.                 session_destroy();
  178.                 session_write_close();
  179.                 OIDplus::cookieUtils()->unsetcookie(session_name()); // remove cookie, so GDPR people are happy
  180.         }
  181.  
  182.         /**
  183.          * @return bool
  184.          */
  185.         public function isActive(): bool {
  186.                 return isset($_COOKIE[session_name()]);
  187.         }
  188.  
  189.         /**
  190.          * @param string $data
  191.          * @param string $key
  192.          * @return string
  193.          * @throws \Exception
  194.          */
  195.         protected static function encrypt(string $data, string $key): string {
  196.                 if (function_exists('openssl_encrypt')) {
  197.                         $iv = random_bytes(16); // AES block size in CBC mode
  198.                         // Encryption
  199.                         $ciphertext = openssl_encrypt(
  200.                                 $data,
  201.                                 'AES-256-CBC',
  202.                                 hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, true),
  203.                                 OPENSSL_RAW_DATA,
  204.                                 $iv
  205.                         );
  206.                         // Authentication
  207.                         $hmac = sha3_512_hmac($iv . $ciphertext, $key, true);
  208.                         return $hmac . $iv . $ciphertext;
  209.                 } else {
  210.                         // When OpenSSL is not available, then we just do a HMAC
  211.                         $hmac = sha3_512_hmac($data, $key, true);
  212.                         return $hmac . $data;
  213.                 }
  214.         }
  215.  
  216.         /**
  217.          * @param string $data
  218.          * @param string $key
  219.          * @return string
  220.          * @throws OIDplusException
  221.          */
  222.         protected static function decrypt(string $data, string $key): string {
  223.                 if (function_exists('openssl_decrypt')) {
  224.                         $hmac       = mb_substr($data, 0, 64, '8bit');
  225.                         $iv         = mb_substr($data, 64, 16, '8bit');
  226.                         $ciphertext = mb_substr($data, 80, null, '8bit');
  227.                         // Authentication
  228.                         $hmacNew = sha3_512_hmac($iv . $ciphertext, $key, true);
  229.                         if (!hash_equals($hmac, $hmacNew)) {
  230.                                 throw new OIDplusException(_L('Authentication failed'));
  231.                         }
  232.                         // Decryption
  233.                         $cleartext = openssl_decrypt(
  234.                                 $ciphertext,
  235.                                 'AES-256-CBC',
  236.                                 hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, true),
  237.                                 OPENSSL_RAW_DATA,
  238.                                 $iv
  239.                         );
  240.                         if ($cleartext === false) {
  241.                                 throw new OIDplusException(_L('Decryption failed'));
  242.                         }
  243.                         return $cleartext;
  244.                 } else {
  245.                         // When OpenSSL is not available, then we just do a HMAC
  246.                         $hmac       = mb_substr($data, 0, 64, '8bit');
  247.                         $cleartext  = mb_substr($data, 64, null, '8bit');
  248.                         $hmacNew    = sha3_512_hmac($cleartext, $key, true);
  249.                         if (!hash_equals($hmac, $hmacNew)) {
  250.                                 throw new OIDplusException(_L('Authentication failed'));
  251.                         }
  252.                         return $cleartext;
  253.                 }
  254.         }
  255. }
  256.