1: <?php
2: /**
3: * This file contains classes implementing security manager feature.
4: *
5: * @author Qiang Xue <qiang.xue@gmail.com>
6: * @link http://www.yiiframework.com/
7: * @copyright 2008-2013 Yii Software LLC
8: * @license http://www.yiiframework.com/license/
9: */
10:
11: /**
12: * CSecurityManager provides private keys, hashing and encryption functions.
13: *
14: * CSecurityManager is used by Yii components and applications for security-related purpose.
15: * For example, it is used in cookie validation feature to prevent cookie data
16: * from being tampered.
17: *
18: * CSecurityManager is mainly used to protect data from being tampered and viewed.
19: * It can generate HMAC and encrypt the data. The private key used to generate HMAC
20: * is set by {@link setValidationKey ValidationKey}. The key used to encrypt data is
21: * specified by {@link setEncryptionKey EncryptionKey}. If the above keys are not
22: * explicitly set, random keys will be generated and used.
23: *
24: * To protected data with HMAC, call {@link hashData()}; and to check if the data
25: * is tampered, call {@link validateData()}, which will return the real data if
26: * it is not tampered. The algorithm used to generated HMAC is specified by
27: * {@link validation}.
28: *
29: * To encrypt and decrypt data, call {@link encrypt()} and {@link decrypt()}
30: * respectively, which uses 3DES encryption algorithm. Note, the PHP Mcrypt
31: * extension must be installed and loaded.
32: *
33: * CSecurityManager is a core application component that can be accessed via
34: * {@link CApplication::getSecurityManager()}.
35: *
36: * @property string $validationKey The private key used to generate HMAC.
37: * If the key is not explicitly set, a random one is generated and returned.
38: * @property string $encryptionKey The private key used to encrypt/decrypt data.
39: * If the key is not explicitly set, a random one is generated and returned.
40: * @property string $validation
41: *
42: * @author Qiang Xue <qiang.xue@gmail.com>
43: * @package system.base
44: * @since 1.0
45: */
46: class CSecurityManager extends CApplicationComponent
47: {
48: const STATE_VALIDATION_KEY='Yii.CSecurityManager.validationkey';
49: const STATE_ENCRYPTION_KEY='Yii.CSecurityManager.encryptionkey';
50:
51: /**
52: * @var array known minimum lengths per encryption algorithm
53: */
54: protected static $encryptionKeyMinimumLengths=array(
55: 'blowfish'=>4,
56: 'arcfour'=>5,
57: 'rc2'=>5,
58: );
59:
60: /**
61: * @var boolean if encryption key should be validated
62: */
63: public $validateEncryptionKey=true;
64:
65: /**
66: * @var string the name of the hashing algorithm to be used by {@link computeHMAC}.
67: * See {@link http://php.net/manual/en/function.hash-algos.php hash-algos} for the list of possible
68: * hash algorithms. Note that if you are using PHP 5.1.1 or below, you can only use 'sha1' or 'md5'.
69: *
70: * Defaults to 'sha1', meaning using SHA1 hash algorithm.
71: * @since 1.1.3
72: */
73: public $hashAlgorithm='sha1';
74: /**
75: * @var mixed the name of the crypt algorithm to be used by {@link encrypt} and {@link decrypt}.
76: * This will be passed as the first parameter to {@link http://php.net/manual/en/function.mcrypt-module-open.php mcrypt_module_open}.
77: *
78: * This property can also be configured as an array. In this case, the array elements will be passed in order
79: * as parameters to mcrypt_module_open. For example, <code>array('rijndael-128', '', 'ofb', '')</code>.
80: *
81: * Defaults to AES
82: *
83: * Note: MCRYPT_RIJNDAEL_192 and MCRYPT_RIJNDAEL_256 are *not* AES-192 and AES-256. The numbers of the MCRYPT_RIJNDAEL
84: * constants refer to the block size, whereas the numbers of the AES variants refer to the key length. AES is Rijndael
85: * with a block size of 128 bits and a key length of 128 bits, 192 bits or 256 bits. So to use AES in Mcrypt, you need
86: * MCRYPT_RIJNDAEL_128 and a key with 16 bytes (AES-128), 24 bytes (AES-192) or 32 bytes (AES-256). The other two
87: * Rijndael variants in Mcrypt should be avoided, because they're not standardized and have been analyzed much less
88: * than AES.
89: *
90: * @since 1.1.3
91: */
92: public $cryptAlgorithm='rijndael-128';
93:
94: private $_validationKey;
95: private $_encryptionKey;
96: private $_mbstring;
97:
98: public function init()
99: {
100: parent::init();
101: $this->_mbstring=extension_loaded('mbstring');
102: }
103:
104: /**
105: * @return string a randomly generated private key.
106: * @deprecated in favor of {@link generateRandomString()} since 1.1.14. Never use this method.
107: */
108: protected function generateRandomKey()
109: {
110: return $this->generateRandomString(32);
111: }
112:
113: /**
114: * @return string the private key used to generate HMAC.
115: * If the key is not explicitly set, a random one is generated and returned.
116: * @throws CException in case random string cannot be generated.
117: */
118: public function getValidationKey()
119: {
120: if($this->_validationKey!==null)
121: return $this->_validationKey;
122: else
123: {
124: if(($key=Yii::app()->getGlobalState(self::STATE_VALIDATION_KEY))!==null)
125: $this->setValidationKey($key);
126: else
127: {
128: if(($key=$this->generateRandomString(32,true))===false)
129: if(($key=$this->generateRandomString(32,false))===false)
130: throw new CException(Yii::t('yii',
131: 'CSecurityManager::generateRandomString() cannot generate random string in the current environment.'));
132: $this->setValidationKey($key);
133: Yii::app()->setGlobalState(self::STATE_VALIDATION_KEY,$key);
134: }
135: return $this->_validationKey;
136: }
137: }
138:
139: /**
140: * @param string $value the key used to generate HMAC
141: * @throws CException if the key is empty
142: */
143: public function setValidationKey($value)
144: {
145: if(!empty($value))
146: $this->_validationKey=$value;
147: else
148: throw new CException(Yii::t('yii','CSecurityManager.validationKey cannot be empty.'));
149: }
150:
151: /**
152: * @return string the private key used to encrypt/decrypt data.
153: * If the key is not explicitly set, a random one is generated and returned.
154: * @throws CException in case random string cannot be generated.
155: */
156: public function getEncryptionKey()
157: {
158: if($this->_encryptionKey!==null)
159: return $this->_encryptionKey;
160: else
161: {
162: if(($key=Yii::app()->getGlobalState(self::STATE_ENCRYPTION_KEY))!==null)
163: $this->setEncryptionKey($key);
164: else
165: {
166: if(($key=$this->generateRandomString(32,true))===false)
167: if(($key=$this->generateRandomString(32,false))===false)
168: throw new CException(Yii::t('yii',
169: 'CSecurityManager::generateRandomString() cannot generate random string in the current environment.'));
170: $this->setEncryptionKey($key);
171: Yii::app()->setGlobalState(self::STATE_ENCRYPTION_KEY,$key);
172: }
173: return $this->_encryptionKey;
174: }
175: }
176:
177: /**
178: * @param string $value the key used to encrypt/decrypt data.
179: * @throws CException if the key is empty
180: */
181: public function setEncryptionKey($value)
182: {
183: $this->validateEncryptionKey($value);
184: $this->_encryptionKey=$value;
185: }
186:
187: /**
188: * This method has been deprecated since version 1.1.3.
189: * Please use {@link hashAlgorithm} instead.
190: * @return string -
191: * @deprecated
192: */
193: public function getValidation()
194: {
195: return $this->hashAlgorithm;
196: }
197:
198: /**
199: * This method has been deprecated since version 1.1.3.
200: * Please use {@link hashAlgorithm} instead.
201: * @param string $value -
202: * @deprecated
203: */
204: public function setValidation($value)
205: {
206: $this->hashAlgorithm=$value;
207: }
208:
209: /**
210: * Encrypts data.
211: * @param string $data data to be encrypted.
212: * @param string $key the decryption key. This defaults to null, meaning using {@link getEncryptionKey EncryptionKey}.
213: * @return string the encrypted data
214: * @throws CException if PHP Mcrypt extension is not loaded or key is invalid
215: */
216: public function encrypt($data,$key=null)
217: {
218: if($key===null)
219: $key=$this->getEncryptionKey();
220: $this->validateEncryptionKey($key);
221: $module=$this->openCryptModule();
222: srand();
223: $iv=mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND);
224: mcrypt_generic_init($module,$key,$iv);
225: $encrypted=$iv.mcrypt_generic($module,$data);
226: mcrypt_generic_deinit($module);
227: mcrypt_module_close($module);
228: return $encrypted;
229: }
230:
231: /**
232: * Decrypts data
233: * @param string $data data to be decrypted.
234: * @param string $key the decryption key. This defaults to null, meaning using {@link getEncryptionKey EncryptionKey}.
235: * @return string the decrypted data
236: * @throws CException if PHP Mcrypt extension is not loaded or key is invalid
237: */
238: public function decrypt($data,$key=null)
239: {
240: if($key===null)
241: $key=$this->getEncryptionKey();
242: $this->validateEncryptionKey($key);
243: $module=$this->openCryptModule();
244: $ivSize=mcrypt_enc_get_iv_size($module);
245: $iv=$this->substr($data,0,$ivSize);
246: mcrypt_generic_init($module,$key,$iv);
247: $decrypted=mdecrypt_generic($module,$this->substr($data,$ivSize,$this->strlen($data)));
248: mcrypt_generic_deinit($module);
249: mcrypt_module_close($module);
250: return rtrim($decrypted,"\0");
251: }
252:
253: /**
254: * Opens the mcrypt module with the configuration specified in {@link cryptAlgorithm}.
255: * @throws CException if failed to initialize the mcrypt module or PHP mcrypt extension
256: * @return resource the mycrypt module handle.
257: * @since 1.1.3
258: */
259: protected function openCryptModule()
260: {
261: if(extension_loaded('mcrypt'))
262: {
263: if(is_array($this->cryptAlgorithm))
264: $module=@call_user_func_array('mcrypt_module_open',$this->cryptAlgorithm);
265: else
266: $module=@mcrypt_module_open($this->cryptAlgorithm,'', MCRYPT_MODE_CBC,'');
267:
268: if($module===false)
269: throw new CException(Yii::t('yii','Failed to initialize the mcrypt module.'));
270:
271: return $module;
272: }
273: else
274: throw new CException(Yii::t('yii','CSecurityManager requires PHP mcrypt extension to be loaded in order to use data encryption feature.'));
275: }
276:
277: /**
278: * Prefixes data with an HMAC.
279: * @param string $data data to be hashed.
280: * @param string $key the private key to be used for generating HMAC. Defaults to null, meaning using {@link validationKey}.
281: * @return string data prefixed with HMAC
282: */
283: public function hashData($data,$key=null)
284: {
285: return $this->computeHMAC($data,$key).$data;
286: }
287:
288: /**
289: * Validates if data is tampered.
290: * @param string $data data to be validated. The data must be previously
291: * generated using {@link hashData()}.
292: * @param string $key the private key to be used for generating HMAC. Defaults to null, meaning using {@link validationKey}.
293: * @return string the real data with HMAC stripped off. False if the data
294: * is tampered.
295: */
296: public function validateData($data,$key=null)
297: {
298: if (!is_string($data))
299: return false;
300:
301: $len=$this->strlen($this->computeHMAC('test'));
302: if($this->strlen($data)>=$len)
303: {
304: $hmac=$this->substr($data,0,$len);
305: $data2=$this->substr($data,$len,$this->strlen($data));
306: return $this->compareString($hmac,$this->computeHMAC($data2,$key))?$data2:false;
307: }
308: else
309: return false;
310: }
311:
312: /**
313: * Computes the HMAC for the data with {@link getValidationKey validationKey}. This method has been made public
314: * since 1.1.14.
315: * @param string $data data to be generated HMAC.
316: * @param string|null $key the private key to be used for generating HMAC. Defaults to null, meaning using
317: * {@link validationKey} value.
318: * @param string|null $hashAlgorithm the name of the hashing algorithm to be used.
319: * See {@link http://php.net/manual/en/function.hash-algos.php hash-algos} for the list of possible
320: * hash algorithms. Note that if you are using PHP 5.1.1 or below, you can only use 'sha1' or 'md5'.
321: * Defaults to null, meaning using {@link hashAlgorithm} value.
322: * @return string the HMAC for the data.
323: * @throws CException on unsupported hash algorithm given.
324: */
325: public function computeHMAC($data,$key=null,$hashAlgorithm=null)
326: {
327: if($key===null)
328: $key=$this->getValidationKey();
329: if($hashAlgorithm===null)
330: $hashAlgorithm=$this->hashAlgorithm;
331:
332: if(function_exists('hash_hmac'))
333: return hash_hmac($hashAlgorithm,$data,$key);
334:
335: if(0===strcasecmp($hashAlgorithm,'sha1'))
336: {
337: $pack='H40';
338: $func='sha1';
339: }
340: elseif(0===strcasecmp($hashAlgorithm,'md5'))
341: {
342: $pack='H32';
343: $func='md5';
344: }
345: else
346: {
347: throw new CException(Yii::t('yii','Only SHA1 and MD5 hashing algorithms are supported when using PHP 5.1.1 or below.'));
348: }
349: if($this->strlen($key)>64)
350: $key=pack($pack,$func($key));
351: if($this->strlen($key)<64)
352: $key=str_pad($key,64,chr(0));
353: $key=$this->substr($key,0,64);
354: return $func((str_repeat(chr(0x5C), 64) ^ $key) . pack($pack, $func((str_repeat(chr(0x36), 64) ^ $key) . $data)));
355: }
356:
357: /**
358: * Generate a random ASCII string. Generates only [0-9a-zA-z_~] characters which are all
359: * transparent in raw URL encoding.
360: * @param integer $length length of the generated string in characters.
361: * @param boolean $cryptographicallyStrong set this to require cryptographically strong randomness.
362: * @return string|boolean random string or false in case it cannot be generated.
363: * @since 1.1.14
364: */
365: public function generateRandomString($length,$cryptographicallyStrong=true)
366: {
367: if(($randomBytes=$this->generateRandomBytes($length+2,$cryptographicallyStrong))!==false)
368: return strtr($this->substr(base64_encode($randomBytes),0,$length),array('+'=>'_','/'=>'~'));
369: return false;
370: }
371:
372: /**
373: * Generates a string of random bytes.
374: * @param integer $length number of random bytes to be generated.
375: * @param boolean $cryptographicallyStrong whether to fail if a cryptographically strong
376: * result cannot be generated. The method attempts to read from a cryptographically strong
377: * pseudorandom number generator (CS-PRNG), see
378: * {@link https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator#Requirements Wikipedia}.
379: * However, in some runtime environments, PHP has no access to a CS-PRNG, in which case
380: * the method returns false if $cryptographicallyStrong is true. When $cryptographicallyStrong is false,
381: * the method always returns a pseudorandom result but may fall back to using {@link generatePseudoRandomBlock}.
382: * This method does not guarantee that entropy, from sources external to the CS-PRNG, was mixed into
383: * the CS-PRNG state between each successive call. The caller can therefore expect non-blocking
384: * behavior, unlike, for example, reading from /dev/random on Linux, see
385: * {@link http://eprint.iacr.org/2006/086.pdf Gutterman et al 2006}.
386: * @return boolean|string generated random binary string or false on failure.
387: * @since 1.1.14
388: */
389: public function generateRandomBytes($length,$cryptographicallyStrong=true)
390: {
391: $bytes='';
392: if(function_exists('openssl_random_pseudo_bytes'))
393: {
394: $bytes=openssl_random_pseudo_bytes($length,$strong);
395: if($this->strlen($bytes)>=$length && ($strong || !$cryptographicallyStrong))
396: return $this->substr($bytes,0,$length);
397: }
398:
399: if(function_exists('mcrypt_create_iv') &&
400: ($bytes=mcrypt_create_iv($length, MCRYPT_DEV_URANDOM))!==false &&
401: $this->strlen($bytes)>=$length)
402: {
403: return $this->substr($bytes,0,$length);
404: }
405:
406: if(($file=@fopen('/dev/urandom','rb'))!==false &&
407: ($bytes=@fread($file,$length))!==false &&
408: (fclose($file) || true) &&
409: $this->strlen($bytes)>=$length)
410: {
411: return $this->substr($bytes,0,$length);
412: }
413:
414: $i=0;
415: while($this->strlen($bytes)<$length &&
416: ($byte=$this->generateSessionRandomBlock())!==false &&
417: ++$i<3)
418: {
419: $bytes.=$byte;
420: }
421: if($this->strlen($bytes)>=$length)
422: return $this->substr($bytes,0,$length);
423:
424: if ($cryptographicallyStrong)
425: return false;
426:
427: while($this->strlen($bytes)<$length)
428: $bytes.=$this->generatePseudoRandomBlock();
429: return $this->substr($bytes,0,$length);
430: }
431:
432: /**
433: * Generate a pseudo random block of data using several sources. On some systems this may be a bit
434: * better than PHP's {@link mt_rand} built-in function, which is not really random.
435: * @return string of 64 pseudo random bytes.
436: * @since 1.1.14
437: */
438: public function generatePseudoRandomBlock()
439: {
440: $bytes='';
441:
442: if (function_exists('openssl_random_pseudo_bytes')
443: && ($bytes=openssl_random_pseudo_bytes(512))!==false
444: && $this->strlen($bytes)>=512)
445: {
446: return $this->substr($bytes,0,512);
447: }
448:
449: for($i=0;$i<32;++$i)
450: $bytes.=pack('S',mt_rand(0,0xffff));
451:
452: // On UNIX and UNIX-like operating systems the numerical values in `ps`, `uptime` and `iostat`
453: // ought to be fairly unpredictable. Gather the non-zero digits from those.
454: foreach(array('ps','uptime','iostat') as $command) {
455: @exec($command,$commandResult,$retVal);
456: if(is_array($commandResult) && !empty($commandResult) && $retVal==0)
457: $bytes.=preg_replace('/[^1-9]/','',implode('',$commandResult));
458: }
459:
460: // Gather the current time's microsecond part. Note: this is only a source of entropy on
461: // the first call! If multiple calls are made, the entropy is only as much as the
462: // randomness in the time between calls.
463: $bytes.=$this->substr(microtime(),2,6);
464:
465: // Concatenate everything gathered, mix it with sha512. hash() is part of PHP core and
466: // enabled by default but it can be disabled at compile time but we ignore that possibility here.
467: return hash('sha512',$bytes,true);
468: }
469:
470: /**
471: * Get random bytes from the system entropy source via PHP session manager.
472: * @return boolean|string 20-byte random binary string or false on error.
473: * @since 1.1.14
474: */
475: public function generateSessionRandomBlock()
476: {
477: ini_set('session.entropy_length',20);
478: if(ini_get('session.entropy_length')!=20)
479: return false;
480:
481: // These calls are (supposed to be, according to PHP manual) safe even if
482: // there is already an active session for the calling script.
483: @session_start();
484: @session_regenerate_id();
485:
486: $bytes=session_id();
487: if(!$bytes)
488: return false;
489:
490: // $bytes has 20 bytes of entropy but the session manager converts the binary
491: // random bytes into something readable. We have to convert that back.
492: // SHA-1 should do it without losing entropy.
493: return sha1($bytes,true);
494: }
495:
496: /**
497: * Returns the length of the given string.
498: * If available uses the multibyte string function mb_strlen.
499: * @param string $string the string being measured for length
500: * @return integer the length of the string
501: */
502: private function strlen($string)
503: {
504: return $this->_mbstring ? mb_strlen($string,'8bit') : strlen($string);
505: }
506:
507: /**
508: * Returns the portion of string specified by the start and length parameters.
509: * If available uses the multibyte string function mb_substr
510: * @param string $string the input string. Must be one character or longer.
511: * @param integer $start the starting position
512: * @param integer $length the desired portion length
513: * @return string the extracted part of string, or FALSE on failure or an empty string.
514: */
515: private function substr($string,$start,$length)
516: {
517: return $this->_mbstring ? mb_substr($string,$start,$length,'8bit') : substr($string,$start,$length);
518: }
519:
520: /**
521: * Checks if a key is valid for {@link cryptAlgorithm}.
522: * @param string $key the key to check
523: * @return boolean the validation result
524: * @throws CException if the supported key lengths of the cipher are unknown
525: */
526: protected function validateEncryptionKey($key)
527: {
528: if(is_string($key))
529: {
530: $supportedKeyLengths=mcrypt_module_get_supported_key_sizes($this->cryptAlgorithm);
531:
532: if($supportedKeyLengths)
533: {
534: if(!in_array($this->strlen($key),$supportedKeyLengths)) {
535: throw new CException(Yii::t('yii','Encryption key length can be {keyLengths}',array('{keyLengths}'=>implode(',',$supportedKeyLengths).'.')));
536: }
537: }
538: elseif(isset(self::$encryptionKeyMinimumLengths[$this->cryptAlgorithm]))
539: {
540: $minLength=self::$encryptionKeyMinimumLengths[$this->cryptAlgorithm];
541: $maxLength=mcrypt_module_get_algo_key_size($this->cryptAlgorithm);
542: if($this->strlen($key)<$minLength || $this->strlen($key)>$maxLength)
543: throw new CException(Yii::t('yii','Encryption key length must be between {minLength} and {maxLength}.',array('{minLength}'=>$minLength,'{maxLength}'=>$maxLength)));
544: }
545: else
546: throw new CException(Yii::t('yii','Failed to validate key. Supported key lengths of cipher not known.'));
547: }
548: else
549: throw new CException(Yii::t('yii','Encryption key should be a string.'));
550: }
551:
552: /**
553: * Decrypts legacy ciphertext which was produced by the old, broken implementation of encrypt().
554: * @deprecated use only to convert data encrypted prior to 1.1.16
555: * @param string $data data to be decrypted.
556: * @param string $key the decryption key. This defaults to null, meaning the key should be loaded from persistent storage.
557: * @param string|array $cipher the algorithm to be used
558: * @return string the decrypted data
559: * @throws CException if PHP Mcrypt extension is not loaded
560: * @throws CException if the key is missing
561: */
562: public function legacyDecrypt($data,$key=null,$cipher='des')
563: {
564: if (!$key)
565: {
566: $key=Yii::app()->getGlobalState(self::STATE_ENCRYPTION_KEY);
567: if(!$key)
568: throw new CException(Yii::t('yii','No encryption key specified.'));
569: }
570:
571: if(extension_loaded('mcrypt'))
572: {
573: if(is_array($cipher))
574: $module=@call_user_func_array('mcrypt_module_open',$cipher);
575: else
576: $module=@mcrypt_module_open($cipher,'', MCRYPT_MODE_CBC,'');
577:
578: if($module===false)
579: throw new CException(Yii::t('yii','Failed to initialize the mcrypt module.'));
580: }
581: else
582: throw new CException(Yii::t('yii','CSecurityManager requires PHP mcrypt extension to be loaded in order to use data encryption feature.'));
583:
584: $derivedKey=$this->substr(md5($key),0,mcrypt_enc_get_key_size($module));
585: $ivSize=mcrypt_enc_get_iv_size($module);
586: $iv=$this->substr($data,0,$ivSize);
587: mcrypt_generic_init($module,$derivedKey,$iv);
588: $decrypted=mdecrypt_generic($module,$this->substr($data,$ivSize,$this->strlen($data)));
589: mcrypt_generic_deinit($module);
590: mcrypt_module_close($module);
591: return rtrim($decrypted,"\0");
592: }
593:
594: /**
595: * Performs string comparison using timing attack resistant approach.
596: * @see http://codereview.stackexchange.com/questions/13512
597: * @param string $expected string to compare.
598: * @param string $actual user-supplied string.
599: * @return boolean whether strings are equal.
600: */
601: public function compareString($expected,$actual)
602: {
603: $expected.="\0";
604: $actual.="\0";
605: $expectedLength=$this->strlen($expected);
606: $actualLength=$this->strlen($actual);
607: $diff=$expectedLength-$actualLength;
608: for($i=0;$i<$actualLength;$i++)
609: $diff|=(ord($actual[$i])^ord($expected[$i%$expectedLength]));
610: return $diff===0;
611: }
612: }
613: