diff --git a/classes/security/Crypto.class.php b/classes/security/Crypto.class.php new file mode 100644 index 000000000..0e9e06cf4 --- /dev/null +++ b/classes/security/Crypto.class.php @@ -0,0 +1,357 @@ + */ + +/** + * This class makes encryption and digital signing easy to use in XE. + * + * The encryption methods use AES-128, and is fully compatible with + * https://github.com/defuse/php-encryption + * except that it uses base64-encoded keys and ciphertexts. + * + * The digital signature methods is based on the same SHA-256 based + * key derivation function used by the encryption methods. + * + * A key is automatically generated and saved to the files/config directory + * when first invoked. The same key will be used for all subsequent + * method calls that do not specify a different key. + * The key must be a binary string exactly 16 bytes long. + * + * @file Crypto.class.php + * @author Kijin Sung (kijin@kijinsung.com) + * @package /classes/security + * @version 1.0 + */ +class Crypto +{ + /** + * @brief Default configuration + */ + const ENCRYPTION_ALGO = 'aes-128'; + const ENCRYPTION_MODE = 'cbc'; + const ENCRYPTION_BLOCK_SIZE = 16; + const ENCRYPTION_KEY_SIZE = 16; + const ENCRYPTION_KEY_INFO = 'DefusePHP|KeyForEncryption'; + const ENCRYPTION_MAC_ALGO = 'sha256'; + const ENCRYPTION_MAC_SIZE = 32; + const ENCRYPTION_MAC_INFO = 'DefusePHP|KeyForAuthentication'; + const SIGNATURE_ALGO = 'sha256'; + const SIGNATURE_SIZE = '32'; + + /** + * @brief The default key + */ + protected static $_default_key = null; + + /** + * @brief The currently selected extension + */ + protected static $_extension = null; + + /** + * @brief If this is true, encryption and signature are only valid in current session + */ + protected $_current_session_only = false; + + /** + * @brief Constructor + */ + public function __construct() + { + if(function_exists('openssl_encrypt')) + { + self::$_extension = 'openssl'; + } + elseif(function_exists('mcrypt_encrypt')) + { + self::$_extension = 'mcrypt'; + } + else + { + throw new Exception('Crypto class requires openssl or mcrypt extension.'); + } + } + + /** + * @brief Check if cryptography is supported on this server + * @return bool + */ + public static function isSupported() + { + return (function_exists('openssl_encrypt') || function_exists('mcrypt_encrypt')); + } + + /** + * @brief Make encryption and signature only valid in current session + * @return void + */ + public function currentSessionOnly() + { + $this->_current_session_only = true; + } + + /** + * @brief Encrypt a string + * @param string $plaintext The string to encrypt + * @param string $key Optional key. If empty, default key will be used. + * @return string + */ + public function encrypt($plaintext, $key = null) + { + if($key === null || $key === '') + { + $key = $this->_getSessionKey(); + } + + // Generate subkey for encryption + $enc_key = self::_defuseCompatibleHKDF($key, self::ENCRYPTION_KEY_INFO); + + // Generate IV + $iv = self::_createIV(); + + // Encrypt the plaintext + if(self::$_extension === 'openssl') + { + $openssl_method = self::ENCRYPTION_ALGO . '-' . self::ENCRYPTION_MODE; + $ciphertext = openssl_encrypt($plaintext, $openssl_method, $enc_key, OPENSSL_RAW_DATA, $iv); + } + else + { + $mcrypt_method = str_replace('aes', 'rijndael', self::ENCRYPTION_ALGO); + $plaintext = self::_applyPKCS7Padding($plaintext, self::ENCRYPTION_BLOCK_SIZE); + $ciphertext = mcrypt_encrypt($mcrypt_method, $enc_key, $plaintext, self::ENCRYPTION_MODE, $iv); + } + + // Generate MAC + $mac_key = self::_defuseCompatibleHKDF($key, self::ENCRYPTION_MAC_INFO); + $mac = hash_hmac(self::ENCRYPTION_MAC_ALGO, ($iv . $ciphertext), $mac_key, true); + + // Return the MAC, IV, and ciphertext as a base64 encoded string + return base64_encode($mac . $iv . $ciphertext); + } + + /** + * @brief Decrypt a string + * @param string $ciphertext The string to decrypt + * @param string $key Optional key. If empty, default key will be used. + * @return string + */ + public function decrypt($ciphertext, $key = null) + { + if($key === null || $key === '') + { + $key = $this->_getSessionKey(); + } + + // Base64 decode the ciphertext and check the length + $ciphertext = @base64_decode($ciphertext); + if(strlen($ciphertext) < (self::ENCRYPTION_MAC_SIZE + (self::ENCRYPTION_BLOCK_SIZE * 2))) + { + return false; + } + + // Extract MAC and IV from the remainder of the ciphertext + $mac = substr($ciphertext, 0, self::ENCRYPTION_MAC_SIZE); + $iv = substr($ciphertext, self::ENCRYPTION_MAC_SIZE, self::ENCRYPTION_BLOCK_SIZE); + $ciphertext = substr($ciphertext, self::ENCRYPTION_MAC_SIZE + self::ENCRYPTION_BLOCK_SIZE); + + // Validate MAC + $mac_key = self::_defuseCompatibleHKDF($key, self::ENCRYPTION_MAC_INFO); + $mac_compare = hash_hmac(self::ENCRYPTION_MAC_ALGO, ($iv . $ciphertext), $mac_key, true); + $oPassword = new Password(); + if(!$oPassword->strcmpConstantTime($mac, $mac_compare)) + { + return false; + } + + // Generate subkey for encryption + $enc_key = self::_defuseCompatibleHKDF($key, self::ENCRYPTION_KEY_INFO); + + // Decrypt the ciphertext + if (self::$_extension === 'openssl') + { + $openssl_method = self::ENCRYPTION_ALGO . '-' . self::ENCRYPTION_MODE; + $plaintext = openssl_decrypt($ciphertext, $openssl_method, $enc_key, OPENSSL_RAW_DATA, $iv); + } + else + { + $mcrypt_method = str_replace('aes', 'rijndael', self::ENCRYPTION_ALGO); + $plaintext = @mcrypt_decrypt($mcrypt_method, $enc_key, $ciphertext, self::ENCRYPTION_MODE, $iv); + if($plaintext === false) + { + return false; + } + $plaintext = self::_stripPKCS7Padding($plaintext, self::ENCRYPTION_BLOCK_SIZE); + if($plaintext === false) + { + return false; + } + } + + // Return the plaintext + return $plaintext; + } + + /** + * @brief Create a digital signature of a string + * @param string $plaintext The string to sign + * @param string $key Optional key. If empty, default key will be used. + * @return string + */ + public function createSignature($plaintext, $key = null) + { + if($key === null || $key === '') + { + $key = $this->_getSessionKey(); + } + + // Generate a signature using HMAC + return bin2hex(self::_defuseCompatibleHKDF($plaintext, $key)); + } + + /** + * @brief Verify a digital signature + * @param string $signature The signature to verify + * @param string $plaintext The string to verify + * @param string $key Optional key. If empty, default key will be used. + * @return bool + */ + public function verifySignature($signature, $plaintext, $key = null) + { + if($key === null || $key === '') + { + $key = $this->_getSessionKey(); + } + + // Verify the signature using HMAC + $oPassword = new Password(); + $compare = bin2hex(self::_defuseCompatibleHKDF($plaintext, $key)); + return $oPassword->strcmpConstantTime($signature, $compare); + } + + /** + * @brief Get the default key applicable to this instance + * @return string + */ + protected function _getSessionKey() + { + if($this->_current_session_only) + { + if(!isset($_SESSION['XE_CRYPTO_SESSKEY'])) + { + $_SESSION['XE_CRYPTO_SESSKEY'] = self::_createSecureKey(); + } + $session_key = base64_decode($_SESSION['XE_CRYPTO_SESSKEY']); + return strval(self::_getDefaultKey()) ^ strval($session_key); + } + else + { + return strval(self::_getDefaultKey()); + } + } + + /** + * @brief Get the default key + * @return string + */ + protected static function _getDefaultKey() + { + if(self::$_default_key !== null) + { + return base64_decode(self::$_default_key); + } + else + { + $file_name = _XE_PATH_ . 'files/config/crypto.config.php'; + if(file_exists($file_name) && is_readable($file_name)) + { + $key = (include $file_name); + } + if(!isset($key) || !is_string($key)) + { + $key = self::_createSecureKey(); + self::_setDefaultKey($key); + } + return base64_decode(self::$_default_key = $key); + } + } + + /** + * @brief Set the default key + * @param string $key The default key + * @return void + */ + protected static function _setDefaultKey($key) + { + self::$_default_key = $key = trim($key); + $file_name = _XE_PATH_ . 'files/config/crypto.config.php'; + $file_content = 'createSecureSalt(ENCRYPTION_KEY_SIZE, 'binary')); + } + + /** + * @brief Create an IV + * @return string + */ + protected static function _createIV() + { + $oPassword = new Password(); + return $oPassword->createSecureSalt(self::ENCRYPTION_BLOCK_SIZE, 'binary'); + } + + + /** + * @brief Apply PKCS#7 padding to a string + * @param string $str The string + * @param int $block_size The block size + * @return string + */ + protected static function _applyPKCS7Padding($str, $block_size) + { + $padding_size = $block_size - (strlen($str) % $block_size); + if ($padding_size === 0) $padding_size = $block_size; + return $str . str_repeat(chr($padding_size), $padding_size); + } + + /** + * @brief Remove PKCS#7 padding from a string + * @param string $str The string + * @param int $block_size The block size + * @return string + */ + protected static function _stripPKCS7Padding($str, $block_size) + { + if (strlen($str) % $block_size !== 0) return false; + $padding_size = ord(substr($str, -1)); + if ($padding_size < 1 || $padding_size > $block_size) return false; + if (substr($str, (-1 * $padding_size)) !== str_repeat(chr($padding_size), $padding_size)) return false; + return substr($str, 0, strlen($str) - $padding_size); + } + + /** + * @brief HKDF function compatible with defuse/php-encryption + * @return string + */ + protected static function _defuseCompatibleHKDF($key, $info) + { + $salt = str_repeat("\x00", self::ENCRYPTION_MAC_SIZE); + $prk = hash_hmac(self::ENCRYPTION_MAC_ALGO, $key, $salt, true); + $t = $last_block = ''; + for ($block_index = 1; strlen($t) < self::ENCRYPTION_KEY_SIZE; $block_index++) + { + $t .= $last_block = hash_hmac(self::ENCRYPTION_MAC_ALGO, ($last_block . $info . chr($block_index)), $prk, true); + } + return substr($t, 0, self::ENCRYPTION_KEY_SIZE); + } +} +/* End of file : Crypto.class.php */ +/* Location: ./classes/security/Crypto.class.php */ diff --git a/config/config.inc.php b/config/config.inc.php index 4428530b1..6501ca334 100644 --- a/config/config.inc.php +++ b/config/config.inc.php @@ -342,6 +342,7 @@ $GLOBALS['__xe_autoload_file_map'] = array_change_key_case(array( 'ModuleObject' => 'classes/module/ModuleObject.class.php', 'Object' => 'classes/object/Object.class.php', 'PageHandler' => 'classes/page/PageHandler.class.php', + 'Crypto' => 'classes/security/Crypto.class.php', 'EmbedFilter' => 'classes/security/EmbedFilter.class.php', 'IpFilter' => 'classes/security/IpFilter.class.php', 'Password' => 'classes/security/Password.class.php',