Subversion Repositories oidplus

Rev

Rev 1130 | Go to most recent revision | Details | Compare with Previous | 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);
1282 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
}