Details | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
2 | daniel-mar | 1 | <?php |
2 | |||
3 | /* |
||
4 | * OIDplus 2.0 |
||
1086 | daniel-mar | 5 | * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft |
2 | daniel-mar | 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 | |||
1050 | daniel-mar | 20 | namespace ViaThinkSoft\OIDplus; |
511 | daniel-mar | 21 | |
1086 | daniel-mar | 22 | // phpcs:disable PSR1.Files.SideEffects |
23 | \defined('INSIDE_OIDPLUS') or die; |
||
24 | // phpcs:enable PSR1.Files.SideEffects |
||
25 | |||
730 | daniel-mar | 26 | class OIDplusSessionHandler extends OIDplusBaseClass implements OIDplusGetterSetterInterface { |
2 | daniel-mar | 27 | |
1130 | daniel-mar | 28 | /** |
29 | * @var string|null |
||
30 | */ |
||
263 | daniel-mar | 31 | private $secret = ''; |
1130 | daniel-mar | 32 | |
33 | /** |
||
34 | * @var int|null |
||
35 | */ |
||
847 | daniel-mar | 36 | protected $sessionLifetime = 0; |
2 | daniel-mar | 37 | |
1116 | daniel-mar | 38 | /** |
39 | * @throws OIDplusException |
||
40 | */ |
||
263 | daniel-mar | 41 | public function __construct() { |
261 | daniel-mar | 42 | $this->sessionLifetime = OIDplus::baseConfig()->getValue('SESSION_LIFETIME', 30*60); |
1283 | daniel-mar | 43 | $this->secret = OIDplus::authUtils()->makeSecret(['b118abc8-f4ec-11ed-86ca-3c4a92df8582']); |
261 | daniel-mar | 44 | |
2 | daniel-mar | 45 | // **PREVENTING SESSION HIJACKING** |
46 | // Prevents javascript XSS attacks aimed to steal the session ID |
||
592 | daniel-mar | 47 | @ini_set('session.cookie_httponly', '1'); |
2 | daniel-mar | 48 | |
49 | // **PREVENTING SESSION FIXATION** |
||
50 | // Session ID cannot be passed through URLs |
||
592 | daniel-mar | 51 | @ini_set('session.use_only_cookies', '1'); |
2 | daniel-mar | 52 | |
592 | daniel-mar | 53 | @ini_set('session.use_trans_sid', '0'); |
85 | daniel-mar | 54 | |
2 | daniel-mar | 55 | // Uses a secure connection (HTTPS) if possible |
1117 | daniel-mar | 56 | @ini_set('session.cookie_secure', OIDplus::isSslAvailable() ? '1' : '0'); |
2 | daniel-mar | 57 | |
801 | daniel-mar | 58 | $path = OIDplus::webpath(null,OIDplus::PATH_RELATIVE); |
555 | daniel-mar | 59 | if (empty($path)) $path = '/'; |
60 | @ini_set('session.cookie_path', $path); |
||
2 | daniel-mar | 61 | |
563 | daniel-mar | 62 | @ini_set('session.cookie_samesite', OIDplus::baseConfig()->getValue('COOKIE_SAMESITE_POLICY', 'Strict')); |
2 | daniel-mar | 63 | |
592 | daniel-mar | 64 | @ini_set('session.use_strict_mode', '1'); |
2 | daniel-mar | 65 | |
261 | daniel-mar | 66 | @ini_set('session.gc_maxlifetime', $this->sessionLifetime); |
2 | daniel-mar | 67 | } |
68 | |||
1116 | daniel-mar | 69 | /** |
70 | * @return void |
||
71 | * @throws OIDplusException |
||
72 | */ |
||
86 | daniel-mar | 73 | protected function sessionSafeStart() { |
179 | daniel-mar | 74 | if (!isset($_SESSION)) { |
75 | // TODO: session_name() makes some problems. Leave it away for now. |
||
261 | daniel-mar | 76 | //session_name('OIDplus_SESHDLR'); |
179 | daniel-mar | 77 | if (!session_start()) { |
360 | daniel-mar | 78 | throw new OIDplusException(_L('Session could not be started')); |
179 | daniel-mar | 79 | } |
80 | } |
||
86 | daniel-mar | 81 | |
87 | daniel-mar | 82 | if (!isset($_SESSION['ip'])) { |
83 | if (!isset($_SERVER['REMOTE_ADDR'])) return; |
||
86 | daniel-mar | 84 | |
87 | daniel-mar | 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! |
||
435 | daniel-mar | 90 | |
91 | // We don't use $this->destroySession(), because this calls sessionSafeStart() again |
||
92 | $_SESSION = array(); |
||
93 | session_destroy(); |
||
94 | session_write_close(); |
||
557 | daniel-mar | 95 | OIDplus::cookieUtils()->unsetcookie(session_name()); // remove cookie, so GDPR people are happy |
87 | daniel-mar | 96 | } |
86 | daniel-mar | 97 | } |
98 | } |
||
99 | |||
1116 | daniel-mar | 100 | /** |
101 | * @return void |
||
102 | */ |
||
2 | daniel-mar | 103 | function __destruct() { |
104 | session_write_close(); |
||
105 | } |
||
106 | |||
424 | daniel-mar | 107 | private $cacheSetValues = array(); // Important if you do a setValue() followed by an getValue() |
108 | |||
1116 | daniel-mar | 109 | /** |
110 | * @param string $name |
||
111 | * @param mixed $value |
||
112 | * @return void |
||
113 | * @throws OIDplusException |
||
114 | */ |
||
115 | public function setValue(string $name, $value) { |
||
716 | daniel-mar | 116 | $enc_data = self::encrypt($value, $this->secret); |
426 | daniel-mar | 117 | |
716 | daniel-mar | 118 | $this->cacheSetValues[$name] = $enc_data; |
119 | |||
86 | daniel-mar | 120 | $this->sessionSafeStart(); |
557 | daniel-mar | 121 | OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime); |
85 | daniel-mar | 122 | |
716 | daniel-mar | 123 | $_SESSION[$name] = $enc_data; |
2 | daniel-mar | 124 | } |
125 | |||
1116 | daniel-mar | 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) { |
||
424 | daniel-mar | 133 | if (isset($this->cacheSetValues[$name])) return self::decrypt($this->cacheSetValues[$name], $this->secret); |
134 | |||
585 | daniel-mar | 135 | if (!$this->isActive()) return $default; // GDPR: Only start a session when we really need one |
86 | daniel-mar | 136 | $this->sessionSafeStart(); |
557 | daniel-mar | 137 | OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime); |
85 | daniel-mar | 138 | |
569 | daniel-mar | 139 | if (!isset($_SESSION[$name])) return $default; |
2 | daniel-mar | 140 | return self::decrypt($_SESSION[$name], $this->secret); |
141 | } |
||
142 | |||
1116 | daniel-mar | 143 | /** |
144 | * @param string $name |
||
145 | * @return bool |
||
146 | * @throws OIDplusException |
||
147 | */ |
||
148 | public function exists(string $name): bool { |
||
569 | daniel-mar | 149 | if (isset($this->cacheSetValues[$name])) return true; |
150 | |||
585 | daniel-mar | 151 | if (!$this->isActive()) return false; // GDPR: Only start a session when we really need one |
569 | daniel-mar | 152 | $this->sessionSafeStart(); |
153 | OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime); |
||
154 | |||
1116 | daniel-mar | 155 | return isset($_SESSION[$name]); |
569 | daniel-mar | 156 | } |
157 | |||
1116 | daniel-mar | 158 | /** |
159 | * @param string $name |
||
160 | * @return void |
||
161 | * @throws OIDplusException |
||
162 | */ |
||
163 | public function delete(string $name) { |
||
569 | daniel-mar | 164 | if (isset($this->cacheSetValues[$name])) unset($this->cacheSetValues[$name]); |
165 | |||
585 | daniel-mar | 166 | if (!$this->isActive()) return; // GDPR: Only start a session when we really need one |
569 | daniel-mar | 167 | $this->sessionSafeStart(); |
168 | OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime); |
||
169 | |||
170 | unset($_SESSION[$name]); |
||
171 | } |
||
172 | |||
1116 | daniel-mar | 173 | /** |
174 | * @return void |
||
175 | * @throws OIDplusException |
||
176 | */ |
||
85 | daniel-mar | 177 | public function destroySession() { |
585 | daniel-mar | 178 | if (!$this->isActive()) return; |
85 | daniel-mar | 179 | |
86 | daniel-mar | 180 | $this->sessionSafeStart(); |
557 | daniel-mar | 181 | OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime); |
85 | daniel-mar | 182 | |
183 | $_SESSION = array(); |
||
184 | session_destroy(); |
||
185 | session_write_close(); |
||
557 | daniel-mar | 186 | OIDplus::cookieUtils()->unsetcookie(session_name()); // remove cookie, so GDPR people are happy |
85 | daniel-mar | 187 | } |
188 | |||
1116 | daniel-mar | 189 | /** |
190 | * @return bool |
||
191 | */ |
||
192 | public function isActive(): bool { |
||
585 | daniel-mar | 193 | return isset($_COOKIE[session_name()]); |
194 | } |
||
195 | |||
1116 | daniel-mar | 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 { |
||
465 | daniel-mar | 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', |
||
826 | daniel-mar | 209 | hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, true), |
465 | daniel-mar | 210 | OPENSSL_RAW_DATA, |
211 | $iv |
||
212 | ); |
||
213 | // Authentication |
||
711 | daniel-mar | 214 | $hmac = sha3_512_hmac($iv . $ciphertext, $key, true); |
465 | daniel-mar | 215 | return $hmac . $iv . $ciphertext; |
216 | } else { |
||
217 | // When OpenSSL is not available, then we just do a HMAC |
||
711 | daniel-mar | 218 | $hmac = sha3_512_hmac($data, $key, true); |
465 | daniel-mar | 219 | return $hmac . $data; |
220 | } |
||
6 | daniel-mar | 221 | } |
2 | daniel-mar | 222 | |
1116 | daniel-mar | 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 { |
||
465 | daniel-mar | 230 | if (function_exists('openssl_decrypt')) { |
711 | daniel-mar | 231 | $hmac = mb_substr($data, 0, 64, '8bit'); |
232 | $iv = mb_substr($data, 64, 16, '8bit'); |
||
233 | $ciphertext = mb_substr($data, 80, null, '8bit'); |
||
465 | daniel-mar | 234 | // Authentication |
711 | daniel-mar | 235 | $hmacNew = sha3_512_hmac($iv . $ciphertext, $key, true); |
465 | daniel-mar | 236 | if (!hash_equals($hmac, $hmacNew)) { |
237 | throw new OIDplusException(_L('Authentication failed')); |
||
238 | } |
||
239 | // Decryption |
||
711 | daniel-mar | 240 | $cleartext = openssl_decrypt( |
465 | daniel-mar | 241 | $ciphertext, |
242 | 'AES-256-CBC', |
||
826 | daniel-mar | 243 | hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, true), |
465 | daniel-mar | 244 | OPENSSL_RAW_DATA, |
245 | $iv |
||
246 | ); |
||
711 | daniel-mar | 247 | if ($cleartext === false) { |
248 | throw new OIDplusException(_L('Decryption failed')); |
||
249 | } |
||
250 | return $cleartext; |
||
465 | daniel-mar | 251 | } else { |
252 | // When OpenSSL is not available, then we just do a HMAC |
||
711 | daniel-mar | 253 | $hmac = mb_substr($data, 0, 64, '8bit'); |
254 | $cleartext = mb_substr($data, 64, null, '8bit'); |
||
255 | $hmacNew = sha3_512_hmac($cleartext, $key, true); |
||
465 | daniel-mar | 256 | if (!hash_equals($hmac, $hmacNew)) { |
257 | throw new OIDplusException(_L('Authentication failed')); |
||
258 | } |
||
259 | return $cleartext; |
||
6 | daniel-mar | 260 | } |
261 | } |
||
424 | daniel-mar | 262 | } |