1
2/*
3 +------------------------------------------------------------------------+
4 | Phalcon Framework                                                      |
5 +------------------------------------------------------------------------+
6 | Copyright (c) 2011-2017 Phalcon Team (https://phalconphp.com)          |
7 +------------------------------------------------------------------------+
8 | This source file is subject to the New BSD License that is bundled     |
9 | with this package in the file LICENSE.txt.                             |
10 |                                                                        |
11 | If you did not receive a copy of the license and are unable to         |
12 | obtain it through the world-wide-web, please send an email             |
13 | to license@phalconphp.com so we can send you a copy immediately.       |
14 +------------------------------------------------------------------------+
15 | Authors: Andres Gutierrez <andres@phalconphp.com>                      |
16 |          Eduar Carvajal <eduar@phalconphp.com>                         |
17 +------------------------------------------------------------------------+
18 */
19
20namespace Phalcon;
21
22use Phalcon\DiInterface;
23use Phalcon\Security\Random;
24use Phalcon\Security\Exception;
25use Phalcon\Di\InjectionAwareInterface;
26use Phalcon\Session\AdapterInterface as SessionInterface;
27
28/**
29 * Phalcon\Security
30 *
31 * This component provides a set of functions to improve the security in Phalcon applications
32 *
33 *<code>
34 * $login    = $this->request->getPost("login");
35 * $password = $this->request->getPost("password");
36 *
37 * $user = Users::findFirstByLogin($login);
38 *
39 * if ($user) {
40 *     if ($this->security->checkHash($password, $user->password)) {
41 *         // The password is valid
42 *     }
43 * }
44 *</code>
45 */
46class Security implements InjectionAwareInterface
47{
48
49	protected _dependencyInjector;
50
51	protected _workFactor = 8 { set, get };
52
53	protected _numberBytes = 16;
54
55	protected _tokenKeySessionID = "$PHALCON/CSRF/KEY$";
56
57	protected _tokenValueSessionID = "$PHALCON/CSRF$";
58
59	protected _token;
60
61	protected _tokenKey;
62
63	protected _random;
64
65	protected _defaultHash;
66
67	const CRYPT_DEFAULT	   =	0;
68
69	const CRYPT_STD_DES	   =	1;
70
71	const CRYPT_EXT_DES	   =	2;
72
73	const CRYPT_MD5		   =	3;
74
75	const CRYPT_BLOWFISH       =	4;
76
77	const CRYPT_BLOWFISH_A     =    5;
78
79	const CRYPT_BLOWFISH_X     =	6;
80
81	const CRYPT_BLOWFISH_Y     =	7;
82
83	const CRYPT_SHA256	   =	8;
84
85	const CRYPT_SHA512	   =	9;
86
87	/**
88	 * Phalcon\Security constructor
89	 */
90	public function __construct()
91	{
92		let this->_random = new Random();
93	}
94
95	/**
96	 * Sets the dependency injector
97	 */
98	public function setDI(<DiInterface> dependencyInjector) -> void
99	{
100		let this->_dependencyInjector = dependencyInjector;
101	}
102
103	/**
104	 * Returns the internal dependency injector
105	 */
106	public function getDI() -> <DiInterface>
107	{
108		return this->_dependencyInjector;
109	}
110
111	/**
112	 * Sets a number of bytes to be generated by the openssl pseudo random generator
113	 */
114	public function setRandomBytes(long! randomBytes) -> <Security>
115	{
116		let this->_numberBytes = randomBytes;
117
118		return this;
119	}
120
121	/**
122	 * Returns a number of bytes to be generated by the openssl pseudo random generator
123	 */
124	public function getRandomBytes() -> string
125	{
126		return this->_numberBytes;
127	}
128
129	/**
130	 * Returns a secure random number generator instance
131	 */
132	public function getRandom() -> <Random>
133	{
134		return this->_random;
135	}
136
137	/**
138	 * Generate a >22-length pseudo random string to be used as salt for passwords
139	 */
140	public function getSaltBytes(int numberBytes = 0) -> string
141	{
142		var safeBytes;
143
144		if !numberBytes {
145			let numberBytes = (int) this->_numberBytes;
146		}
147
148		loop {
149			let safeBytes = this->_random->base64Safe(numberBytes);
150
151			if !safeBytes || strlen(safeBytes) < numberBytes {
152				continue;
153			}
154
155			break;
156		}
157
158		return safeBytes;
159	}
160
161	/**
162	 * Creates a password hash using bcrypt with a pseudo random salt
163	 */
164	public function hash(string password, int workFactor = 0) -> string
165	{
166		int hash;
167		string variant;
168		var saltBytes;
169
170		if !workFactor {
171			let workFactor = (int) this->_workFactor;
172		}
173
174		let hash = (int) this->_defaultHash;
175
176		switch hash {
177
178			case self::CRYPT_BLOWFISH_A:
179				let variant = "a";
180				break;
181
182			case self::CRYPT_BLOWFISH_X:
183				let variant = "x";
184				break;
185
186			case self::CRYPT_BLOWFISH_Y:
187				let variant = "y";
188				break;
189
190			case self::CRYPT_MD5:
191				let variant = "1";
192				break;
193
194			case self::CRYPT_SHA256:
195				let variant = "5";
196				break;
197
198			case self::CRYPT_SHA512:
199				let variant = "6";
200				break;
201
202			case self::CRYPT_DEFAULT:
203			default:
204				let variant = "y";
205				break;
206		}
207
208		switch hash {
209
210			case self::CRYPT_STD_DES:
211			case self::CRYPT_EXT_DES:
212
213				/* Standard DES-based hash with a two character salt from the alphabet "./0-9A-Za-z". */
214
215				if (hash == self::CRYPT_EXT_DES) {
216					let saltBytes = "_".this->getSaltBytes(8);
217				} else {
218					let saltBytes = this->getSaltBytes(2);
219				}
220
221				if typeof saltBytes != "string" {
222					throw new Exception("Unable to get random bytes for the salt");
223				}
224
225				return crypt(password, saltBytes);
226
227			case self::CRYPT_MD5:
228			case self::CRYPT_SHA256:
229			case self::CRYPT_SHA512:
230
231				/*
232				 * MD5 hashing with a twelve character salt
233				 * SHA-256/SHA-512 hash with a sixteen character salt.
234				 */
235
236				let saltBytes = this->getSaltBytes(hash == self::CRYPT_MD5 ? 12 : 16);
237
238				if typeof saltBytes != "string" {
239					throw new Exception("Unable to get random bytes for the salt");
240				}
241
242				return crypt(password, "$" . variant . "$"  . saltBytes . "$");
243
244			case self::CRYPT_DEFAULT:
245			case self::CRYPT_BLOWFISH:
246			case self::CRYPT_BLOWFISH_X:
247			case self::CRYPT_BLOWFISH_Y:
248			default:
249
250				/*
251				 * Blowfish hashing with a salt as follows: "$2a$", "$2x$" or "$2y$",
252				 * a two digit cost parameter, "$", and 22 characters from the alphabet
253				 * "./0-9A-Za-z". Using characters outside of this range in the salt
254				 * will cause crypt() to return a zero-length string. The two digit cost
255				 * parameter is the base-2 logarithm of the iteration count for the
256				 * underlying Blowfish-based hashing algorithm and must be in
257				 * range 04-31, values outside this range will cause crypt() to fail.
258				 */
259
260				let saltBytes = this->getSaltBytes(22);
261				if typeof saltBytes != "string" {
262					throw new Exception("Unable to get random bytes for the salt");
263				}
264
265				if workFactor < 4 {
266					let workFactor = 4;
267				} else {
268					if workFactor > 31 {
269						let workFactor = 31;
270					}
271				}
272
273				return crypt(password, "$2" . variant . "$" . sprintf("%02s", workFactor) . "$" . saltBytes . "$");
274		}
275
276		return "";
277	}
278
279	/**
280	 * Checks a plain text password and its hash version to check if the password matches
281	 */
282	public function checkHash(string password, string passwordHash, int maxPassLength = 0) -> boolean
283	{
284		char ch;
285		string cryptedHash;
286		int i, sum, cryptedLength, passwordLength;
287
288		if maxPassLength {
289			if maxPassLength > 0 && strlen(password) > maxPassLength {
290				return false;
291			}
292		}
293
294		let cryptedHash = (string) crypt(password, passwordHash);
295
296		let cryptedLength = strlen(cryptedHash),
297			passwordLength = strlen(passwordHash);
298
299		let cryptedHash .= passwordHash;
300
301		let sum = cryptedLength - passwordLength;
302		for i, ch in passwordHash {
303			let sum = sum | (cryptedHash[i] ^ ch);
304		}
305
306		return 0 === sum;
307	}
308
309	/**
310	 * Checks if a password hash is a valid bcrypt's hash
311	 */
312	public function isLegacyHash(string passwordHash) -> boolean
313	{
314		return starts_with(passwordHash, "$2a$");
315	}
316
317	/**
318	 * Generates a pseudo random token key to be used as input's name in a CSRF check
319	 */
320	public function getTokenKey() -> string
321	{
322		var dependencyInjector, session;
323
324		if null === this->_tokenKey {
325			let dependencyInjector = <DiInterface> this->_dependencyInjector;
326			if typeof dependencyInjector != "object" {
327				throw new Exception("A dependency injection container is required to access the 'session' service");
328			}
329
330			let this->_tokenKey = this->_random->base64Safe(this->_numberBytes);
331			let session = <SessionInterface> dependencyInjector->getShared("session");
332			session->set(this->_tokenKeySessionID, this->_tokenKey);
333		}
334
335		return this->_tokenKey;
336	}
337
338	/**
339	 * Generates a pseudo random token value to be used as input's value in a CSRF check
340	 */
341	public function getToken() -> string
342	{
343		var dependencyInjector, session;
344
345		if null === this->_token {
346			let this->_token = this->_random->base64Safe(this->_numberBytes);
347
348			let dependencyInjector = <DiInterface> this->_dependencyInjector;
349
350			if typeof dependencyInjector != "object" {
351				throw new Exception("A dependency injection container is required to access the 'session' service");
352			}
353
354			let session = <SessionInterface> dependencyInjector->getShared("session");
355			session->set(this->_tokenValueSessionID, this->_token);
356		}
357
358		return this->_token;
359	}
360
361	/**
362	 * Check if the CSRF token sent in the request is the same that the current in session
363	 */
364	public function checkToken(var tokenKey = null, var tokenValue = null, boolean destroyIfValid = true) -> boolean
365	{
366		var dependencyInjector, session, request, equals, userToken, knownToken;
367
368		let dependencyInjector = <DiInterface> this->_dependencyInjector;
369
370		if typeof dependencyInjector != "object" {
371			throw new Exception("A dependency injection container is required to access the 'session' service");
372		}
373
374		let session = <SessionInterface> dependencyInjector->getShared("session");
375
376		if !tokenKey {
377			let tokenKey = session->get(this->_tokenKeySessionID);
378		}
379
380		/**
381		 * If tokenKey does not exist in session return false
382		 */
383		if !tokenKey {
384			return false;
385		}
386
387		if !tokenValue {
388			let request = dependencyInjector->getShared("request");
389
390			/**
391			 * We always check if the value is correct in post
392			 */
393			let userToken = request->getPost(tokenKey);
394		} else {
395			let userToken = tokenValue;
396		}
397
398		/**
399		 * The value is the same?
400		 */
401		let knownToken = session->get(this->_tokenValueSessionID);
402		let equals = hash_equals(knownToken, userToken);
403
404		/**
405		 * Remove the key and value of the CSRF token in session
406		 */
407		if equals && destroyIfValid {
408			this->destroyToken();
409		}
410
411		return equals;
412	}
413
414	/**
415	 * Returns the value of the CSRF token in session
416	 */
417	public function getSessionToken() -> string
418	{
419		var dependencyInjector, session;
420
421		let dependencyInjector = <DiInterface> this->_dependencyInjector;
422
423		if typeof dependencyInjector != "object" {
424			throw new Exception("A dependency injection container is required to access the 'session' service");
425		}
426
427		let session = <SessionInterface> dependencyInjector->getShared("session");
428
429		return session->get(this->_tokenValueSessionID);
430	}
431
432	/**
433	 * Removes the value of the CSRF token and key from session
434	 */
435	public function destroyToken() -> <Security>
436	{
437		var dependencyInjector, session;
438
439		let dependencyInjector = <DiInterface> this->_dependencyInjector;
440
441		if typeof dependencyInjector != "object" {
442			throw new Exception("A dependency injection container is required to access the 'session' service");
443		}
444
445		let session = <SessionInterface> dependencyInjector->getShared("session");
446
447		session->remove(this->_tokenKeySessionID);
448		session->remove(this->_tokenValueSessionID);
449
450		let this->_token = null;
451		let this->_tokenKey = null;
452
453		return this;
454	}
455
456	/**
457	 * Computes a HMAC
458	 */
459	public function computeHmac(string data, string key, string algo, boolean raw = false) -> string
460	{
461		var hmac;
462
463		let hmac = hash_hmac(algo, data, key, raw);
464		if !hmac {
465			throw new Exception("Unknown hashing algorithm: %s" . algo);
466		}
467
468		return hmac;
469	}
470
471	/**
472 	 * Sets the default hash
473 	 */
474	public function setDefaultHash(int defaultHash) -> <Security>
475	{
476		let this->_defaultHash = defaultHash;
477
478		return this;
479	}
480
481	/**
482 	 * Returns the default hash
483 	 */
484	public function getDefaultHash() -> int | null
485	{
486		return this->_defaultHash;
487	}
488
489	/**
490	 * Testing for LibreSSL
491	 *
492	 * @deprecated Will be removed in 4.0.0
493	 */
494	public function hasLibreSsl() -> boolean
495	{
496		if !defined("OPENSSL_VERSION_TEXT") {
497			return false;
498		}
499
500		return strpos(OPENSSL_VERSION_TEXT, "LibreSSL") === 0;
501	}
502
503	/**
504	 * Getting OpenSSL or LibreSSL version.
505	 *
506	 * Parse OPENSSL_VERSION_TEXT because OPENSSL_VERSION_NUMBER is no use for LibreSSL.
507	 * This constant show not the current system openssl library version but version PHP was compiled with.
508	 *
509	 * @deprecated Will be removed in 4.0.0
510	 * @link https://bugs.php.net/bug.php?id=71143
511	 *
512	 * <code>
513	 * if ($security->getSslVersionNumber() >= 20105) {
514	 *     // ...
515	 * }
516	 * </code>
517	 */
518	public function getSslVersionNumber() -> int
519	{
520		var major, minor, patch, matches = null;
521
522		if !defined("OPENSSL_VERSION_TEXT") {
523			return 0;
524		}
525
526		preg_match("#(?:Libre|Open)SSL ([\d]+)\.([\d]+)(?:\.([\d]+))?#", OPENSSL_VERSION_TEXT, matches);
527
528		if !isset matches[2] {
529			return 0;
530		}
531
532		let major = (int) matches[1],
533			minor = (int) matches[2];
534
535		if isset matches[3] {
536			let patch = (int) matches[3];
537		}
538
539		return 10000 * major + 100 * minor + patch;
540	}
541}
542