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