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