1 | <?php |
||
2 | |||
3 | /** |
||
4 | * TSecurityManager class file |
||
5 | * |
||
6 | * @author Qiang Xue <[email protected]> |
||
7 | * @author LANDWEHR Computer und Software GmbH <[email protected]> |
||
8 | * @link https://github.com/pradosoft/prado |
||
9 | * @license https://github.com/pradosoft/prado/blob/master/LICENSE |
||
10 | */ |
||
11 | |||
12 | namespace Prado\Security; |
||
13 | |||
14 | use Prado\Exceptions\TInvalidDataValueException; |
||
15 | use Prado\Exceptions\TNotSupportedException; |
||
16 | use Prado\TPropertyValue; |
||
17 | |||
18 | /** |
||
19 | * TSecurityManager class |
||
20 | * |
||
21 | * TSecurityManager provides private keys, hashing and encryption |
||
22 | * functionalities that may be used by other PRADO components, |
||
23 | * such as viewstate persister, cookies. |
||
24 | * |
||
25 | * TSecurityManager is mainly used to protect data from being tampered |
||
26 | * and viewed. It can generate HMAC and encrypt the data. |
||
27 | * The private key used to generate HMAC is set by {@see setValidationKey ValidationKey}. |
||
28 | * The key used to encrypt data is specified by {@see setEncryptionKey EncryptionKey}. |
||
29 | * If the above keys are not explicitly set, random keys will be generated |
||
30 | * and used. |
||
31 | * |
||
32 | * To prefix data with an HMAC, call {@see hashData()}. |
||
33 | * To validate if data is tampered, call {@see validateData()}, which will |
||
34 | * return the real data if it is not tampered. |
||
35 | * The algorithm used to generated HMAC is specified by {@see setHashAlgorithm HashAlgorithm}. |
||
36 | * |
||
37 | * To encrypt and decrypt data, call {@see encrypt()} and {@see decrypt()} |
||
38 | * respectively. The encryption algorithm can be set by {@see setCryptAlgorithm CryptAlgorithm}. |
||
39 | * |
||
40 | * Note, to use encryption, the PHP OpenSSL extension must be loaded. This was introduced in |
||
41 | * Prado4, older versions used the deprecated mcrypt extension with rijndael-256 cipher as |
||
42 | * default, which does not have an equivalent in OpenSSL. Developers should keep that in mind |
||
43 | * when migrating from Prado3 to Prado4. |
||
44 | * |
||
45 | * @author Qiang Xue <[email protected]> |
||
46 | * @author LANDWEHR Computer und Software GmbH <[email protected]> |
||
47 | * @since 3.0 |
||
48 | */ |
||
49 | class TSecurityManager extends \Prado\TModule |
||
50 | { |
||
51 | public const STATE_VALIDATION_KEY = 'prado:securitymanager:validationkey'; |
||
52 | public const STATE_ENCRYPTION_KEY = 'prado:securitymanager:encryptionkey'; |
||
53 | |||
54 | private $_validationKey; |
||
55 | private $_encryptionKey; |
||
56 | private $_hashAlgorithm = 'sha256'; |
||
57 | private $_cryptAlgorithm = 'aes-256-cbc'; |
||
58 | private $_mbstring; |
||
59 | |||
60 | /** |
||
61 | * Initializes the module. |
||
62 | * The security module is registered with the application. |
||
63 | * @param \Prado\Xml\TXmlElement $config initial module configuration |
||
64 | */ |
||
65 | public function init($config) |
||
66 | 9 | { |
|
67 | $this->_mbstring = extension_loaded('mbstring'); |
||
68 | 9 | $this->getApplication()->setSecurityManager($this); |
|
69 | 9 | parent::init($config); |
|
70 | 9 | } |
|
71 | |||
72 | /** |
||
73 | * Generates a random key. |
||
74 | */ |
||
75 | 3 | protected function generateRandomKey() |
|
76 | { |
||
77 | 3 | return sprintf('%08x%08x%08x%08x', mt_rand(), mt_rand(), mt_rand(), mt_rand()); |
|
78 | } |
||
79 | |||
80 | /** |
||
81 | * @return string the private key used to generate HMAC. |
||
82 | * If the key is not explicitly set, a random one is generated and returned. |
||
83 | */ |
||
84 | 4 | public function getValidationKey() |
|
85 | { |
||
86 | 4 | if (null === $this->_validationKey) { |
|
87 | 2 | if (null === ($this->_validationKey = $this->getApplication()->getGlobalState(self::STATE_VALIDATION_KEY))) { |
|
88 | 2 | $this->_validationKey = $this->generateRandomKey(); |
|
89 | 2 | $this->getApplication()->setGlobalState(self::STATE_VALIDATION_KEY, $this->_validationKey, null, true); |
|
90 | } |
||
91 | } |
||
92 | 4 | return $this->_validationKey; |
|
93 | } |
||
94 | |||
95 | /** |
||
96 | * @param string $value the key used to generate HMAC |
||
97 | * @throws TInvalidDataValueException if the key is empty |
||
98 | */ |
||
99 | 3 | public function setValidationKey($value) |
|
100 | { |
||
101 | 3 | if ('' === $value) { |
|
102 | 1 | throw new TInvalidDataValueException('securitymanager_validationkey_invalid'); |
|
103 | } |
||
104 | |||
105 | 3 | $this->_validationKey = $value; |
|
106 | 3 | } |
|
107 | |||
108 | /** |
||
109 | * @return string the private key used to encrypt/decrypt data. |
||
110 | * If the key is not explicitly set, a random one is generated and returned. |
||
111 | */ |
||
112 | 2 | public function getEncryptionKey() |
|
113 | { |
||
114 | 2 | if (null === $this->_encryptionKey) { |
|
115 | 1 | if (null === ($this->_encryptionKey = $this->getApplication()->getGlobalState(self::STATE_ENCRYPTION_KEY))) { |
|
116 | 1 | $this->_encryptionKey = $this->generateRandomKey(); |
|
117 | 1 | $this->getApplication()->setGlobalState(self::STATE_ENCRYPTION_KEY, $this->_encryptionKey, null, true); |
|
118 | } |
||
119 | } |
||
120 | 2 | return $this->_encryptionKey; |
|
121 | } |
||
122 | |||
123 | /** |
||
124 | * @param string $value the key used to encrypt/decrypt data. |
||
125 | * @throws TInvalidDataValueException if the key is empty |
||
126 | */ |
||
127 | 2 | public function setEncryptionKey($value) |
|
128 | { |
||
129 | 2 | if ('' === $value) { |
|
130 | 1 | throw new TInvalidDataValueException('securitymanager_encryptionkey_invalid'); |
|
131 | } |
||
132 | |||
133 | 2 | $this->_encryptionKey = $value; |
|
134 | 2 | } |
|
135 | |||
136 | /** |
||
137 | * @return string hashing algorithm used to generate HMAC. Defaults to 'sha256'. |
||
138 | */ |
||
139 | 1 | public function getHashAlgorithm() |
|
140 | { |
||
141 | 1 | return $this->_hashAlgorithm; |
|
142 | } |
||
143 | |||
144 | /** |
||
145 | * This method accepts all hash algorithms returned by hash_algos(). |
||
146 | * @param string $value hashing algorithm used to generate HMAC. |
||
147 | * @throws TInvalidDataValueException if the hash algorithm is not supported. |
||
148 | */ |
||
149 | 3 | public function setHashAlgorithm($value) |
|
150 | { |
||
151 | 3 | $this->_hashAlgorithm = TPropertyValue::ensureString($value); |
|
152 | 3 | if (!in_array($this->_hashAlgorithm, function_exists('hash_hmac_algos') ? hash_hmac_algos() : hash_algos())) { |
|
153 | 1 | throw new TInvalidDataValueException('securitymanager_hash_algorithm_invalid'); |
|
154 | } |
||
155 | 3 | } |
|
156 | |||
157 | /** |
||
158 | * @return mixed the algorithm used to encrypt/decrypt data. Defaults to the string 'aes-256-cbc'. |
||
159 | */ |
||
160 | 1 | public function getCryptAlgorithm() |
|
161 | { |
||
162 | 1 | return $this->_cryptAlgorithm; |
|
163 | } |
||
164 | |||
165 | /** |
||
166 | * Sets the crypt algorithm (also known as cipher or cypher) that will be used for {@see encrypt} and {@see decrypt}. |
||
167 | * @param mixed $value either a string containing the cipther name. |
||
168 | */ |
||
169 | 1 | public function setCryptAlgorithm($value) |
|
170 | { |
||
171 | 1 | $this->_cryptAlgorithm = TPropertyValue::ensureString($value); |
|
172 | 1 | if (!in_array($this->_cryptAlgorithm, openssl_get_cipher_methods())) { |
|
173 | 1 | throw new TInvalidDataValueException('securitymanager_crypt_algorithm_invalid'); |
|
174 | } |
||
175 | } |
||
176 | |||
177 | /** |
||
178 | * Encrypts data with {@see getEncryptionKey EncryptionKey}. |
||
179 | * @param string $data data to be encrypted. |
||
180 | * @throws TNotSupportedException if PHP OpenSSL extension is not loaded |
||
181 | * @return string the encrypted data |
||
182 | */ |
||
183 | 1 | public function encrypt($data) |
|
184 | { |
||
185 | 1 | if (extension_loaded('openssl')) { |
|
186 | 1 | $key = md5($this->getEncryptionKey()); |
|
187 | 1 | $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($this->_cryptAlgorithm)); |
|
188 | 1 | return $iv . openssl_encrypt($data, $this->_cryptAlgorithm, $key, 0, $iv); |
|
189 | } else { |
||
190 | throw new TNotSupportedException('securitymanager_openssl_required'); |
||
191 | } |
||
192 | } |
||
193 | |||
194 | /** |
||
195 | * Decrypts data with {@see getEncryptionKey EncryptionKey}. |
||
196 | * @param string $data data to be decrypted. |
||
197 | * @throws TNotSupportedException if PHP OpenSSL extension is not loaded |
||
198 | * @return string the decrypted data |
||
199 | */ |
||
200 | 1 | public function decrypt($data) |
|
201 | { |
||
202 | 1 | if (extension_loaded('openssl')) { |
|
203 | 1 | $key = md5($this->getEncryptionKey()); |
|
204 | 1 | $iv = $this->substr($data, 0, openssl_cipher_iv_length($this->_cryptAlgorithm)); |
|
205 | 1 | return openssl_decrypt($this->substr($data, $this->strlen($iv), $this->strlen($data)), $this->_cryptAlgorithm, $key, 0, $iv); |
|
206 | } else { |
||
207 | throw new TNotSupportedException('securitymanager_openssl_required'); |
||
208 | } |
||
209 | } |
||
210 | |||
211 | /** |
||
212 | * Prefixes data with an HMAC. |
||
213 | * @param string $data data to be hashed. |
||
214 | * @return string data prefixed with HMAC |
||
215 | */ |
||
216 | 3 | public function hashData($data) |
|
217 | { |
||
218 | 3 | $hmac = $this->computeHMAC($data); |
|
219 | 3 | return $hmac . $data; |
|
220 | } |
||
221 | |||
222 | /** |
||
223 | * Validates if data is tampered. |
||
224 | * @param string $data data to be validated. The data must be previously |
||
225 | * generated using {@see hashData()}. |
||
226 | * @return string the real data with HMAC stripped off. False if the data |
||
227 | * is tampered. |
||
228 | */ |
||
229 | 2 | public function validateData($data) |
|
230 | { |
||
231 | 2 | $len = $this->strlen($this->computeHMAC('test')); |
|
232 | |||
233 | 2 | if ($this->strlen($data) < $len) { |
|
234 | 1 | return false; |
|
0 ignored issues
–
show
Bug
Best Practice
introduced
by
![]() |
|||
235 | } |
||
236 | |||
237 | 2 | $hmac = $this->substr($data, 0, $len); |
|
238 | 2 | $data2 = $this->substr($data, $len, $this->strlen($data)); |
|
239 | 2 | return $hmac === $this->computeHMAC($data2) ? $data2 : false; |
|
0 ignored issues
–
show
The expression
return $hmac === $this->...data2) ? $data2 : false could also return false which is incompatible with the documented return type string . Did you maybe forget to handle an error condition?
If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled. ![]() |
|||
240 | } |
||
241 | |||
242 | /** |
||
243 | * Computes the HMAC for the data with {@see getValidationKey ValidationKey}. |
||
244 | * @param string $data data to be generated HMAC |
||
245 | * @return string the HMAC for the data |
||
246 | */ |
||
247 | 3 | protected function computeHMAC($data) |
|
248 | { |
||
249 | 3 | return hash_hmac($this->_hashAlgorithm, $data, $this->getValidationKey()); |
|
250 | } |
||
251 | |||
252 | /** |
||
253 | * Returns the length of the given string. |
||
254 | * If available uses the multibyte string function mb_strlen. |
||
255 | * @param string $string $string the string being measured for length |
||
256 | * @return int the length of the string |
||
257 | */ |
||
258 | 3 | private function strlen($string) |
|
259 | { |
||
260 | 3 | return $this->_mbstring ? mb_strlen($string, '8bit') : strlen($string); |
|
261 | } |
||
262 | |||
263 | /** |
||
264 | * Returns the portion of string specified by the start and length parameters. |
||
265 | * If available uses the multibyte string function mb_substr |
||
266 | * @param string $string the input string. Must be one character or longer. |
||
267 | * @param int $start the starting position |
||
268 | * @param int $length the desired portion length |
||
269 | * @return string the extracted part of string, or FALSE on failure or an empty string. |
||
270 | */ |
||
271 | 3 | private function substr($string, $start, $length) |
|
272 | { |
||
273 | 3 | return $this->_mbstring ? mb_substr($string, $start, $length, '8bit') : substr($string, $start, $length); |
|
274 | } |
||
275 | } |
||
276 |