Issues (1474)

framework/Security/TSecurityManager.php (2 issues)

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
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
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
Bug Best Practice introduced by
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.

Loading history...
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