1
|
|
|
<?php |
|
|
|
|
2
|
|
|
/** |
3
|
|
|
* Encrypt PHP session data for the internal PHP save handlers |
4
|
|
|
* |
5
|
|
|
* The encryption is built using OpenSSL extension with AES-256-CBC and the |
6
|
|
|
* authentication is provided using HMAC with SHA256. |
7
|
|
|
* |
8
|
|
|
* @author Enrico Zimuel ([email protected]) |
9
|
|
|
* @copyright MIT License |
10
|
|
|
*/ |
11
|
|
|
namespace PHPSecureSession; |
12
|
|
|
|
13
|
|
|
use SessionHandler; |
14
|
|
|
|
15
|
|
|
class SecureHandler extends SessionHandler |
16
|
|
|
{ |
17
|
|
|
/** |
18
|
|
|
* Encryption and authentication key |
19
|
|
|
* @var string |
20
|
|
|
*/ |
21
|
|
|
protected $key; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Constructor |
25
|
|
|
*/ |
26
|
|
|
public function __construct() |
27
|
|
|
{ |
28
|
|
|
if (!extension_loaded('openssl')) { |
29
|
|
|
throw new \RuntimeException(sprintf( |
30
|
|
|
"You need the OpenSSL extension to use %s", |
31
|
|
|
__CLASS__ |
32
|
|
|
)); |
33
|
|
|
} |
34
|
|
|
if (!extension_loaded('mbstring')) { |
35
|
|
|
throw new \RuntimeException(sprintf( |
36
|
|
|
"You need the Multibytes extension to use %s", |
37
|
|
|
__CLASS__ |
38
|
|
|
)); |
39
|
|
|
} |
40
|
|
|
} |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Open the session |
44
|
|
|
* |
45
|
|
|
* @param string $save_path |
46
|
|
|
* @param string $session_name |
47
|
|
|
* @return bool |
48
|
|
|
*/ |
49
|
|
|
public function open($save_path, $session_name) |
50
|
|
|
{ |
51
|
|
|
$this->key = $this->getKey('KEY_'.$session_name); |
52
|
|
|
return parent::open($save_path, $session_name); |
53
|
|
|
} |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* Read from session and decrypt |
57
|
|
|
* |
58
|
|
|
* @param string $session_id |
59
|
|
|
*/ |
60
|
|
|
public function read($session_id) |
61
|
|
|
{ |
62
|
|
|
$data = parent::read($session_id); |
63
|
|
|
return empty($data) ? '' : $this->decrypt($data, $this->key); |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Encrypt the data and write into the session |
68
|
|
|
* |
69
|
|
|
* @param string $session_id |
70
|
|
|
* @param string $data |
71
|
|
|
*/ |
72
|
|
|
public function write($session_id, $data) |
73
|
|
|
{ |
74
|
|
|
return parent::write($session_id, $this->encrypt($data, $this->key)); |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Encrypt and authenticate |
79
|
|
|
* |
80
|
|
|
* @param string $data |
81
|
|
|
* @param string $key |
82
|
|
|
* @return string |
83
|
|
|
*/ |
84
|
|
|
protected function encrypt($data, $key) |
85
|
|
|
{ |
86
|
|
|
$block_iv = random_bytes(16); // AES block size in CBC mode |
87
|
|
|
// Encryption |
88
|
|
|
$ciphertext = openssl_encrypt( |
89
|
|
|
$data, |
90
|
|
|
'AES-256-CBC', |
91
|
|
|
mb_substr($key, 0, 32, '8bit'), |
92
|
|
|
OPENSSL_RAW_DATA, |
93
|
|
|
$block_iv |
94
|
|
|
); |
95
|
|
|
// Authentication |
96
|
|
|
$hmac = hash_hmac( |
97
|
|
|
'SHA256', |
98
|
|
|
$block_iv.$ciphertext, |
99
|
|
|
mb_substr($key, 32, null, '8bit'), |
100
|
|
|
true |
101
|
|
|
); |
102
|
|
|
return $hmac.$block_iv.$ciphertext; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Authenticate and decrypt |
107
|
|
|
* |
108
|
|
|
* @param string $data |
109
|
|
|
* @param string $key |
110
|
|
|
* @return string |
111
|
|
|
*/ |
112
|
|
|
protected function decrypt($data, $key) |
113
|
|
|
{ |
114
|
|
|
$hmac = mb_substr($data, 0, 32, '8bit'); |
115
|
|
|
$block_iv = mb_substr($data, 32, 16, '8bit'); |
116
|
|
|
$ciphertext = mb_substr($data, 48, null, '8bit'); |
117
|
|
|
// Authentication |
118
|
|
|
$hmacNew = hash_hmac( |
119
|
|
|
'SHA256', |
120
|
|
|
$block_iv.$ciphertext, |
121
|
|
|
mb_substr($key, 32, null, '8bit'), |
122
|
|
|
true |
123
|
|
|
); |
124
|
|
|
if (!$this->hash_equals($hmac, $hmacNew)) { |
125
|
|
|
throw new \RuntimeException('Authentication failed'); |
126
|
|
|
} |
127
|
|
|
// Decrypt |
128
|
|
|
return openssl_decrypt( |
129
|
|
|
$ciphertext, |
130
|
|
|
'AES-256-CBC', |
131
|
|
|
mb_substr($key, 0, 32, '8bit'), |
132
|
|
|
OPENSSL_RAW_DATA, |
133
|
|
|
$block_iv |
134
|
|
|
); |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* Get the encryption and authentication keys from cookie |
139
|
|
|
* |
140
|
|
|
* @param string $name |
141
|
|
|
* @return string |
142
|
|
|
*/ |
143
|
|
|
protected function getKey($name) |
144
|
|
|
{ |
145
|
|
|
if (empty($_COOKIE[$name]) === true) { |
146
|
|
|
// 32 for encryption and 32 for authentication |
|
|
|
|
147
|
|
|
$key = random_bytes(64); |
148
|
|
|
$cookieParam = session_get_cookie_params(); |
149
|
|
|
setcookie( |
150
|
|
|
$name, |
151
|
|
|
base64_encode($key), |
152
|
|
|
// if session cookie lifetime > 0 then add to current time |
153
|
|
|
// otherwise leave it as zero, honoring zero's special meaning |
154
|
|
|
// expire at browser close. |
155
|
|
|
($cookieParam['lifetime'] > 0) ? time() + $cookieParam['lifetime'] : 0, |
156
|
|
|
$cookieParam['path'], |
157
|
|
|
$cookieParam['domain'], |
158
|
|
|
$cookieParam['secure'], |
159
|
|
|
$cookieParam['httponly'] |
160
|
|
|
); |
161
|
|
|
return $key; |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
// If not returned before - cookie exists |
165
|
|
|
return base64_decode($_COOKIE[$name]); |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* Hash equals function for PHP 5.5+ |
170
|
|
|
* |
171
|
|
|
* @param string $expected |
172
|
|
|
* @param string $actual |
173
|
|
|
* @return bool |
174
|
|
|
*/ |
175
|
|
|
protected function hash_equals($expected, $actual) |
176
|
|
|
{ |
177
|
|
|
$expected = filter_var($expected, FILTER_SANITIZE_STRING); |
178
|
|
|
$actual = filter_var($actual, FILTER_SANITIZE_STRING); |
179
|
|
|
if (function_exists('hash_equals')) { |
180
|
|
|
return hash_equals($expected, $actual); |
181
|
|
|
} |
182
|
|
|
$lenExpected = mb_strlen($expected, '8bit'); |
183
|
|
|
$lenActual = mb_strlen($actual, '8bit'); |
184
|
|
|
$len = min($lenExpected, $lenActual); |
185
|
|
|
$result = 0; |
186
|
|
|
for ($i = 0; $i < $len; $i++) { |
187
|
|
|
$result |= ord($expected[$i]) ^ ord($actual[$i]); |
188
|
|
|
} |
189
|
|
|
$result |= $lenExpected ^ $lenActual; |
190
|
|
|
return ($result === 0); |
191
|
|
|
} |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
// random_bytes is a PHP7 new function required by SecureHandler. |
195
|
|
|
// random_compat has been written to define it in PHP5 |
196
|
|
|
if (file_exists('./includes/libraries/misc/random_compat/random.php')) { |
197
|
|
|
require_once('./includes/libraries/misc/random_compat/random.php'); |
198
|
|
|
} else { |
199
|
|
|
if (file_exists('../includes/libraries/misc/random_compat/random.php')) { |
200
|
|
|
require_once('../includes/libraries/misc/random_compat/random.php'); |
201
|
|
|
} else { |
202
|
|
|
require_once('../../includes/libraries/misc/random_compat/random.php'); |
203
|
|
|
} |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
// load |
207
|
|
|
session_set_save_handler( |
208
|
|
|
new SecureHandler(), |
209
|
|
|
true |
210
|
|
|
); |
211
|
|
|
|
The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.
The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.
To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.