From c56b1bdb0065b752930f74eabe20f985722268ac Mon Sep 17 00:00:00 2001 From: xue <> Date: Sat, 11 Feb 2006 21:30:59 +0000 Subject: Added TSecurityManager and state encryption method. --- .gitattributes | 1 + buildscripts/phpbuilder/files.txt | 3 +- framework/Security/TSecurityManager.php | 249 +++++++++++++++++++++++++++++++ framework/TApplication.php | 31 +++- framework/Web/Services/TPageService.php | 26 ---- framework/Web/UI/TPage.php | 81 ++++++++-- framework/Web/UI/TPageStatePersister.php | 116 +++++--------- requirements/index.php | 1 + requirements/messages-zh.txt | 3 +- requirements/messages.txt | 3 +- 10 files changed, 392 insertions(+), 122 deletions(-) create mode 100644 framework/Security/TSecurityManager.php diff --git a/.gitattributes b/.gitattributes index 6656423b..9a66425d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -580,6 +580,7 @@ framework/Log/TLogger.php -text framework/Security/TAuthManager.php -text framework/Security/TAuthorizationRule.php -text framework/Security/TMembershipManager.php -text +framework/Security/TSecurityManager.php -text framework/Security/TUserManager.php -text framework/TApplication.php -text framework/TComponent.php -text diff --git a/buildscripts/phpbuilder/files.txt b/buildscripts/phpbuilder/files.txt index 5868fc86..143f7404 100644 --- a/buildscripts/phpbuilder/files.txt +++ b/buildscripts/phpbuilder/files.txt @@ -21,17 +21,18 @@ Web/THttpRequest.php Web/THttpResponse.php Web/THttpSession.php Security/TAuthorizationRule.php +Security/TSecurityManager.php Web/Services/TPageService.php Web/UI/THtmlWriter.php Web/UI/TTemplateManager.php Web/UI/TThemeManager.php Web/UI/TAssetManager.php -Web/UI/TPageStatePersister.php Web/UI/TControl.php Web/UI/TTemplateControl.php Web/UI/TForm.php Web/UI/TClientScriptManager.php Web/UI/TPage.php +Web/UI/TPageStatePersister.php Web/UI/WebControls/TFont.php Web/UI/WebControls/TStyle.php Web/UI/WebControls/TWebControl.php diff --git a/framework/Security/TSecurityManager.php b/framework/Security/TSecurityManager.php new file mode 100644 index 00000000..bc77c1b6 --- /dev/null +++ b/framework/Security/TSecurityManager.php @@ -0,0 +1,249 @@ + + * @link http://www.pradosoft.com/ + * @copyright Copyright © 2005 PradoSoft + * @license http://www.pradosoft.com/license/ + * @version $Revision: $ $Date: $ + * @package System.Security + */ + +/** + * TSecurityManager class + * + * TSecurityManager provides private keys, hashing and encryption + * functionalities that may be used by other PRADO components, + * such as viewstate persister, cookies. + * + * TSecurityManager is mainly used to protect data from being tampered + * and viewed. It can generate HMAC and encrypt the data. + * The private key used to generate HMAC is set by {@link setValidationKey ValidationKey}. + * The key used to encrypt data is specified by {@link setEncryptionKey EncryptionKey}. + * If the above keys are not explicitly set, random keys will be generated + * and used. + * + * To prefix data with an HMAC, call {@link hashData()}. + * To validate if data is tampered, call {@link validateData()}, which will + * return the real data if it is not tampered. + * The algorithm used to generated HMAC is specified by {@link setValidation Validation}. + * + * To encrypt and decrypt data, call {@link encrypt()} and {@link decrypt()} + * respectively. The encryption algorithm can be set by {@link setEncryption Encryption}. + * + * Note, to use encryption, the PHP Mcrypt extension must be loaded. + * + * @author Qiang Xue + * @version $Revision: $ $Date: $ + * @package System.Security + * @since 3.0 + */ +class TSecurityManager extends TModule +{ + const STATE_VALIDATION_KEY='prado:securitymanager:validationkey'; + const STATE_ENCRYPTION_KEY='prado:securitymanager:encryptionkey'; + private $_validationKey; + private $_encryptionKey; + private $_validation='SHA1'; + private $_encryption='3DES'; + + /** + * Initializes the module. + * The security module is registered with the application. + * @param TXmlElement initial module configuration + */ + public function init($config) + { + $this->getApplication()->setSecurityManager($this); + } + + /** + * Generates a random key. + */ + protected function generateRandomKey() + { + $v1=rand(); + $v2=rand(); + $v3=rand(); + return md5("$v1$v2$v3"); + } + + /** + * @return string the private key used to generate HMAC. + * If the key is not explicitly set, a random one is generated and used. + */ + public function getValidationKey() + { + if(empty($this->_validationKey)) + { + if(($this->_validationKey=$this->getApplication()->getGlobalState(self::STATE_VALIDATION_KEY))===null) + { + $this->_validationKey=$this->generateRandomKey(); + $this->getApplication()->setGlobalState(self::STATE_VALIDATION_KEY,$this->_validationKey,null); + } + } + return $this->_validationKey; + } + + /** + * @param string the key used to generate HMAC + * @throws TInvalidDataValueException if the key is shorter than 8 characters. + */ + public function setValidationKey($value) + { + if(strlen($value)<8) + throw new TInvalidDataValueException('securitymanager_validationkey_invalid'); + $this->_validationKey=$value; + } + + /** + * @return string the private key used to encrypt/decrypt data. + * If the key is not explicitly set, a random one is generated and used. + */ + public function getEncryptionKey() + { + if(empty($this->_encryptionKey)) + { + if(($this->_encryptionKey=$this->getApplication()->getGlobalState(self::STATE_ENCRYPTION_KEY))===null) + { + $this->_encryptionKey=$this->generateRandomKey(); + $this->getApplication()->setGlobalState(self::STATE_ENCRYPTION_KEY,$this->_encryptionKey,null); + } + } + return $this->_encryptionKey; + } + + /** + * @param string the key used to encrypt/decrypt data. + * @throws TInvalidDataValueException if the key is shorter than 8 characters. + */ + public function setEncryptionKey($value) + { + if(strlen($value)<8) + throw new TInvalidDataValueException('securitymanager_encryptionkey_invalid'); + $this->_encryptionKey=$value; + } + + /** + * @return string hashing algorithm used to generate HMAC. Defaults to 'SHA1'. + */ + public function getValidation() + { + return $this->_validation; + } + + /** + * @param string hashing algorithm used to generate HMAC. Valid values include 'SHA1' and 'MD5'. + */ + public function setValidation($value) + { + $this->_validation=TPropertyValue::ensureEnum($value,'SHA1','MD5'); + } + + /** + * @return string the algorithm used to encrypt/decrypt data. Defaults to '3DES'. + */ + public function getEncryption() + { + return $this->_encryption; + } + + /** + * @throws TNotSupportedException Do not call this method presently. + */ + public function setEncryption($value) + { + throw new TNotSupportedException('Currently only 3DES encryption is supported'); + } + + /** + * Encrypts data with {@link getEncryptionKey EncryptionKey}. + * @param string data to be encrypted. + * @return string the encrypted data + * @throws TNotSupportedException if PHP Mcrypt extension is not loaded + */ + public function encrypt($data) + { + if(function_exists('mcrypt_encrypt')) + { + return mcrypt_encrypt(MCRYPT_3DES, $this->getEncryptionKey(), $data, MCRYPT_MODE_CBC); + } + else + throw new TNotSupportedException('securitymanager_mcryptextension_required'); + } + + /** + * Decrypts data with {@link getEncryptionKey EncryptionKey}. + * @param string data to be decrypted. + * @return string the decrypted data + * @throws TNotSupportedException if PHP Mcrypt extension is not loaded + */ + public function decrypt($data) + { + if(function_exists('mcrypt_decrypt')) + { + return mcrypt_decrypt(MCRYPT_3DES, $this->getEncryptionKey(), $data, MCRYPT_MODE_CBC); + } + else + throw new TNotSupportedException('securitymanager_mcryptextension_required'); + } + + /** + * Prefixes data with an HMAC. + * @param string data to be hashed. + * @return string data prefixed with HMAC + */ + public function hashData($data) + { + $hmac=$this->computeHMAC($data); + return $hmac.$data; + } + + /** + * Validates if data is tampered. + * @param string data to be validated. The data must be previously + * generated using {@link hashData()}. + * @return string the real data with HMAC stripped off. Null if the data + * is tampered. + */ + public function validateData($data) + { + $len=$this->_validation==='SHA1'?40:32; + if(strlen($data)>=$len) + { + $hmac=substr($data,0,$len); + $data2=substr($data,$len); + return $hmac===$this->computeHMAC($data2)?$data2:null; + } + else + return null; + } + + /** + * Computes the HMAC for the data with {@link getValidationKey ValidationKey}. + * @param string data to be generated HMAC + * @return string the HMAC for the data + */ + protected function computeHMAC($data) + { + if($this->_validation==='SHA1') + { + $pack='H40'; + $func='sha1'; + } + else + { + $pack='H32'; + $func='md5'; + } + $key=$this->getValidationKey(); + if (strlen($key) > 64) + $key = pack($pack, $func($key)); + if (strlen($key) < 64) + $key = str_pad($key, 64, chr(0)); + return $func((str_repeat(chr(0x5C), 64) ^ substr($key, 0, 64)) . pack($pack, $func((str_repeat(chr(0x36), 64) ^ substr($key, 0, 64)) . $data))); + } +} + +?> \ No newline at end of file diff --git a/framework/TApplication.php b/framework/TApplication.php index 1a982488..2f25925d 100644 --- a/framework/TApplication.php +++ b/framework/TApplication.php @@ -30,6 +30,10 @@ require_once(PRADO_DIR.'/Web/THttpSession.php'); * Includes TAuthorizationRule class */ require_once(PRADO_DIR.'/Security/TAuthorizationRule.php'); +/** + * Includes TSecurityManager class + */ +require_once(PRADO_DIR.'/Security/TSecurityManager.php'); /** * Includes TPageService class (default service) */ @@ -240,12 +244,14 @@ class TApplication extends TComponent * @var IUser user instance, could be null */ private $_user=null; - /** * @var TGlobalization module, could be null */ private $_globalization=null; - + /** + * @var TSecurityManager security manager module + */ + private $_security=null; /** * @var TAuthorizationRuleCollection collection of authorization rules */ @@ -650,6 +656,27 @@ class TApplication extends TComponent $this->_errorHandler=$handler; } + /** + * @return TSecurityManager the security manager module + */ + public function getSecurityManager() + { + if(!$this->_security) + { + $this->_security=new TSecurityManager; + $this->_security->init(null); + } + return $this->_security; + } + + /** + * @param TSecurityManager the security manager module + */ + public function setSecurityManager(TSecurityManager $sm) + { + $this->_security=$sm; + } + /** * @return IStatePersister application state persister */ diff --git a/framework/Web/Services/TPageService.php b/framework/Web/Services/TPageService.php index 463f2dec..393194a1 100644 --- a/framework/Web/Services/TPageService.php +++ b/framework/Web/Services/TPageService.php @@ -17,7 +17,6 @@ Prado::using('System.Web.UI.TPage'); Prado::using('System.Web.UI.TTemplateManager'); Prado::using('System.Web.UI.TThemeManager'); Prado::using('System.Web.UI.TAssetManager'); -Prado::using('System.Web.UI.TPageStatePersister'); /** * TPageService class. @@ -134,10 +133,6 @@ class TPageService extends TService * @var TTemplateManager template manager */ private $_templateManager=null; - /** - * @var IStatePersister page state persister - */ - private $_pageStatePersister=null; /** * Initializes the service. @@ -343,27 +338,6 @@ class TPageService extends TService $this->_themeManager=$value; } - /** - * @return IStatePersister page state persister - */ - public function getPageStatePersister() - { - if(!$this->_pageStatePersister) - { - $this->_pageStatePersister=new TPageStatePersister; - $this->_pageStatePersister->init(null); - } - return $this->_pageStatePersister; - } - - /** - * @param IStatePersister page state persister - */ - public function setPageStatePersister(IStatePersister $value) - { - $this->_pageStatePersister=$value; - } - /** * @return string the requested page path */ diff --git a/framework/Web/UI/TPage.php b/framework/Web/UI/TPage.php index 11f89ab5..78d9115e 100644 --- a/framework/Web/UI/TPage.php +++ b/framework/Web/UI/TPage.php @@ -129,6 +129,11 @@ class TPage extends TTemplateControl private $_isCrossPagePostBack=false; private $_previousPagePath=''; + private $_statePersisterClass='System.Web.UI.TPageStatePersister'; + private $_statePersister=null; + private $_enableStateHMAC=true; + private $_enableStateEncryption=false; + /** * Constructor. * Sets the page object to itself. @@ -513,14 +518,6 @@ class TPage extends TTemplateControl return $this->_postData!==null; } - /** - * @return IStatePersister page state persister - */ - protected function getPageStatePersister() - { - return $this->getService()->getPageStatePersister(); - } - /** * This method is invoked when control state is to be saved. * You can override this method to do last step state saving. @@ -548,7 +545,7 @@ class TPage extends TTemplateControl */ protected function loadPageState() { - $state=$this->getPageStatePersister()->load(); + $state=$this->getStatePersister()->load(); $this->loadStateRecursive($state,$this->getEnableViewState()); } @@ -558,7 +555,7 @@ class TPage extends TTemplateControl protected function savePageState() { $state=&$this->saveStateRecursive($this->getEnableViewState()); - $this->getPageStatePersister()->save($state); + $this->getStatePersister()->save($state); } /** @@ -806,6 +803,70 @@ class TPage extends TTemplateControl { $this->setViewState('Title',$value,''); } + + public function getStatePersisterClass() + { + return $this->_statePersisterClass; + } + + public function setStatePersisterClass($value) + { + $this->_statePersisterClass=$value; + } + + public function getStatePersister() + { + if($this->_statePersister===null) + { + $this->_statePersister=Prado::createComponent($this->_statePersisterClass); + if(!($this->_statePersister instanceof IPageStatePersister)) + throw new TInvalidDataTypeException('page_statepersister_invalid'); + $this->_statePersister->setPage($this); + } + return $this->_statePersister; + } + + public function getEnableStateHMAC() + { + return $this->_enableStateHMAC; + } + + public function setEnableStateHMAC($value) + { + $this->_enableStateHMAC=TPropertyValue::ensureBoolean($value); + } + + public function getEnableStateEncryption() + { + return $this->_enableStateEncryption; + } + + public function setEnableStateEncryption($value) + { + $this->_enableStateEncryption=TPropertyValue::ensureBoolean($value); + } +} + +interface IPageStatePersister +{ + /** + * @param TPage the page that this persister works for + */ + public function getPage(); + /** + * @param TPage the page that this persister works for + */ + public function setPage(TPage $page); + /** + * Saves state to persistent storage. + * @param string state to be stored + */ + public function save($state); + /** + * Loads page state from persistent storage + * @return string the restored state + */ + public function load(); } ?> \ No newline at end of file diff --git a/framework/Web/UI/TPageStatePersister.php b/framework/Web/UI/TPageStatePersister.php index 4ece9a09..746d93c8 100644 --- a/framework/Web/UI/TPageStatePersister.php +++ b/framework/Web/UI/TPageStatePersister.php @@ -14,36 +14,38 @@ * TPageStatePersister class * * TPageStatePersister implements a page state persistent method based on - * form hidden fields. It is the default way of storing page state. - * Should you need to access this module, you may get it via - * {@link TPageService::getPageStatePersister}. + * form hidden fields. * - * TPageStatePersister uses a private key to generate a private unique hash - * code to prevent the page state from being tampered. - * By default, the private key is a randomly generated string. - * You may specify it explicitly by setting the {@link setPrivateKey PrivateKey} property. - * This may be useful if your application is running on a server farm. + * Depending on the {@link TPage::getEnableStateHMAC() EnableStateHMAC} + * and {@link TPage::getEnableStateEncryption() EnableStateEncryption}, + * TPageStatePersister may do HMAC validation and encryption to prevent + * the state data from being tampered or viewed. + * The private keys and hashing/encryption methods are determined by + * {@link TApplication::getSecurityManager() SecurityManager}. * * @author Qiang Xue * @version $Revision: $ $Date: $ * @package System.Web.UI * @since 3.0 */ -class TPageStatePersister extends TModule implements IStatePersister +class TPageStatePersister extends TComponent implements IPageStatePersister { + private $_page; + /** - * @var string private key + * @param TPage the page that this persister works for */ - private $_privateKey=null; + public function getPage() + { + return $this->_page; + } /** - * Registers the module with the page service. - * This method is required by IModule interface and is invoked when the module is initialized. - * @param TXmlElement module configuration + * @param TPage the page that this persister works for */ - public function init($config) + public function setPage(TPage $page) { - $this->getService()->setPageStatePersister($this); + $this->_page=$page; } /** @@ -53,13 +55,15 @@ class TPageStatePersister extends TModule implements IStatePersister public function save($state) { Prado::trace("Saving state",'System.Web.UI.TPageStatePersister'); - $data=Prado::serialize($state); - $hmac=$this->computeHMAC($data,$this->getPrivateKey()); - if(extension_loaded('zlib')) - $data=gzcompress($hmac.$data); + if($this->_page->getEnableStateHMAC()) + $data=$this->getApplication()->getSecurityManager()->hashData(Prado::serialize($state)); else - $data=$hmac.$data; - $this->getService()->getRequestedPage()->getClientScript()->registerHiddenField(TPage::FIELD_PAGESTATE,base64_encode($data)); + $data=Prado::serialize($state); + if($this->_page->getEnableStateEncryption()) + $data=$this->getApplication()->getSecurityManager()->encrypt($data); + if(extension_loaded('zlib')) + $data=gzcompress($data); + $this->_page->getClientScript()->registerHiddenField(TPage::FIELD_PAGESTATE,base64_encode($data)); } /** @@ -77,69 +81,19 @@ class TPageStatePersister extends TModule implements IStatePersister $data=gzuncompress($str); else $data=$str; - if($data!==false && strlen($data)>32) + if($data!==false) { - $hmac=substr($data,0,32); - $state=substr($data,32); - if($hmac===$this->computeHMAC($state,$this->getPrivateKey())) - return Prado::unserialize($state); - } - throw new THttpException(400,'pagestatepersister_pagestate_corrupted'); - } - - /** - * Generates a random private key used for hashing the state. - * You may override this method to provide your own way of private key generation. - * @return string the rondomly generated private key - */ - protected function generatePrivateKey() - { - $v1=rand(); - $v2=rand(); - $v3=rand(); - return md5("$v1$v2$v3"); - } - - /** - * @return string private key used for hashing the state. - */ - public function getPrivateKey() - { - if(empty($this->_privateKey)) - { - if(($this->_privateKey=$this->getApplication()->getGlobalState('prado:pagestatepersister:privatekey'))===null) + if($this->_page->getEnableStateEncryption()) + $data=$this->getApplication()->getSecurityManager()->decrypt($data); + if($this->_page->getEnableStateHMAC()) { - $this->_privateKey=$this->generatePrivateKey(); - $this->getApplication()->setGlobalState('prado:pagestatepersister:privatekey',$this->_privateKey,null); + if(($data=$this->getApplication()->getSecurityManager()->validateData($data))!==null) + return Prado::unserialize($data); } + else + return $data; } - return $this->_privateKey; - } - - /** - * @param string private key used for hashing the state. - * @throws TInvalidDataValueException if the length of the private key is shorter than 8. - */ - public function setPrivateKey($value) - { - if(strlen($value)<8) - throw new TInvalidDataValueException('pagestatepersister_privatekey_invalid'); - $this->_privateKey=$value; - } - - /** - * Computes a hashing code based on the input data and the private key. - * @param string input data - * @param string the private key - * @return string the hashing code - */ - private function computeHMAC($data,$key) - { - if (strlen($key) > 64) - $key = pack('H32', md5($key)); - else if (strlen($key) < 64) - $key = str_pad($key, 64, "\0"); - return md5((str_repeat("\x5c", 64) ^ substr($key, 0, 64)) . pack('H32', md5((str_repeat("\x36", 64) ^ substr($key, 0, 64)) . $data))); + throw new THttpException(400,'pagestatepersister_pagestate_corrupted'); } } diff --git a/requirements/index.php b/requirements/index.php index 37fff44f..9a29d210 100644 --- a/requirements/index.php +++ b/requirements/index.php @@ -37,6 +37,7 @@ $requirements = array( array(false,'extension_loaded("zlib")','Zlib check','Zlib extension optional'), array(false,'extension_loaded("sqlite")','SQLite check','SQLite extension optional'), array(false,'extension_loaded("memcache")','Memcache check','Memcache extension optional'), + array(false,'extension_loaded("mcrypt")','Mcrypt check','Mcrypt extension optional'), ); $results = "\n"; diff --git a/requirements/messages-zh.txt b/requirements/messages-zh.txt index c9f4714c..9446ba41 100644 --- a/requirements/messages-zh.txt +++ b/requirements/messages-zh.txt @@ -11,4 +11,5 @@ Memcache extension optional = Memcache模块是可选的。如果它不存在, Zlib check = Zlib模块检查 Zlib extension optional = Zlib模块是可选的。如果它不存在,页面的状态信息将无法压缩,由此可能导致您的页面传送数据量增大。 DOM extension required = DOM模块是必须的。如果它不存在,基于XML的各种配置文件将无法解析。 -ICONV extension optional = ICONV模块是可选的。如果它不存在,某些国际化控件将无法正常工作。 \ No newline at end of file +ICONV extension optional = ICONV模块是可选的。如果它不存在,某些国际化控件将无法正常工作。 +Mcrypt extension optional = Mcrypt模块是可选的。如果它不存在,某些敏感数据,例如viewstate,将无法被加密。 \ No newline at end of file diff --git a/requirements/messages.txt b/requirements/messages.txt index 05c4f2aa..1d0cc471 100644 --- a/requirements/messages.txt +++ b/requirements/messages.txt @@ -11,4 +11,5 @@ Memcache extension optional = Memcache extension is optional. If it is absent, Zlib check = Zlib check Zlib extension optional = Zlib extension is optional. If it is absent, page state will not be compressed and your page size may increase. DOM extension required = DOM extension is required by PRADO. It is used in TXmlDocument to parse all sorts of XML-based configurations. -ICONV extension optional = ICONV extension is optional. If it is absent, some internationalization components may not work properly. \ No newline at end of file +ICONV extension optional = ICONV extension is optional. If it is absent, some internationalization components may not work properly. +Mcrypt extension optional = Mcrypt extension is optional. If it is absent, sensitive data, such as viewstate, cannot be encrypted. \ No newline at end of file -- cgit v1.2.3