|
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
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.