Subversion Repositories oidplus

Rev

Rev 1282 | 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.         /**
  29.          * @var string|null
  30.          */
  31.         private $secret = '';
  32.  
  33.         /**
  34.          * @var int|null
  35.          */
  36.         protected $sessionLifetime = 0;
  37.  
  38.         /**
  39.          * @throws OIDplusException
  40.          */
  41.         public function __construct() {
  42.                 $this->sessionLifetime = OIDplus::baseConfig()->getValue('SESSION_LIFETIME', 30*60);
  43.                 $this->secret = OIDplus::authUtils()->makeSecret(['b118abc8-f4ec-11ed-86ca-3c4a92df8582']);
  44.  
  45.                 // **PREVENTING SESSION HIJACKING**
  46.                 // Prevents javascript XSS attacks aimed to steal the session ID
  47.                 @ini_set('session.cookie_httponly', '1');
  48.  
  49.                 // **PREVENTING SESSION FIXATION**
  50.                 // Session ID cannot be passed through URLs
  51.                 @ini_set('session.use_only_cookies', '1');
  52.  
  53.                 @ini_set('session.use_trans_sid', '0');
  54.  
  55.                 // Uses a secure connection (HTTPS) if possible
  56.                 @ini_set('session.cookie_secure', OIDplus::isSslAvailable() ? '1' : '0');
  57.  
  58.                 $path = OIDplus::webpath(null,OIDplus::PATH_RELATIVE);
  59.                 if (empty($path)) $path = '/';
  60.                 @ini_set('session.cookie_path', $path);
  61.  
  62.                 @ini_set('session.cookie_samesite', OIDplus::baseConfig()->getValue('COOKIE_SAMESITE_POLICY', 'Strict'));
  63.  
  64.                 @ini_set('session.use_strict_mode', '1');
  65.  
  66.                 @ini_set('session.gc_maxlifetime', $this->sessionLifetime);
  67.         }
  68.  
  69.         /**
  70.          * @return void
  71.          * @throws OIDplusException
  72.          */
  73.         protected function sessionSafeStart() {
  74.                 if (!isset($_SESSION)) {
  75.                         // TODO: session_name() makes some problems. Leave it away for now.
  76.                         //session_name('OIDplus_SESHDLR');
  77.                         if (!session_start()) {
  78.                                 throw new OIDplusException(_L('Session could not be started'));
  79.                         }
  80.                 }
  81.  
  82.                 if (!isset($_SESSION['ip'])) {
  83.                         if (!isset($_SERVER['REMOTE_ADDR'])) return;
  84.  
  85.                         // Remember the IP address of the user
  86.                         $_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
  87.                 } else {
  88.                         if ($_SERVER['REMOTE_ADDR'] != $_SESSION['ip']) {
  89.                                 // Was the session hijacked?! Get out of here!
  90.  
  91.                                 // We don't use $this->destroySession(), because this calls sessionSafeStart() again
  92.                                 $_SESSION = array();
  93.                                 session_destroy();
  94.                                 session_write_close();
  95.                                 OIDplus::cookieUtils()->unsetcookie(session_name()); // remove cookie, so GDPR people are happy
  96.                         }
  97.                 }
  98.         }
  99.  
  100.         /**
  101.          * @return void
  102.          */
  103.         function __destruct() {
  104.                 session_write_close();
  105.         }
  106.  
  107.         private $cacheSetValues = array(); // Important if you do a setValue() followed by an getValue()
  108.  
  109.         /**
  110.          * @param string $name
  111.          * @param mixed $value
  112.          * @return void
  113.          * @throws OIDplusException
  114.          */
  115.         public function setValue(string $name, $value) {
  116.                 $enc_data = self::encrypt($value, $this->secret);
  117.  
  118.                 $this->cacheSetValues[$name] = $enc_data;
  119.  
  120.                 $this->sessionSafeStart();
  121.                 OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime);
  122.  
  123.                 $_SESSION[$name] = $enc_data;
  124.         }
  125.  
  126.         /**
  127.          * @param string $name
  128.          * @param mixed|null $default
  129.          * @return mixed|null
  130.          * @throws OIDplusException
  131.          */
  132.         public function getValue(string $name, $default = NULL) {
  133.                 if (isset($this->cacheSetValues[$name])) return self::decrypt($this->cacheSetValues[$name], $this->secret);
  134.  
  135.                 if (!$this->isActive()) return $default; // GDPR: Only start a session when we really need one
  136.                 $this->sessionSafeStart();
  137.                 OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime);
  138.  
  139.                 if (!isset($_SESSION[$name])) return $default;
  140.                 return self::decrypt($_SESSION[$name], $this->secret);
  141.         }
  142.  
  143.         /**
  144.          * @param string $name
  145.          * @return bool
  146.          * @throws OIDplusException
  147.          */
  148.         public function exists(string $name): bool {
  149.                 if (isset($this->cacheSetValues[$name])) return true;
  150.  
  151.                 if (!$this->isActive()) return false; // GDPR: Only start a session when we really need one
  152.                 $this->sessionSafeStart();
  153.                 OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime);
  154.  
  155.                 return isset($_SESSION[$name]);
  156.         }
  157.  
  158.         /**
  159.          * @param string $name
  160.          * @return void
  161.          * @throws OIDplusException
  162.          */
  163.         public function delete(string $name) {
  164.                 if (isset($this->cacheSetValues[$name])) unset($this->cacheSetValues[$name]);
  165.  
  166.                 if (!$this->isActive()) return; // GDPR: Only start a session when we really need one
  167.                 $this->sessionSafeStart();
  168.                 OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime);
  169.  
  170.                 unset($_SESSION[$name]);
  171.         }
  172.  
  173.         /**
  174.          * @return void
  175.          * @throws OIDplusException
  176.          */
  177.         public function destroySession() {
  178.                 if (!$this->isActive()) return;
  179.  
  180.                 $this->sessionSafeStart();
  181.                 OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime);
  182.  
  183.                 $_SESSION = array();
  184.                 session_destroy();
  185.                 session_write_close();
  186.                 OIDplus::cookieUtils()->unsetcookie(session_name()); // remove cookie, so GDPR people are happy
  187.         }
  188.  
  189.         /**
  190.          * @return bool
  191.          */
  192.         public function isActive(): bool {
  193.                 return isset($_COOKIE[session_name()]);
  194.         }
  195.  
  196.         /**
  197.          * @param string $data
  198.          * @param string $key
  199.          * @return string
  200.          * @throws \Exception
  201.          */
  202.         protected static function encrypt(string $data, string $key): string {
  203.                 if (function_exists('openssl_encrypt')) {
  204.                         $iv = random_bytes(16); // AES block size in CBC mode
  205.                         // Encryption
  206.                         $ciphertext = openssl_encrypt(
  207.                                 $data,
  208.                                 'AES-256-CBC',
  209.                                 hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, true),
  210.                                 OPENSSL_RAW_DATA,
  211.                                 $iv
  212.                         );
  213.                         // Authentication
  214.                         $hmac = sha3_512_hmac($iv . $ciphertext, $key, true);
  215.                         return $hmac . $iv . $ciphertext;
  216.                 } else {
  217.                         // When OpenSSL is not available, then we just do a HMAC
  218.                         $hmac = sha3_512_hmac($data, $key, true);
  219.                         return $hmac . $data;
  220.                 }
  221.         }
  222.  
  223.         /**
  224.          * @param string $data
  225.          * @param string $key
  226.          * @return string
  227.          * @throws OIDplusException
  228.          */
  229.         protected static function decrypt(string $data, string $key): string {
  230.                 if (function_exists('openssl_decrypt')) {
  231.                         $hmac       = mb_substr($data, 0, 64, '8bit');
  232.                         $iv         = mb_substr($data, 64, 16, '8bit');
  233.                         $ciphertext = mb_substr($data, 80, null, '8bit');
  234.                         // Authentication
  235.                         $hmacNew = sha3_512_hmac($iv . $ciphertext, $key, true);
  236.                         if (!hash_equals($hmac, $hmacNew)) {
  237.                                 throw new OIDplusException(_L('Authentication failed'));
  238.                         }
  239.                         // Decryption
  240.                         $cleartext = openssl_decrypt(
  241.                                 $ciphertext,
  242.                                 'AES-256-CBC',
  243.                                 hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, true),
  244.                                 OPENSSL_RAW_DATA,
  245.                                 $iv
  246.                         );
  247.                         if ($cleartext === false) {
  248.                                 throw new OIDplusException(_L('Decryption failed'));
  249.                         }
  250.                         return $cleartext;
  251.                 } else {
  252.                         // When OpenSSL is not available, then we just do a HMAC
  253.                         $hmac       = mb_substr($data, 0, 64, '8bit');
  254.                         $cleartext  = mb_substr($data, 64, null, '8bit');
  255.                         $hmacNew    = sha3_512_hmac($cleartext, $key, true);
  256.                         if (!hash_equals($hmac, $hmacNew)) {
  257.                                 throw new OIDplusException(_L('Authentication failed'));
  258.                         }
  259.                         return $cleartext;
  260.                 }
  261.         }
  262. }
  263.