Passed
Push — development ( 81a4db...5845ec )
by Nils
07:52
created

SecureHandler   A

Complexity

Total Complexity 16

Size/Duplication

Total Lines 176
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 176
rs 10
c 0
b 0
f 0
wmc 16
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