Subversion Repositories oidplus

Rev

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