Subversion Repositories oidplus

Rev

Rev 1300 | 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 OIDplusAuthContentStoreSession extends OIDplusAuthContentStore {
  27.  
  28.         // Override abstract functions
  29.  
  30.         /**
  31.          * @param string $name
  32.          * @param mixed|null $default
  33.          * @return mixed|null
  34.          * @throws OIDplusException
  35.          */
  36.         public function getValue(string $name, $default = NULL) {
  37.                 try {
  38.                         if (isset($this->cacheSetValues[$name])) return self::decrypt($this->cacheSetValues[$name], $this->secret);
  39.  
  40.                         if (!$this->isActive()) return $default; // GDPR: Only start a session when we really need one
  41.                         $this->sessionSafeStart();
  42.                         OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime);
  43.  
  44.                         if (!isset($_SESSION[$name])) return $default;
  45.                         return self::decrypt($_SESSION[$name], $this->secret);
  46.                 } catch (\Exception $e) {
  47.                         $this->destroySession();
  48.                         // TODO: For some reason If destroySession() is called, we won't get this Exception?!
  49.                         throw new OIDplusException(_L('Internal error with session. Please reload the page and log-in again. %1', $e->getMessage()));
  50.                 }
  51.         }
  52.  
  53.         /**
  54.          * @param string $name
  55.          * @param mixed $value
  56.          * @return void
  57.          * @throws OIDplusException
  58.          */
  59.         public function setValue(string $name, $value) {
  60.                 $enc_data = self::encrypt($value, $this->secret);
  61.  
  62.                 $this->cacheSetValues[$name] = $enc_data;
  63.  
  64.                 $this->sessionSafeStart();
  65.                 OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime);
  66.  
  67.                 $_SESSION[$name] = $enc_data;
  68.         }
  69.  
  70.         /**
  71.          * @param string $name
  72.          * @return bool
  73.          * @throws OIDplusException
  74.          */
  75.         public function exists(string $name): bool {
  76.                 if (isset($this->cacheSetValues[$name])) return true;
  77.  
  78.                 if (!$this->isActive()) return false; // GDPR: Only start a session when we really need one
  79.                 $this->sessionSafeStart();
  80.                 OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime);
  81.  
  82.                 return isset($_SESSION[$name]);
  83.         }
  84.  
  85.         /**
  86.          * @param string $name
  87.          * @return void
  88.          * @throws OIDplusException
  89.          */
  90.         public function delete(string $name) {
  91.                 if (isset($this->cacheSetValues[$name])) unset($this->cacheSetValues[$name]);
  92.  
  93.                 if (!$this->isActive()) return; // GDPR: Only start a session when we really need one
  94.                 $this->sessionSafeStart();
  95.                 OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime);
  96.  
  97.                 unset($_SESSION[$name]);
  98.         }
  99.  
  100.         /**
  101.          * @return void
  102.          * @throws OIDplusException
  103.          */
  104.         public function destroySession() {
  105.                 if (!$this->isActive()) return;
  106.  
  107.                 $this->sessionSafeStart();
  108.                 OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime);
  109.  
  110.                 $_SESSION = array();
  111.                 session_destroy();
  112.                 session_write_close();
  113.                 OIDplus::cookieUtils()->unsetcookie(session_name()); // remove cookie, so GDPR people are happy
  114.         }
  115.  
  116.         /**
  117.          * @return OIDplusAuthContentStoreSession|null
  118.          * @throws OIDplusException
  119.          */
  120.         public static function getActiveProvider()/*: ?OIDplusAuthContentStore*/ {
  121.                 static $contentProvider = null;
  122.  
  123.                 $rel_url = substr($_SERVER['REQUEST_URI'], strlen(OIDplus::webpath(null, OIDplus::PATH_RELATIVE_TO_ROOT)));
  124.                 if (str_starts_with($rel_url, 'rest/')) { // <== TODO: Find a way how to move this into the plugin, since REST does not belong to the core. (Maybe some kind of "stateless mode" that is enabled by the REST plugin)
  125.                         // For REST, we must only allow JWT from Bearer and nothing else! So disable cookies if we are accessing the REST plugin
  126.                         return null;
  127.                 }
  128.  
  129.                 if (!$contentProvider) {
  130.                         if (self::isActive()) {
  131.                                 $contentProvider = new OIDplusAuthContentStoreSession();
  132.                         }
  133.                 }
  134.  
  135.                 return $contentProvider;
  136.         }
  137.  
  138.         /**
  139.          * @param string $email
  140.          * @param string $loginfo
  141.          * @return void
  142.          * @throws OIDplusException
  143.          */
  144.         public function raLoginEx(string $email, string &$loginfo) {
  145.                 $this->raLogin($email);
  146.                 if (is_null(self::getActiveProvider())) {
  147.                         $loginfo = 'into new PHP session';
  148.                 } else {
  149.                         $loginfo = 'into existing PHP session';
  150.                 }
  151.         }
  152.  
  153.         /**
  154.          * @param string $loginfo
  155.          * @return void
  156.          * @throws OIDplusException
  157.          */
  158.         public function adminLoginEx(string &$loginfo) {
  159.                 $this->adminLogin();
  160.                 if (is_null(self::getActiveProvider())) {
  161.                         $loginfo = 'into new PHP session';
  162.                 } else {
  163.                         $loginfo = 'into existing PHP session';
  164.                 }
  165.         }
  166.  
  167.         /**
  168.          * @param string $email
  169.          * @param string $loginfo
  170.          * @return void
  171.          */
  172.         public function raLogoutEx(string $email, string &$loginfo) {
  173.                 $this->raLogout($email);
  174.                 $loginfo = 'from PHP session';
  175.         }
  176.  
  177.         /**
  178.          * @param string $loginfo
  179.          * @return void
  180.          */
  181.         public function adminLogoutEx(string &$loginfo) {
  182.                 $this->adminLogout();
  183.                 $loginfo = 'from PHP session';
  184.         }
  185.  
  186.         /**
  187.          * @return void
  188.          */
  189.         public function activate() {
  190.                 # Sessions automatically activate during setValue()
  191.         }
  192.  
  193.         # ------------------------------------------------------------------------------------------------------------------
  194.  
  195.         /**
  196.          * @var string|null
  197.          */
  198.         private $secret = '';
  199.  
  200.         /**
  201.          * @var int|null
  202.          */
  203.         protected $sessionLifetime = 0;
  204.  
  205.         /**
  206.          * @throws OIDplusException
  207.          */
  208.         public function __construct() {
  209.                 $this->sessionLifetime = OIDplus::baseConfig()->getValue('SESSION_LIFETIME', 30*60);
  210.                 $this->secret = OIDplus::authUtils()->makeSecret(['b118abc8-f4ec-11ed-86ca-3c4a92df8582']);
  211.  
  212.                 // **PREVENTING SESSION HIJACKING**
  213.                 // Prevents javascript XSS attacks aimed to steal the session ID
  214.                 @ini_set('session.cookie_httponly', '1');
  215.  
  216.                 // **PREVENTING SESSION FIXATION**
  217.                 // Session ID cannot be passed through URLs
  218.                 @ini_set('session.use_only_cookies', '1');
  219.  
  220.                 @ini_set('session.use_trans_sid', '0');
  221.  
  222.                 // Uses a secure connection (HTTPS) if possible
  223.                 @ini_set('session.cookie_secure', OIDplus::isSslAvailable() ? '1' : '0');
  224.  
  225.                 $path = OIDplus::webpath(null,OIDplus::PATH_RELATIVE);
  226.                 if (empty($path)) $path = '/';
  227.                 @ini_set('session.cookie_path', $path);
  228.  
  229.                 @ini_set('session.cookie_samesite', OIDplus::baseConfig()->getValue('COOKIE_SAMESITE_POLICY', 'Strict'));
  230.  
  231.                 @ini_set('session.use_strict_mode', '1');
  232.  
  233.                 @ini_set('session.gc_maxlifetime', $this->sessionLifetime);
  234.         }
  235.  
  236.         /**
  237.          * @return void
  238.          * @throws OIDplusException
  239.          */
  240.         protected function sessionSafeStart() {
  241.                 if (!isset($_SESSION)) {
  242.                         // TODO: session_name() makes some problems. Leave it away for now.
  243.                         //session_name('OIDplus_SESHDLR');
  244.                         if (!session_start()) {
  245.                                 throw new OIDplusException(_L('Session could not be started'));
  246.                         }
  247.                 }
  248.  
  249.                 if (!isset($_SESSION['ip'])) {
  250.                         if (!isset($_SERVER['REMOTE_ADDR'])) return;
  251.  
  252.                         // Remember the IP address of the user
  253.                         $_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
  254.                 } else {
  255.                         if ($_SERVER['REMOTE_ADDR'] != $_SESSION['ip']) {
  256.                                 // Was the session hijacked?! Get out of here!
  257.  
  258.                                 // We don't use $this->destroySession(), because this calls sessionSafeStart() again
  259.                                 $_SESSION = array();
  260.                                 session_destroy();
  261.                                 session_write_close();
  262.                                 OIDplus::cookieUtils()->unsetcookie(session_name()); // remove cookie, so GDPR people are happy
  263.                         }
  264.                 }
  265.         }
  266.  
  267.         /**
  268.          * @return void
  269.          */
  270.         function __destruct() {
  271.                 session_write_close();
  272.         }
  273.  
  274.         private $cacheSetValues = array(); // Important if you do a setValue() followed by an getValue()
  275.  
  276.         /**
  277.          * @return bool
  278.          */
  279.         public static function isActive(): bool {
  280.                 return isset($_COOKIE[session_name()]);
  281.         }
  282.  
  283.         /**
  284.          * @param string $data
  285.          * @param string $key
  286.          * @return string
  287.          * @throws \Exception
  288.          */
  289.         protected static function encrypt(string $data, string $key): string {
  290.                 if (function_exists('openssl_encrypt')) {
  291.                         $iv = random_bytes(16); // AES block size in CBC mode
  292.                         // Encryption
  293.                         $ciphertext = openssl_encrypt(
  294.                                 $data,
  295.                                 'AES-256-CBC',
  296.                                 hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, true),
  297.                                 OPENSSL_RAW_DATA,
  298.                                 $iv
  299.                         );
  300.                         // Authentication
  301.                         $hmac = sha3_512_hmac($iv . $ciphertext, $key, true);
  302.                         return $hmac . $iv . $ciphertext;
  303.                 } else {
  304.                         // When OpenSSL is not available, then we just do a HMAC
  305.                         $hmac = sha3_512_hmac($data, $key, true);
  306.                         return $hmac . $data;
  307.                 }
  308.         }
  309.  
  310.         /**
  311.          * @param string $data
  312.          * @param string $key
  313.          * @return string
  314.          * @throws OIDplusException
  315.          */
  316.         protected static function decrypt(string $data, string $key): string {
  317.                 if (function_exists('openssl_decrypt')) {
  318.                         $hmac       = mb_substr($data, 0, 64, '8bit');
  319.                         $iv         = mb_substr($data, 64, 16, '8bit');
  320.                         $ciphertext = mb_substr($data, 80, null, '8bit');
  321.                         // Authentication
  322.                         $hmacNew = sha3_512_hmac($iv . $ciphertext, $key, true);
  323.                         if (!hash_equals($hmac, $hmacNew)) {
  324.                                 throw new OIDplusException(_L('Authentication failed'));
  325.                         }
  326.                         // Decryption
  327.                         $cleartext = openssl_decrypt(
  328.                                 $ciphertext,
  329.                                 'AES-256-CBC',
  330.                                 hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, true),
  331.                                 OPENSSL_RAW_DATA,
  332.                                 $iv
  333.                         );
  334.                         if ($cleartext === false) {
  335.                                 throw new OIDplusException(_L('Decryption failed'));
  336.                         }
  337.                         return $cleartext;
  338.                 } else {
  339.                         // When OpenSSL is not available, then we just do a HMAC
  340.                         $hmac       = mb_substr($data, 0, 64, '8bit');
  341.                         $cleartext  = mb_substr($data, 64, null, '8bit');
  342.                         $hmacNew    = sha3_512_hmac($cleartext, $key, true);
  343.                         if (!hash_equals($hmac, $hmacNew)) {
  344.                                 throw new OIDplusException(_L('Authentication failed'));
  345.                         }
  346.                         return $cleartext;
  347.                 }
  348.         }
  349.  
  350.  
  351. }
  352.