From ee47928f8dcb471a206a04bed7c3687b27dd7b53 Mon Sep 17 00:00:00 2001 From: Master Yoda Date: Sun, 7 May 2017 23:04:30 -0700 Subject: [PATCH 01/18] Base conversion --- system/Encryption/Encryption.php | 882 ++++++++++++++++++ tests/system/Encryption/Encryption_test.php | 400 ++++++++ .../source/libraries/encryption.rst | 585 ++++++++++++ 3 files changed, 1867 insertions(+) create mode 100644 system/Encryption/Encryption.php create mode 100644 tests/system/Encryption/Encryption_test.php create mode 100644 user_guide_src/source/libraries/encryption.rst diff --git a/system/Encryption/Encryption.php b/system/Encryption/Encryption.php new file mode 100644 index 000000000000..afbe860a37d5 --- /dev/null +++ b/system/Encryption/Encryption.php @@ -0,0 +1,882 @@ + array( + 'cbc' => 'cbc', + 'ecb' => 'ecb', + 'ofb' => 'nofb', + 'ofb8' => 'ofb', + 'cfb' => 'ncfb', + 'cfb8' => 'cfb', + 'ctr' => 'ctr', + 'stream' => 'stream' + ), + 'openssl' => array( + 'cbc' => 'cbc', + 'ecb' => 'ecb', + 'ofb' => 'ofb', + 'cfb' => 'cfb', + 'cfb8' => 'cfb8', + 'ctr' => 'ctr', + 'stream' => '', + 'xts' => 'xts' + ) + ); + + /** + * List of supported HMAC algorithms + * + * name => digest size pairs + * + * @var array + */ + protected $_digests = array( + 'sha224' => 28, + 'sha256' => 32, + 'sha384' => 48, + 'sha512' => 64 + ); + + /** + * mbstring.func_overload flag + * + * @var bool + */ + protected static $func_overload; + + // -------------------------------------------------------------------- + + /** + * Class constructor + * + * @param array $params Configuration parameters + * @return void + */ + public function __construct(array $params = array()) + { + $this->_drivers = array( + 'mcrypt' => defined('MCRYPT_DEV_URANDOM'), + 'openssl' => extension_loaded('openssl') + ); + + if (!$this->_drivers['mcrypt'] && !$this->_drivers['openssl']) + { + throw new \Exception('Encryption: Unable to find an available encryption driver.'); + } + + isset(self::$func_overload) OR self::$func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload')); + $this->initialize($params); + + if (!isset($this->_key) && self::strlen($key = config_item('encryption_key')) > 0) + { + $this->_key = $key; + } + + log_message('info', 'Encryption Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * Initialize + * + * @param array $params Configuration parameters + * @return CI_Encryption + */ + public function initialize(array $params) + { + if (!empty($params['driver'])) + { + if (isset($this->_drivers[$params['driver']])) + { + if ($this->_drivers[$params['driver']]) + { + $this->_driver = $params['driver']; + } else + { + throw new \Exception("Encryption: Driver '" . $params['driver'] . "' is not available."); + } + } else + { + throw new \Exception("Encryption: Unknown driver '" . $params['driver'] . "' cannot be configured."); + } + } + + if (empty($this->_driver)) + { + $this->_driver = ($this->_drivers['openssl'] === TRUE) ? 'openssl' : 'mcrypt'; + + log_message('debug', "Encryption: Auto-configured driver '" . $this->_driver . "'."); + } + + empty($params['cipher']) && $params['cipher'] = $this->_cipher; + empty($params['key']) OR $this->_key = $params['key']; + $this->{'_' . $this->_driver . '_initialize'}($params); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Initialize MCrypt + * + * @param array $params Configuration parameters + * @return void + */ + protected function _mcrypt_initialize($params) + { + if (!empty($params['cipher'])) + { + $params['cipher'] = strtolower($params['cipher']); + $this->_cipher_alias($params['cipher']); + + if (!in_array($params['cipher'], mcrypt_list_algorithms(), TRUE)) + { + throw new \Exception('Encryption: MCrypt cipher ' . strtoupper($params['cipher']) . ' is not available.'); + } else + { + $this->_cipher = $params['cipher']; + } + } + + if (!empty($params['mode'])) + { + $params['mode'] = strtolower($params['mode']); + if (!isset($this->_modes['mcrypt'][$params['mode']])) + { + throw new \Exception('Encryption: MCrypt mode ' . strtoupper($params['cipher']) . ' is not available.'); + } else + { + $this->_mode = $this->_modes['mcrypt'][$params['mode']]; + } + } + + if (isset($this->_cipher, $this->_mode)) + { + if (is_resource($this->_handle) && (strtolower(mcrypt_enc_get_algorithms_name($this->_handle)) !== $this->_cipher + OR strtolower(mcrypt_enc_get_modes_name($this->_handle)) !== $this->_mode) + ) + { + mcrypt_module_close($this->_handle); + } + + if ($this->_handle = mcrypt_module_open($this->_cipher, '', $this->_mode, '')) + { + log_message('info', 'Encryption: MCrypt cipher ' . strtoupper($this->_cipher) . ' initialized in ' . strtoupper($this->_mode) . ' mode.'); + } else + { + throw new \Exception('Encryption: Unable to initialize MCrypt with cipher ' . strtoupper($this->_cipher) . ' in ' . strtoupper($this->_mode) . ' mode.'); + } + } + } + + // -------------------------------------------------------------------- + + /** + * Initialize OpenSSL + * + * @param array $params Configuration parameters + * @return void + */ + protected function _openssl_initialize($params) + { + if (!empty($params['cipher'])) + { + $params['cipher'] = strtolower($params['cipher']); + $this->_cipher_alias($params['cipher']); + $this->_cipher = $params['cipher']; + } + + if (!empty($params['mode'])) + { + $params['mode'] = strtolower($params['mode']); + if (!isset($this->_modes['openssl'][$params['mode']])) + { + log_message('error', 'Encryption: OpenSSL mode ' . strtoupper($params['mode']) . ' is not available.'); + } else + { + $this->_mode = $this->_modes['openssl'][$params['mode']]; + } + } + + if (isset($this->_cipher, $this->_mode)) + { + // This is mostly for the stream mode, which doesn't get suffixed in OpenSSL + $handle = empty($this->_mode) ? $this->_cipher : $this->_cipher . '-' . $this->_mode; + + if (!in_array($handle, openssl_get_cipher_methods(), TRUE)) + { + $this->_handle = NULL; + log_message('error', 'Encryption: Unable to initialize OpenSSL with method ' . strtoupper($handle) . '.'); + } else + { + $this->_handle = $handle; + log_message('info', 'Encryption: OpenSSL initialized with method ' . strtoupper($handle) . '.'); + } + } + } + + // -------------------------------------------------------------------- + + /** + * Create a random key + * + * @param int $length Output length + * @return string + */ + public function create_key($length) + { + if (function_exists('random_bytes')) + { + try + { + return random_bytes((int) $length); + } catch (Exception $e) + { + log_message('error', $e->getMessage()); + return FALSE; + } + } elseif (defined('MCRYPT_DEV_URANDOM')) + { + return mcrypt_create_iv($length, MCRYPT_DEV_URANDOM); + } + + $is_secure = NULL; + $key = openssl_random_pseudo_bytes($length, $is_secure); + return ($is_secure === TRUE) ? $key : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Encrypt + * + * @param string $data Input data + * @param array $params Input parameters + * @return string + */ + public function encrypt($data, array $params = NULL) + { + if (($params = $this->_get_params($params)) === FALSE) + { + return FALSE; + } + + isset($params['key']) OR $params['key'] = $this->hkdf($this->_key, 'sha512', NULL, self::strlen($this->_key), 'encryption'); + + if (($data = $this->{'_' . $this->_driver . '_encrypt'}($data, $params)) === FALSE) + { + return FALSE; + } + + $params['base64'] && $data = base64_encode($data); + + if (isset($params['hmac_digest'])) + { + isset($params['hmac_key']) OR $params['hmac_key'] = $this->hkdf($this->_key, 'sha512', NULL, NULL, 'authentication'); + return hash_hmac($params['hmac_digest'], $data, $params['hmac_key'], !$params['base64']) . $data; + } + + return $data; + } + + // -------------------------------------------------------------------- + + /** + * Encrypt via MCrypt + * + * @param string $data Input data + * @param array $params Input parameters + * @return string + */ + protected function _mcrypt_encrypt($data, $params) + { + if (!is_resource($params['handle'])) + { + return FALSE; + } + + // The greater-than-1 comparison is mostly a work-around for a bug, + // where 1 is returned for ARCFour instead of 0. + $iv = (($iv_size = mcrypt_enc_get_iv_size($params['handle'])) > 1) ? $this->create_key($iv_size) : NULL; + + if (mcrypt_generic_init($params['handle'], $params['key'], $iv) < 0) + { + if ($params['handle'] !== $this->_handle) + { + mcrypt_module_close($params['handle']); + } + + return FALSE; + } + + // Use PKCS#7 padding in order to ensure compatibility with OpenSSL + // and other implementations outside of PHP. + if (in_array(strtolower(mcrypt_enc_get_modes_name($params['handle'])), array('cbc', 'ecb'), TRUE)) + { + $block_size = mcrypt_enc_get_block_size($params['handle']); + $pad = $block_size - (self::strlen($data) % $block_size); + $data .= str_repeat(chr($pad), $pad); + } + + // Work-around for yet another strange behavior in MCrypt. + // + // When encrypting in ECB mode, the IV is ignored. Yet + // mcrypt_enc_get_iv_size() returns a value larger than 0 + // even if ECB is used AND mcrypt_generic_init() complains + // if you don't pass an IV with length equal to the said + // return value. + // + // This probably would've been fine (even though still wasteful), + // but OpenSSL isn't that dumb and we need to make the process + // portable, so ... + $data = (mcrypt_enc_get_modes_name($params['handle']) !== 'ECB') ? $iv . mcrypt_generic($params['handle'], $data) : mcrypt_generic($params['handle'], $data); + + mcrypt_generic_deinit($params['handle']); + if ($params['handle'] !== $this->_handle) + { + mcrypt_module_close($params['handle']); + } + + return $data; + } + + // -------------------------------------------------------------------- + + /** + * Encrypt via OpenSSL + * + * @param string $data Input data + * @param array $params Input parameters + * @return string + */ + protected function _openssl_encrypt($data, $params) + { + if (empty($params['handle'])) + { + return FALSE; + } + + $iv = ($iv_size = openssl_cipher_iv_length($params['handle'])) ? $this->create_key($iv_size) : NULL; + + $data = openssl_encrypt( + $data, $params['handle'], $params['key'], OPENSSL_RAW_DATA, $iv + ); + + if ($data === FALSE) + { + return FALSE; + } + + return $iv . $data; + } + + // -------------------------------------------------------------------- + + /** + * Decrypt + * + * @param string $data Encrypted data + * @param array $params Input parameters + * @return string + */ + public function decrypt($data, array $params = NULL) + { + if (($params = $this->_get_params($params)) === FALSE) + { + return FALSE; + } + + if (isset($params['hmac_digest'])) + { + // This might look illogical, but it is done during encryption as well ... + // The 'base64' value is effectively an inverted "raw data" parameter + $digest_size = ($params['base64']) ? $this->_digests[$params['hmac_digest']] * 2 : $this->_digests[$params['hmac_digest']]; + + if (self::strlen($data) <= $digest_size) + { + return FALSE; + } + + $hmac_input = self::substr($data, 0, $digest_size); + $data = self::substr($data, $digest_size); + + isset($params['hmac_key']) OR $params['hmac_key'] = $this->hkdf($this->_key, 'sha512', NULL, NULL, 'authentication'); + $hmac_check = hash_hmac($params['hmac_digest'], $data, $params['hmac_key'], !$params['base64']); + + // Time-attack-safe comparison + $diff = 0; + for ($i = 0; $i < $digest_size; $i++) + { + $diff |= ord($hmac_input[$i]) ^ ord($hmac_check[$i]); + } + + if ($diff !== 0) + { + return FALSE; + } + } + + if ($params['base64']) + { + $data = base64_decode($data); + } + + isset($params['key']) OR $params['key'] = $this->hkdf($this->_key, 'sha512', NULL, self::strlen($this->_key), 'encryption'); + + return $this->{'_' . $this->_driver . '_decrypt'}($data, $params); + } + + // -------------------------------------------------------------------- + + /** + * Decrypt via MCrypt + * + * @param string $data Encrypted data + * @param array $params Input parameters + * @return string + */ + protected function _mcrypt_decrypt($data, $params) + { + if (!is_resource($params['handle'])) + { + return FALSE; + } + + // The greater-than-1 comparison is mostly a work-around for a bug, + // where 1 is returned for ARCFour instead of 0. + if (($iv_size = mcrypt_enc_get_iv_size($params['handle'])) > 1) + { + if (mcrypt_enc_get_modes_name($params['handle']) !== 'ECB') + { + $iv = self::substr($data, 0, $iv_size); + $data = self::substr($data, $iv_size); + } else + { + // MCrypt is dumb and this is ignored, only size matters + $iv = str_repeat("\x0", $iv_size); + } + } else + { + $iv = NULL; + } + + if (mcrypt_generic_init($params['handle'], $params['key'], $iv) < 0) + { + if ($params['handle'] !== $this->_handle) + { + mcrypt_module_close($params['handle']); + } + + return FALSE; + } + + $data = mdecrypt_generic($params['handle'], $data); + // Remove PKCS#7 padding, if necessary + if (in_array(strtolower(mcrypt_enc_get_modes_name($params['handle'])), array('cbc', 'ecb'), TRUE)) + { + $data = self::substr($data, 0, -ord($data[self::strlen($data) - 1])); + } + + mcrypt_generic_deinit($params['handle']); + if ($params['handle'] !== $this->_handle) + { + mcrypt_module_close($params['handle']); + } + + return $data; + } + + // -------------------------------------------------------------------- + + /** + * Decrypt via OpenSSL + * + * @param string $data Encrypted data + * @param array $params Input parameters + * @return string + */ + protected function _openssl_decrypt($data, $params) + { + if ($iv_size = openssl_cipher_iv_length($params['handle'])) + { + $iv = self::substr($data, 0, $iv_size); + $data = self::substr($data, $iv_size); + } else + { + $iv = NULL; + } + + return empty($params['handle']) ? FALSE : openssl_decrypt( + $data, $params['handle'], $params['key'], OPENSSL_RAW_DATA, $iv + ); + } + + // -------------------------------------------------------------------- + + /** + * Get params + * + * @param array $params Input parameters + * @return array + */ + protected function _get_params($params) + { + if (empty($params)) + { + return isset($this->_cipher, $this->_mode, $this->_key, $this->_handle) ? array( + 'handle' => $this->_handle, + 'cipher' => $this->_cipher, + 'mode' => $this->_mode, + 'key' => NULL, + 'base64' => TRUE, + 'hmac_digest' => 'sha512', + 'hmac_key' => NULL + ) : FALSE; + } elseif (!isset($params['cipher'], $params['mode'], $params['key'])) + { + return FALSE; + } + + if (isset($params['mode'])) + { + $params['mode'] = strtolower($params['mode']); + if (!isset($this->_modes[$this->_driver][$params['mode']])) + { + return FALSE; + } else + { + $params['mode'] = $this->_modes[$this->_driver][$params['mode']]; + } + } + + if (isset($params['hmac']) && $params['hmac'] === FALSE) + { + $params['hmac_digest'] = $params['hmac_key'] = NULL; + } else + { + if (!isset($params['hmac_key'])) + { + return FALSE; + } elseif (isset($params['hmac_digest'])) + { + $params['hmac_digest'] = strtolower($params['hmac_digest']); + if (!isset($this->_digests[$params['hmac_digest']])) + { + return FALSE; + } + } else + { + $params['hmac_digest'] = 'sha512'; + } + } + + $params = array( + 'handle' => NULL, + 'cipher' => $params['cipher'], + 'mode' => $params['mode'], + 'key' => $params['key'], + 'base64' => isset($params['raw_data']) ? !$params['raw_data'] : FALSE, + 'hmac_digest' => $params['hmac_digest'], + 'hmac_key' => $params['hmac_key'] + ); + + $this->_cipher_alias($params['cipher']); + $params['handle'] = ($params['cipher'] !== $this->_cipher OR $params['mode'] !== $this->_mode) ? $this->{'_' . $this->_driver . '_get_handle'}($params['cipher'], $params['mode']) : $this->_handle; + + return $params; + } + + // -------------------------------------------------------------------- + + /** + * Get MCrypt handle + * + * @param string $cipher Cipher name + * @param string $mode Encryption mode + * @return resource + */ + protected function _mcrypt_get_handle($cipher, $mode) + { + return mcrypt_module_open($cipher, '', $mode, ''); + } + + // -------------------------------------------------------------------- + + /** + * Get OpenSSL handle + * + * @param string $cipher Cipher name + * @param string $mode Encryption mode + * @return string + */ + protected function _openssl_get_handle($cipher, $mode) + { + // OpenSSL methods aren't suffixed with '-stream' for this mode + return ($mode === 'stream') ? $cipher : $cipher . '-' . $mode; + } + + // -------------------------------------------------------------------- + + /** + * Cipher alias + * + * Tries to translate cipher names between MCrypt and OpenSSL's "dialects". + * + * @param string $cipher Cipher name + * @return void + */ + protected function _cipher_alias(&$cipher) + { + static $dictionary; + + if (empty($dictionary)) + { + $dictionary = array( + 'mcrypt' => array( + 'aes-128' => 'rijndael-128', + 'aes-192' => 'rijndael-128', + 'aes-256' => 'rijndael-128', + 'des3-ede3' => 'tripledes', + 'bf' => 'blowfish', + 'cast5' => 'cast-128', + 'rc4' => 'arcfour', + 'rc4-40' => 'arcfour' + ), + 'openssl' => array( + 'rijndael-128' => 'aes-128', + 'tripledes' => 'des-ede3', + 'blowfish' => 'bf', + 'cast-128' => 'cast5', + 'arcfour' => 'rc4-40', + 'rc4' => 'rc4-40' + ) + ); + + // Notes: + // + // - Rijndael-128 is, at the same time all three of AES-128, + // AES-192 and AES-256. The only difference between them is + // the key size. Rijndael-192, Rijndael-256 on the other hand + // also have different block sizes and are NOT AES-compatible. + // + // - Blowfish is said to be supporting key sizes between + // 4 and 56 bytes, but it appears that between MCrypt and + // OpenSSL, only those of 16 and more bytes are compatible. + // Also, don't know what MCrypt's 'blowfish-compat' is. + // + // - CAST-128/CAST5 produces a longer cipher when encrypted via + // OpenSSL, but (strangely enough) can be decrypted by either + // extension anyway. + // Also, it appears that OpenSSL uses 16 rounds regardless of + // the key size, while RFC2144 says that for key sizes lower + // than 11 bytes, only 12 rounds should be used. This makes + // it portable only with keys of between 11 and 16 bytes. + // + // - RC4 (ARCFour) has a strange implementation under OpenSSL. + // Its 'rc4-40' cipher method seems to work flawlessly, yet + // there's another one, 'rc4' that only works with a 16-byte key. + // + // - DES is compatible, but doesn't need an alias. + // + // Other seemingly matching ciphers between MCrypt, OpenSSL: + // + // - RC2 is NOT compatible and only an obscure forum post + // confirms that it is MCrypt's fault. + } + + if (isset($dictionary[$this->_driver][$cipher])) + { + $cipher = $dictionary[$this->_driver][$cipher]; + } + } + + // -------------------------------------------------------------------- + + /** + * HKDF + * + * @link https://tools.ietf.org/rfc/rfc5869.txt + * @param $key Input key + * @param $digest A SHA-2 hashing algorithm + * @param $salt Optional salt + * @param $length Output length (defaults to the selected digest size) + * @param $info Optional context/application-specific info + * @return string A pseudo-random key + */ + public function hkdf($key, $digest = 'sha512', $salt = NULL, $length = NULL, $info = '') + { + if (!isset($this->_digests[$digest])) + { + return FALSE; + } + + if (empty($length) OR ! is_int($length)) + { + $length = $this->_digests[$digest]; + } elseif ($length > (255 * $this->_digests[$digest])) + { + return FALSE; + } + + self::strlen($salt) OR $salt = str_repeat("\0", $this->_digests[$digest]); + + $prk = hash_hmac($digest, $key, $salt, TRUE); + $key = ''; + for ($key_block = '', $block_index = 1; self::strlen($key) < $length; $block_index++) + { + $key_block = hash_hmac($digest, $key_block . $info . chr($block_index), $prk, TRUE); + $key .= $key_block; + } + + return self::substr($key, 0, $length); + } + + // -------------------------------------------------------------------- + + /** + * __get() magic + * + * @param string $key Property name + * @return mixed + */ + public function __get($key) + { + // Because aliases + if ($key === 'mode') + { + return array_search($this->_mode, $this->_modes[$this->_driver], TRUE); + } elseif (in_array($key, array('cipher', 'driver', 'drivers', 'digests'), TRUE)) + { + return $this->{'_' . $key}; + } + + return NULL; + } + + // -------------------------------------------------------------------- + + /** + * Byte-safe strlen() + * + * @param string $str + * @return int + */ + protected static function strlen($str) + { + return (self::$func_overload) ? mb_strlen($str, '8bit') : strlen($str); + } + + // -------------------------------------------------------------------- + + /** + * Byte-safe substr() + * + * @param string $str + * @param int $start + * @param int $length + * @return string + */ + protected static function substr($str, $start, $length = NULL) + { + if (self::$func_overload) + { + return mb_substr($str, $start, $length, '8bit'); + } + + return isset($length) ? substr($str, $start, $length) : substr($str, $start); + } + +} diff --git a/tests/system/Encryption/Encryption_test.php b/tests/system/Encryption/Encryption_test.php new file mode 100644 index 000000000000..99c5d4b9dafe --- /dev/null +++ b/tests/system/Encryption/Encryption_test.php @@ -0,0 +1,400 @@ +encryption = new Mock_Libraries_Encryption(); + } + + // -------------------------------------------------------------------- + + /** + * __construct test + * + * Covers behavior with $config['encryption_key'] set or not + */ + public function test___construct() + { + // Assume no configuration from set_up() + $this->assertNull($this->encryption->get_key()); + + // Try with an empty value + $this->ci_set_config('encryption_key'); + $this->encrypt = new Mock_Libraries_Encryption(); + $this->assertNull($this->encrypt->get_key()); + + $this->ci_set_config('encryption_key', str_repeat("\x0", 16)); + $this->encrypt = new Mock_Libraries_Encryption(); + $this->assertEquals(str_repeat("\x0", 16), $this->encrypt->get_key()); + } + + // -------------------------------------------------------------------- + + /** + * hkdf() test + * + * Applies test vectors described in Appendix A(1-3) RFC5869. + * Described vectors 4-7 SHA-1, which we don't support and are + * therefore excluded. + * + * Because our implementation is a single method instead of being + * split into hkdf_extract() and hkdf_expand(), we cannot test for + * the PRK value. As long as the OKM is correct though, it's fine. + * + * @link https://tools.ietf.org/rfc/rfc5869.txt + */ + public function test_hkdf() + { + $vectors = array( + // A.1: Basic test case with SHA-256 + array( + 'digest' => 'sha256', + 'ikm' => "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b", + 'salt' => "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c", + 'length' => 42, + 'info' => "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9", + // 'prk' => "\x07\x77\x09\x36\x2c\x2e\x32\xdf\x0d\xdc\x3f\x0d\xc4\x7b\xba\x63\x90\xb6\xc7\x3b\xb5\x0f\x9c\x31\x22\xec\x84\x4a\xd7\xc2\xb3\xe5", + 'okm' => "\x3c\xb2\x5f\x25\xfa\xac\xd5\x7a\x90\x43\x4f\x64\xd0\x36\x2f\x2a\x2d\x2d\x0a\x90\xcf\x1a\x5a\x4c\x5d\xb0\x2d\x56\xec\xc4\xc5\xbf\x34\x00\x72\x08\xd5\xb8\x87\x18\x58\x65" + ), + // A.2: Test with SHA-256 and longer inputs/outputs + array( + 'digest' => 'sha256', + 'ikm' => "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f", + 'salt' => "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf", + 'length' => 82, + 'info' => "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff", + // 'prk' => "\x06\xa6\xb8\x8c\x58\x53\x36\x1a\x06\x10\x4c\x9c\xeb\x35\xb4\x5c\xef\x76\x00\x14\x90\x46\x71\x01\x4a\x19\x3f\x40\xc1\x5f\xc2\x44", + 'okm' => "\xb1\x1e\x39\x8d\xc8\x03\x27\xa1\xc8\xe7\xf7\x8c\x59\x6a\x49\x34\x4f\x01\x2e\xda\x2d\x4e\xfa\xd8\xa0\x50\xcc\x4c\x19\xaf\xa9\x7c\x59\x04\x5a\x99\xca\xc7\x82\x72\x71\xcb\x41\xc6\x5e\x59\x0e\x09\xda\x32\x75\x60\x0c\x2f\x09\xb8\x36\x77\x93\xa9\xac\xa3\xdb\x71\xcc\x30\xc5\x81\x79\xec\x3e\x87\xc1\x4c\x01\xd5\xc1\xf3\x43\x4f\x1d\x87", + ), + // A.3: Test with SHA-256 and zero-length salt/info + array( + 'digest' => 'sha256', + 'ikm' => "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b", + 'salt' => '', + 'length' => 42, + 'info' => '', + // 'prk' => "\x19\xef\x24\xa3\x2c\x71\x7b\x16\x7f\x33\xa9\x1d\x6f\x64\x8b\xdf\x96\x59\x67\x76\xaf\xdb\x63\x77\xac\x43\x4c\x1c\x29\x3c\xcb\x04", + 'okm' => "\x8d\xa4\xe7\x75\xa5\x63\xc1\x8f\x71\x5f\x80\x2a\x06\x3c\x5a\x31\xb8\xa1\x1f\x5c\x5e\xe1\x87\x9e\xc3\x45\x4e\x5f\x3c\x73\x8d\x2d\x9d\x20\x13\x95\xfa\xa4\xb6\x1a\x96\xc8", + ) + ); + + foreach ($vectors as $test) + { + $this->assertEquals( + $test['okm'], + $this->encryption->hkdf( + $test['ikm'], + $test['digest'], + $test['salt'], + $test['length'], + $test['info'] + ) + ); + } + + // Test default length, it must match the digest size + $hkdf_result = $this->encryption->hkdf('foobar', 'sha512'); + $this->assertEquals( + 64, + defined('MB_OVERLOAD_STRING') + ? mb_strlen($hkdf_result, '8bit') + : strlen($hkdf_result) + ); + + // Test maximum length (RFC5869 says that it must be up to 255 times the digest size) + $hkdf_result = $this->encryption->hkdf('foobar', 'sha384', NULL, 48 * 255); + $this->assertEquals( + 12240, + defined('MB_OVERLOAD_STRING') + ? mb_strlen($hkdf_result, '8bit') + : strlen($hkdf_result) + ); + $this->assertFalse($this->encryption->hkdf('foobar', 'sha224', NULL, 28 * 255 + 1)); + + // CI-specific test for an invalid digest + $this->assertFalse($this->encryption->hkdf('fobar', 'sha1')); + } + + // -------------------------------------------------------------------- + + /** + * _get_params() test + */ + public function test__get_params() + { + $key = str_repeat("\x0", 16); + + // Invalid custom parameters + $params = array( + // No cipher, mode or key + array('cipher' => 'aes-128', 'mode' => 'cbc'), + array('cipher' => 'aes-128', 'key' => $key), + array('mode' => 'cbc', 'key' => $key), + // No HMAC key or not a valid digest + array('cipher' => 'aes-128', 'mode' => 'cbc', 'key' => $key), + array('cipher' => 'aes-128', 'mode' => 'cbc', 'key' => $key, 'hmac_digest' => 'sha1', 'hmac_key' => $key), + // Invalid mode + array('cipher' => 'aes-128', 'mode' => 'foo', 'key' => $key, 'hmac_digest' => 'sha256', 'hmac_key' => $key) + ); + + for ($i = 0, $c = count($params); $i < $c; $i++) + { + $this->assertFalse($this->encryption->__get_params($params[$i])); + } + + // Valid parameters + $params = array( + 'cipher' => 'aes-128', + 'mode' => 'cbc', + 'key' => str_repeat("\x0", 16), + 'hmac_key' => str_repeat("\x0", 16) + ); + + $this->assertTrue(is_array($this->encryption->__get_params($params))); + + $params['base64'] = TRUE; + $params['hmac_digest'] = 'sha512'; + + // Including all parameters + $params = array( + 'cipher' => 'aes-128', + 'mode' => 'cbc', + 'key' => str_repeat("\x0", 16), + 'raw_data' => TRUE, + 'hmac_key' => str_repeat("\x0", 16), + 'hmac_digest' => 'sha256' + ); + + $output = $this->encryption->__get_params($params); + unset($output['handle'], $output['cipher'], $params['raw_data'], $params['cipher']); + $params['base64'] = FALSE; + $this->assertEquals($params, $output); + + // HMAC disabled + unset($params['hmac_key'], $params['hmac_digest']); + $params['hmac'] = $params['raw_data'] = FALSE; + $params['cipher'] = 'aes-128'; + $output = $this->encryption->__get_params($params); + unset($output['handle'], $output['cipher'], $params['hmac'], $params['raw_data'], $params['cipher']); + $params['base64'] = TRUE; + $params['hmac_digest'] = $params['hmac_key'] = NULL; + $this->assertEquals($params, $output); + } + + // -------------------------------------------------------------------- + + /** + * initialize(), encrypt(), decrypt() test + * + * Testing the three methods separately is not realistic as they are + * designed to work together. A more thorough test for initialize() + * though is the OpenSSL/MCrypt compatibility test. + * + * @depends test_hkdf + * @depends test__get_params + */ + public function test_initialize_encrypt_decrypt() + { + $message = 'This is a plain-text message.'; + $key = "\xd0\xc9\x08\xc4\xde\x52\x12\x6e\xf8\xcc\xdb\x03\xea\xa0\x3a\x5c"; + + // Default state (AES-128/Rijndael-128 in CBC mode) + $this->encryption->initialize(array('key' => $key)); + + // Was the key properly set? + $this->assertEquals($key, $this->encryption->get_key()); + + $this->assertEquals($message, $this->encryption->decrypt($this->encryption->encrypt($message))); + + // Try DES in ECB mode, just for the sake of changing stuff + $this->encryption->initialize(array('cipher' => 'des', 'mode' => 'ecb', 'key' => substr($key, 0, 8))); + $this->assertEquals($message, $this->encryption->decrypt($this->encryption->encrypt($message))); + } + + // -------------------------------------------------------------------- + + /** + * encrypt(), decrypt test with custom parameters + * + * @depends test___get_params + */ + public function test_encrypt_decrypt_custom() + { + $message = 'Another plain-text message.'; + + // A random invalid parameter + $this->assertFalse($this->encryption->encrypt($message, array('foo'))); + $this->assertFalse($this->encryption->decrypt($message, array('foo'))); + + // No HMAC, binary output + $params = array( + 'cipher' => 'tripledes', + 'mode' => 'cfb', + 'key' => str_repeat("\x1", 16), + 'base64' => FALSE, + 'hmac' => FALSE + ); + + $ciphertext = $this->encryption->encrypt($message, $params); + + $this->assertEquals($message, $this->encryption->decrypt($ciphertext, $params)); + } + + // -------------------------------------------------------------------- + + /** + * _mcrypt_get_handle() test + */ + public function test__mcrypt_get_handle() + { + if ($this->encryption->drivers['mcrypt'] === FALSE) + { + return $this->markTestSkipped('Cannot test MCrypt because it is not available.'); + } + elseif (version_compare(PHP_VERSION, '7.1.0-alpha', '>=')) + { + return $this->markTestSkipped('ext/mcrypt is deprecated since PHP 7.1 and will generate notices here.'); + } + + $this->assertTrue(is_resource($this->encryption->__driver_get_handle('mcrypt', 'rijndael-128', 'cbc'))); + } + + // -------------------------------------------------------------------- + + /** + * _openssl_get_handle() test + */ + public function test__openssl_mcrypt_get_handle() + { + if ($this->encryption->drivers['openssl'] === FALSE) + { + return $this->markTestSkipped('Cannot test OpenSSL because it is not available.'); + } + + $this->assertEquals('aes-128-cbc', $this->encryption->__driver_get_handle('openssl', 'aes-128', 'cbc')); + $this->assertEquals('rc4-40', $this->encryption->__driver_get_handle('openssl', 'rc4-40', 'stream')); + } + + // -------------------------------------------------------------------- + + /** + * OpenSSL/MCrypt portability test + * + * Amongst the obvious stuff, _cipher_alias() is also tested here. + */ + public function test_portability() + { + if ( ! $this->encryption->drivers['mcrypt'] OR ! $this->encryption->drivers['openssl']) + { + $this->markTestSkipped('Both MCrypt and OpenSSL support are required for portability tests.'); + return; + } + elseif (version_compare(PHP_VERSION, '7.1.0-alpha', '>=')) + { + return $this->markTestSkipped('ext/mcrypt is deprecated since PHP 7.1 and will generate notices here.'); + } + + $message = 'This is a message encrypted via MCrypt and decrypted via OpenSSL, or vice-versa.'; + + // Format is: , , + $portable = array( + array('aes-128', 'cbc', 16), + array('aes-128', 'cfb', 16), + array('aes-128', 'cfb8', 16), + array('aes-128', 'ofb', 16), + array('aes-128', 'ecb', 16), + array('aes-128', 'ctr', 16), + array('aes-192', 'cbc', 24), + array('aes-192', 'cfb', 24), + array('aes-192', 'cfb8', 24), + array('aes-192', 'ofb', 24), + array('aes-192', 'ecb', 24), + array('aes-192', 'ctr', 24), + array('aes-256', 'cbc', 32), + array('aes-256', 'cfb', 32), + array('aes-256', 'cfb8', 32), + array('aes-256', 'ofb', 32), + array('aes-256', 'ecb', 32), + array('aes-256', 'ctr', 32), + array('des', 'cbc', 7), + array('des', 'cfb', 7), + array('des', 'cfb8', 7), + array('des', 'ofb', 7), + array('des', 'ecb', 7), + array('tripledes', 'cbc', 7), + array('tripledes', 'cfb', 7), + array('tripledes', 'cfb8', 7), + array('tripledes', 'ofb', 7), + array('tripledes', 'cbc', 14), + array('tripledes', 'cfb', 14), + array('tripledes', 'cfb8', 14), + array('tripledes', 'ofb', 14), + array('tripledes', 'cbc', 21), + array('tripledes', 'cfb', 21), + array('tripledes', 'cfb8', 21), + array('tripledes', 'ofb', 21), + array('blowfish', 'cbc', 16), + array('blowfish', 'cfb', 16), + array('blowfish', 'ofb', 16), + array('blowfish', 'ecb', 16), + array('blowfish', 'cbc', 56), + array('blowfish', 'cfb', 56), + array('blowfish', 'ofb', 56), + array('blowfish', 'ecb', 56), + array('cast5', 'cbc', 11), + array('cast5', 'cfb', 11), + array('cast5', 'ofb', 11), + array('cast5', 'ecb', 11), + array('cast5', 'cbc', 16), + array('cast5', 'cfb', 16), + array('cast5', 'ofb', 16), + array('cast5', 'ecb', 16), + array('rc4', 'stream', 5), + array('rc4', 'stream', 8), + array('rc4', 'stream', 16), + array('rc4', 'stream', 32), + array('rc4', 'stream', 64), + array('rc4', 'stream', 128), + array('rc4', 'stream', 256) + ); + $driver_index = array('mcrypt', 'openssl'); + + foreach ($portable as &$test) + { + // Add some randomness to the selected driver + $driver = mt_rand(0,1); + $params = array( + 'driver' => $driver_index[$driver], + 'cipher' => $test[0], + 'mode' => $test[1], + 'key' => openssl_random_pseudo_bytes($test[2]) + ); + + $this->encryption->initialize($params); + $ciphertext = $this->encryption->encrypt($message); + + $driver = (int) ! $driver; + $params['driver'] = $driver_index[$driver]; + + $this->encryption->initialize($params); + $this->assertEquals($message, $this->encryption->decrypt($ciphertext)); + } + } + + // -------------------------------------------------------------------- + + /** + * __get() test + */ + public function test_magic_get() + { + $this->assertNull($this->encryption->foo); + $this->assertEquals(array('mcrypt', 'openssl'), array_keys($this->encryption->drivers)); + + // 'stream' mode is translated into an empty string for OpenSSL + $this->encryption->initialize(array('cipher' => 'rc4', 'mode' => 'stream')); + $this->assertEquals('stream', $this->encryption->mode); + } + +} diff --git a/user_guide_src/source/libraries/encryption.rst b/user_guide_src/source/libraries/encryption.rst new file mode 100644 index 000000000000..b16511d4d3b2 --- /dev/null +++ b/user_guide_src/source/libraries/encryption.rst @@ -0,0 +1,585 @@ +################## +Encryption Library +################## + +.. important:: DO NOT use this or any other *encryption* library for + user password storage! Passwords must be *hashed* instead, and you + should do that via PHP's own `Password Hashing extension + `_. + +The Encryption Library provides two-way data encryption. To do so in +a cryptographically secure way, it utilizes PHP extensions that are +unfortunately not always available on all systems. +You must meet one of the following dependencies in order to use this +library: + +- `OpenSSL `_ +- `MCrypt `_ (and `MCRYPT_DEV_URANDOM` availability) + +If neither of the above dependencies is met, we simply cannot offer +you a good enough implementation to meet the high standards required +for proper cryptography. + +.. contents:: + :local: + +.. raw:: html + +
+ +**************************** +Using the Encryption Library +**************************** + +Initializing the Class +====================== + +Like most other classes in CodeIgniter, the Encryption library is +initialized in your controller using the ``$this->load->library()`` +method:: + + $this->load->library('encryption'); + +Once loaded, the Encryption library object will be available using:: + + $this->encryption + +Default behavior +================ + +By default, the Encryption Library will use the AES-128 cipher in CBC +mode, using your configured *encryption_key* and SHA512 HMAC authentication. + +.. note:: AES-128 is chosen both because it is proven to be strong and + because of its wide availability across different cryptographic + software and programming languages' APIs. + +However, the *encryption_key* is not used as is. + +If you are somewhat familiar with cryptography, you should already know +that a HMAC also requires a secret key and using the same key for both +encryption and authentication is a bad practice. + +Because of that, two separate keys are derived from your already configured +*encryption_key*: one for encryption and one for authentication. This is +done via a technique called `HMAC-based Key Derivation Function +`_ (HKDF). + +Setting your encryption_key +=========================== + +An *encryption key* is a piece of information that controls the +cryptographic process and permits a plain-text string to be encrypted, +and afterwards - decrypted. It is the secret "ingredient" in the whole +process that allows you to be the only one who is able to decrypt data +that you've decided to hide from the eyes of the public. +After one key is used to encrypt data, that same key provides the **only** +means to decrypt it, so not only must you chose one carefully, but you +must not lose it or you will also lose access to the data. + +It must be noted that to ensure maximum security, such key *should* not +only be as strong as possible, but also often changed. Such behavior +however is rarely practical or possible to implement, and that is why +CodeIgniter gives you the ability to configure a single key that is to be +used (almost) every time. + +It goes without saying that you should guard your key carefully. Should +someone gain access to your key, the data will be easily decrypted. If +your server is not totally under your control it's impossible to ensure +key security so you may want to think carefully before using it for +anything that requires high security, like storing credit card numbers. + +Your encryption key **must** be as long as the encyption algorithm in use +allows. For AES-128, that's 128 bits or 16 bytes (charcters) long. +You will find a table below that shows the supported key lengths of +different ciphers. + +The key should be as random as possible and it **must not** be a regular +text string, nor the output of a hashing function, etc. In order to create +a proper key, you must use the Encryption library's ``create_key()`` method +:: + + // $key will be assigned a 16-byte (128-bit) random key + $key = $this->encryption->create_key(16); + +The key can be either stored in your *application/config/config.php*, or +you can design your own storage mechanism and pass the key dynamically +when encrypting/decrypting. + +To save your key to your *application/config/config.php*, open the file +and set:: + + $config['encryption_key'] = 'YOUR KEY'; + +You'll notice that the ``create_key()`` method outputs binary data, which +is hard to deal with (i.e. a copy-paste may damage it), so you may use +``bin2hex()``, ``hex2bin()`` or Base64-encoding to work with the key in +a more friendly manner. For example:: + + // Get a hex-encoded representation of the key: + $key = bin2hex($this->encryption->create_key(16)); + + // Put the same value in your config with hex2bin(), + // so that it is still passed as binary to the library: + $config['encryption_key'] = hex2bin(); + +.. _ciphers-and-modes: + +Supported encryption ciphers and modes +====================================== + +.. note:: The terms 'cipher' and 'encryption algorithm' are interchangeable. + +Portable ciphers +---------------- + +Because MCrypt and OpenSSL (also called drivers throughout this document) +each support different sets of encryption algorithms and often implement +them in different ways, our Encryption library is designed to use them in +a portable fashion, or in other words - it enables you to use them +interchangeably, at least for the ciphers supported by both drivers. + +It is also implemented in a way that aims to match the standard +implementations in other programming languages and libraries. + +Here's a list of the so called "portable" ciphers, where +"CodeIgniter name" is the string value that you'd have to pass to the +Encryption library to use that cipher: + +======================== ================== ============================ =============================== +Cipher name CodeIgniter name Key lengths (bits / bytes) Supported modes +======================== ================== ============================ =============================== +AES-128 / Rijndael-128 aes-128 128 / 16 CBC, CTR, CFB, CFB8, OFB, ECB +AES-192 aes-192 192 / 24 CBC, CTR, CFB, CFB8, OFB, ECB +AES-256 aes-256 256 / 32 CBC, CTR, CFB, CFB8, OFB, ECB +DES des 56 / 7 CBC, CFB, CFB8, OFB, ECB +TripleDES tripledes 56 / 7, 112 / 14, 168 / 21 CBC, CFB, CFB8, OFB +Blowfish blowfish 128-448 / 16-56 CBC, CFB, OFB, ECB +CAST5 / CAST-128 cast5 88-128 / 11-16 CBC, CFB, OFB, ECB +RC4 / ARCFour rc4 40-2048 / 5-256 Stream +======================== ================== ============================ =============================== + +.. important:: Because of how MCrypt works, if you fail to provide a key + with the appropriate length, you might end up using a different + algorithm than the one configured, so be really careful with that! + +.. note:: In case it isn't clear from the above table, Blowfish, CAST5 + and RC4 support variable length keys. That is, any number in the + shown ranges is valid, although in bit terms that only happens + in 8-bit increments. + +.. note:: Even though CAST5 supports key lengths lower than 128 bits + (16 bytes), in fact they will just be zero-padded to the + maximum length, as specified in `RFC 2144 + `_. + +.. note:: Blowfish supports key lengths as small as 32 bits (4 bytes), but + our tests have shown that only lengths of 128 bits (16 bytes) or + higher are properly supported by both MCrypt and OpenSSL. It is + also a bad practice to use such low-length keys anyway. + +Driver-specific ciphers +----------------------- + +As noted above, MCrypt and OpenSSL support different sets of encryption +ciphers. For portability reasons and because we haven't tested them +properly, we do not advise you to use the ones that are driver-specific, +but regardless, here's a list of most of them: + + +============== ========= ============================== ========================================= +Cipher name Driver Key lengths (bits / bytes) Supported modes +============== ========= ============================== ========================================= +AES-128 OpenSSL 128 / 16 CBC, CTR, CFB, CFB8, OFB, ECB, XTS +AES-192 OpenSSL 192 / 24 CBC, CTR, CFB, CFB8, OFB, ECB, XTS +AES-256 OpenSSL 256 / 32 CBC, CTR, CFB, CFB8, OFB, ECB, XTS +Rijndael-128 MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB +Rijndael-192 MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB +Rijndael-256 MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB +GOST MCrypt 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB +Twofish MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB +CAST-128 MCrypt 40-128 / 5-16 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB +CAST-256 MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB +Loki97 MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB +SaferPlus MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB +Serpent MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB +XTEA MCrypt 128 / 16 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB +RC2 MCrypt 8-1024 / 1-128 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB +RC2 OpenSSL 8-1024 / 1-128 CBC, CFB, OFB, ECB +Camellia-128 OpenSSL 128 / 16 CBC, CFB, CFB8, OFB, ECB +Camellia-192 OpenSSL 192 / 24 CBC, CFB, CFB8, OFB, ECB +Camellia-256 OpenSSL 256 / 32 CBC, CFB, CFB8, OFB, ECB +Seed OpenSSL 128 / 16 CBC, CFB, OFB, ECB +============== ========= ============================== ========================================= + +.. note:: If you wish to use one of those ciphers, you'd have to pass + its name in lower-case to the Encryption library. + +.. note:: You've probably noticed that all AES cipers (and Rijndael-128) + are also listed in the portable ciphers list. This is because + drivers support different modes for these ciphers. Also, it is + important to note that AES-128 and Rijndael-128 are actually + the same cipher, but **only** when used with a 128-bit key. + +.. note:: CAST-128 / CAST-5 is also listed in both the portable and + driver-specific ciphers list. This is because OpenSSL's + implementation doesn't appear to be working correctly with + key sizes of 80 bits and lower. + +.. note:: RC2 is listed as supported by both MCrypt and OpenSSL. + However, both drivers implement them differently and they + are not portable. It is probably worth noting that we only + found one obscure source confirming that it is MCrypt that + is not properly implementing it. + +.. _encryption-modes: + +Encryption modes +---------------- + +Different modes of encryption have different characteristics and serve +for different purposes. Some are stronger than others, some are faster +and some offer extra features. +We are not going in depth into that here, we'll leave that to the +cryptography experts. The table below is to provide brief informational +reference to our more experienced users. If you are a beginner, just +stick to the CBC mode - it is widely accepted as strong and secure for +general purposes. + +=========== ================== ================= =================================================================================================================================================== +Mode name CodeIgniter name Driver support Additional info +=========== ================== ================= =================================================================================================================================================== +CBC cbc MCrypt, OpenSSL A safe default choice +CTR ctr MCrypt, OpenSSL Considered as theoretically better than CBC, but not as widely available +CFB cfb MCrypt, OpenSSL N/A +CFB8 cfb8 MCrypt, OpenSSL Same as CFB, but operates in 8-bit mode (not recommended). +OFB ofb MCrypt, OpenSSL N/A +OFB8 ofb8 MCrypt Same as OFB, but operates in 8-bit mode (not recommended). +ECB ecb MCrypt, OpenSSL Ignores IV (not recommended). +XTS xts OpenSSL Usually used for encrypting random access data such as RAM or hard-disk storage. +Stream stream MCrypt, OpenSSL This is not actually a mode, it just says that a stream cipher is being used. Required because of the general cipher+mode initialization process. +=========== ================== ================= =================================================================================================================================================== + +Message Length +============== + +It's probably important for you to know that an encrypted string is usually +longer than the original, plain-text string (depending on the cipher). + +This is influenced by the cipher algorithm itself, the IV prepended to the +cipher-text and the HMAC authentication message that is also prepended. +Furthermore, the encrypted message is also Base64-encoded so that it is safe +for storage and transmission, regardless of a possible character set in use. + +Keep this information in mind when selecting your data storage mechanism. +Cookies, for example, can only hold 4K of information. + +.. _configuration: + +Configuring the library +======================= + +For usability, performance, but also historical reasons tied to our old +:doc:`Encrypt Class `, the Encryption library is designed to +use repeatedly the same driver, encryption cipher, mode and key. + +As noted in the "Default behavior" section above, this means using an +auto-detected driver (OpenSSL has a higher priority), the AES-128 ciper +in CBC mode, and your ``$config['encryption_key']`` value. + +If you wish to change that however, you need to use the ``initialize()`` +method. It accepts an associative array of parameters, all of which are +optional: + +======== =============================================== +Option Possible values +======== =============================================== +driver 'mcrypt', 'openssl' +cipher Cipher name (see :ref:`ciphers-and-modes`) +mode Encryption mode (see :ref:`encryption-modes`) +key Encryption key +======== =============================================== + +For example, if you were to change the encryption algorithm and +mode to AES-256 in CTR mode, this is what you should do:: + + $this->encryption->initialize( + array( + 'cipher' => 'aes-256', + 'mode' => 'ctr', + 'key' => '' + ) + ); + +Note that we only mentioned that you want to change the ciper and mode, +but we also included a key in the example. As previously noted, it is +important that you choose a key with a proper size for the used algorithm. + +There's also the ability to change the driver, if for some reason you +have both, but want to use MCrypt instead of OpenSSL:: + + // Switch to the MCrypt driver + $this->encryption->initialize(array('driver' => 'mcrypt')); + + // Switch back to the OpenSSL driver + $this->encryption->initialize(array('driver' => 'openssl')); + +Encrypting and decrypting data +============================== + +Encrypting and decrypting data with the already configured library +settings is simple. As simple as just passing the string to the +``encrypt()`` and/or ``decrypt()`` methods:: + + $plain_text = 'This is a plain-text message!'; + $ciphertext = $this->encryption->encrypt($plain_text); + + // Outputs: This is a plain-text message! + echo $this->encryption->decrypt($ciphertext); + +And that's it! The Encryption library will do everything necessary +for the whole process to be cryptographically secure out-of-the-box. +You don't need to worry about it. + +.. important:: Both methods will return FALSE in case of an error. + While for ``encrypt()`` this can only mean incorrect + configuration, you should always check the return value + of ``decrypt()`` in production code. + +How it works +------------ + +If you must know how the process works, here's what happens under +the hood: + +- ``$this->encryption->encrypt($plain_text)`` + + #. Derive an encryption key and a HMAC key from your configured + *encryption_key* via HKDF, using the SHA-512 digest algorithm. + + #. Generate a random initialization vector (IV). + + #. Encrypt the data via AES-128 in CBC mode (or another previously + configured cipher and mode), using the above-mentioned derived + encryption key and IV. + + #. Prepend said IV to the resulting cipher-text. + + #. Base64-encode the resulting string, so that it can be safely + stored or transferred without worrying about character sets. + + #. Create a SHA-512 HMAC authentication message using the derived + HMAC key to ensure data integrity and prepend it to the Base64 + string. + +- ``$this->encryption->decrypt($ciphertext)`` + + #. Derive an encryption key and a HMAC key from your configured + *encryption_key* via HKDF, using the SHA-512 digest algorithm. + Because your configured *encryption_key* is the same, this + will produce the same result as in the ``encrypt()`` method + above - otherwise you won't be able to decrypt it. + + #. Check if the string is long enough, separate the HMAC out of + it and validate if it is correct (this is done in a way that + prevents timing attacks against it). Return FALSE if either of + the checks fails. + + #. Base64-decode the string. + + #. Separate the IV out of the cipher-text and decrypt the said + cipher-text using that IV and the derived encryption key. + +.. _custom-parameters: + +Using custom parameters +----------------------- + +Let's say you have to interact with another system that is out +of your control and uses another method to encrypt data. A +method that will most certainly not match the above-described +sequence and probably not use all of the steps either. + +The Encryption library allows you to change how its encryption +and decryption processes work, so that you can easily tailor a +custom solution for such situations. + +.. note:: It is possible to use the library in this way, without + setting an *encryption_key* in your configuration file. + +All you have to do is to pass an associative array with a few +parameters to either the ``encrypt()`` or ``decrypt()`` method. +Here's an example:: + + // Assume that we have $ciphertext, $key and $hmac_key + // from on outside source + + $message = $this->encryption->decrypt( + $ciphertext, + array( + 'cipher' => 'blowfish', + 'mode' => 'cbc', + 'key' => $key, + 'hmac_digest' => 'sha256', + 'hmac_key' => $hmac_key + ) + ); + +In the above example, we are decrypting a message that was encrypted +using the Blowfish cipher in CBC mode and authenticated via a SHA-256 +HMAC. + +.. important:: Note that both 'key' and 'hmac_key' are used in this + example. When using custom parameters, encryption and HMAC keys + are not derived like the default behavior of the library is. + +Below is a list of the available options. + +However, unless you really need to and you know what you are doing, +we advise you to not change the encryption process as this could +impact security, so please do so with caution. + +============= =============== ============================= ====================================================== +Option Default value Mandatory / Optional Description +============= =============== ============================= ====================================================== +cipher N/A Yes Encryption algorithm (see :ref:`ciphers-and-modes`). +mode N/A Yes Encryption mode (see :ref:`encryption-modes`). +key N/A Yes Encryption key. +hmac TRUE No Whether to use a HMAC. + Boolean. If set to FALSE, then *hmac_digest* and + *hmac_key* will be ignored. +hmac_digest sha512 No HMAC message digest algorithm (see :ref:`digests`). +hmac_key N/A Yes, unless *hmac* is FALSE HMAC key. +raw_data FALSE No Whether the cipher-text should be raw. + Boolean. If set to TRUE, then Base64 encoding and + decoding will not be performed and HMAC will not + be a hexadecimal string. +============= =============== ============================= ====================================================== + +.. important:: ``encrypt()`` and ``decrypt()`` will return FALSE if + a mandatory parameter is not provided or if a provided + value is incorrect. This includes *hmac_key*, unless *hmac* + is set to FALSE. + +.. _digests: + +Supported HMAC authentication algorithms +---------------------------------------- + +For HMAC message authentication, the Encryption library supports +usage of the SHA-2 family of algorithms: + +=========== ==================== ============================ +Algorithm Raw length (bytes) Hex-encoded length (bytes) +=========== ==================== ============================ +sha512 64 128 +sha384 48 96 +sha256 32 64 +sha224 28 56 +=========== ==================== ============================ + +The reason for not including other popular algorithms, such as +MD5 or SHA1 is that they are no longer considered secure enough +and as such, we don't want to encourage their usage. +If you absolutely need to use them, it is easy to do so via PHP's +native `hash_hmac() `_ function. + +Stronger algorithms of course will be added in the future as they +appear and become widely available. + +*************** +Class Reference +*************** + +.. php:class:: CI_Encryption + + .. php:method:: initialize($params) + + :param array $params: Configuration parameters + :returns: CI_Encryption instance (method chaining) + :rtype: CI_Encryption + + Initializes (configures) the library to use a different + driver, cipher, mode or key. + + Example:: + + $this->encryption->initialize( + array('mode' => 'ctr') + ); + + Please refer to the :ref:`configuration` section for detailed info. + + .. php:method:: encrypt($data[, $params = NULL]) + + :param string $data: Data to encrypt + :param array $params: Optional parameters + :returns: Encrypted data or FALSE on failure + :rtype: string + + Encrypts the input data and returns its ciphertext. + + Example:: + + $ciphertext = $this->encryption->encrypt('My secret message'); + + Please refer to the :ref:`custom-parameters` section for information + on the optional parameters. + + .. php:method:: decrypt($data[, $params = NULL]) + + :param string $data: Data to decrypt + :param array $params: Optional parameters + :returns: Decrypted data or FALSE on failure + :rtype: string + + Decrypts the input data and returns it in plain-text. + + Example:: + + echo $this->encryption->decrypt($ciphertext); + + Please refer to the :ref:`custom-parameters` secrion for information + on the optional parameters. + + .. php:method:: create_key($length) + + :param int $length: Output length + :returns: A pseudo-random cryptographic key with the specified length, or FALSE on failure + :rtype: string + + Creates a cryptographic key by fetching random data from + the operating system's sources (i.e. /dev/urandom). + + .. php:method:: hkdf($key[, $digest = 'sha512'[, $salt = NULL[, $length = NULL[, $info = '']]]]) + + :param string $key: Input key material + :param string $digest: A SHA-2 family digest algorithm + :param string $salt: Optional salt + :param int $length: Optional output length + :param string $info: Optional context/application-specific info + :returns: A pseudo-random key or FALSE on failure + :rtype: string + + Derives a key from another, presumably weaker key. + + This method is used internally to derive an encryption and HMAC key + from your configured *encryption_key*. + + It is publicly available due to its otherwise general purpose. It is + described in `RFC 5869 `_. + + However, as opposed to the description in RFC 5869, this implementation + doesn't support SHA1. + + Example:: + + $hmac_key = $this->encryption->hkdf( + $key, + 'sha512', + NULL, + NULL, + 'authentication' + ); + + // $hmac_key is a pseudo-random key with a length of 64 bytes \ No newline at end of file From c4b69b073ceed7c309f622d1adabf731633df6e3 Mon Sep 17 00:00:00 2001 From: Master Yoda Date: Tue, 9 May 2017 08:57:18 -0700 Subject: [PATCH 02/18] Refactor Encryption --- application/Config/Encryption.php | 13 +++++++ system/Encryption/Encryption.php | 8 +++- tests/_support/Encryption/MockEncryption.php | 39 +++++++++++++++++++ ...Encryption_test.php => EncryptionTest.php} | 17 ++++---- 4 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 application/Config/Encryption.php create mode 100644 tests/_support/Encryption/MockEncryption.php rename tests/system/Encryption/{Encryption_test.php => EncryptionTest.php} (97%) diff --git a/application/Config/Encryption.php b/application/Config/Encryption.php new file mode 100644 index 000000000000..3806b23e201e --- /dev/null +++ b/application/Config/Encryption.php @@ -0,0 +1,13 @@ +config = $config; + $this->_drivers = array( 'mcrypt' => defined('MCRYPT_DEV_URANDOM'), 'openssl' => extension_loaded('openssl') @@ -159,7 +163,7 @@ public function __construct(array $params = array()) isset(self::$func_overload) OR self::$func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload')); $this->initialize($params); - if (!isset($this->_key) && self::strlen($key = config_item('encryption_key')) > 0) + if (!isset($this->_key) && self::strlen($key = $this->config->encryptionKey) > 0) { $this->_key = $key; } diff --git a/tests/_support/Encryption/MockEncryption.php b/tests/_support/Encryption/MockEncryption.php new file mode 100644 index 000000000000..b621e3653e34 --- /dev/null +++ b/tests/_support/Encryption/MockEncryption.php @@ -0,0 +1,39 @@ +_get_params($params); + } + + // -------------------------------------------------------------------- + + /** + * get_key() + * + * Allows checking for key changes. + */ + public function get_key() + { + return $this->_key; + } + + // -------------------------------------------------------------------- + + /** + * __driver_get_handle() + * + * Allows checking for _mcrypt_get_handle(), _openssl_get_handle() + */ + public function __driver_get_handle($driver, $cipher, $mode) + { + return $this->{'_'.$driver.'_get_handle'}($cipher, $mode); + } + +} \ No newline at end of file diff --git a/tests/system/Encryption/Encryption_test.php b/tests/system/Encryption/EncryptionTest.php similarity index 97% rename from tests/system/Encryption/Encryption_test.php rename to tests/system/Encryption/EncryptionTest.php index 99c5d4b9dafe..96b7efbb5840 100644 --- a/tests/system/Encryption/Encryption_test.php +++ b/tests/system/Encryption/EncryptionTest.php @@ -1,10 +1,13 @@ -encryption = new Mock_Libraries_Encryption(); + $this->encryption = new MockEncryption(); } // -------------------------------------------------------------------- @@ -12,7 +15,7 @@ public function set_up() /** * __construct test * - * Covers behavior with $config['encryption_key'] set or not + * Covers behavior with config encryptionKey set or not */ public function test___construct() { @@ -21,11 +24,11 @@ public function test___construct() // Try with an empty value $this->ci_set_config('encryption_key'); - $this->encrypt = new Mock_Libraries_Encryption(); + $this->encrypt = new MockEncryption(); $this->assertNull($this->encrypt->get_key()); $this->ci_set_config('encryption_key', str_repeat("\x0", 16)); - $this->encrypt = new Mock_Libraries_Encryption(); + $this->encrypt = new MockEncryption(); $this->assertEquals(str_repeat("\x0", 16), $this->encrypt->get_key()); } From 76c778d91200d923411351aad913e72a44c83c2a Mon Sep 17 00:00:00 2001 From: Master Yoda Date: Wed, 10 May 2017 00:27:40 -0700 Subject: [PATCH 03/18] Encryption port from CI3 --- application/Config/Encryption.php | 2 +- system/Config/Services.php | 20 +++++ system/Encryption/Encryption.php | 36 ++++++--- tests/system/Encryption/EncryptionTest.php | 8 +- .../source/libraries/encryption.rst | 75 ++++++++----------- user_guide_src/source/libraries/index.rst | 5 +- 6 files changed, 87 insertions(+), 59 deletions(-) diff --git a/application/Config/Encryption.php b/application/Config/Encryption.php index 3806b23e201e..7ea3165da9f0 100644 --- a/application/Config/Encryption.php +++ b/application/Config/Encryption.php @@ -8,6 +8,6 @@ class Encryption extends BaseConfig { // If you use the Encryption class, you must set an encryption key. // See the user guide for more info. - public $encryptionKey = ''; + public $key = ''; } diff --git a/system/Config/Services.php b/system/Config/Services.php index 43024dd79f07..cb91c848ab90 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -161,6 +161,26 @@ public static function curlrequest(array $options = [], $response = null, \Confi //-------------------------------------------------------------------- + /** + * The Encryption class provides two-way encryption. + */ + public static function encrypter(\Config\Encryption $config = null) + { + if ($getShared === true) + { + return self::getSharedInstance('encrypter', $config); + } + + if (! is_object($config)) + { + $config = new \Config\Encryption(); + } + + return new \CodeIgniter\Encryption\Encryption($config); + } + + //-------------------------------------------------------------------- + /** * The Exceptions class holds the methods that handle: * diff --git a/system/Encryption/Encryption.php b/system/Encryption/Encryption.php index 6bf7e08b556a..0fe7778755aa 100644 --- a/system/Encryption/Encryption.php +++ b/system/Encryption/Encryption.php @@ -38,6 +38,14 @@ * @filesource */ +/** + * Encryption exception + * + */ +class EncryptionException extends \Exception +{ +} + /** * CodeIgniter Encryption Class * @@ -143,6 +151,8 @@ class Encryption { * * @param array $params Configuration parameters * @return void + * + * @throws \CodeIgniter\Encryption\EncryptionException */ public function __construct($config = null) { @@ -150,6 +160,8 @@ public function __construct($config = null) $config = new \Config\Encryption(); $this->config = $config; + $params = (array) $this->config; + $this->_drivers = array( 'mcrypt' => defined('MCRYPT_DEV_URANDOM'), 'openssl' => extension_loaded('openssl') @@ -157,13 +169,13 @@ public function __construct($config = null) if (!$this->_drivers['mcrypt'] && !$this->_drivers['openssl']) { - throw new \Exception('Encryption: Unable to find an available encryption driver.'); + throw new EncryptionException('Unable to find an available encryption driver.'); } isset(self::$func_overload) OR self::$func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload')); $this->initialize($params); - if (!isset($this->_key) && self::strlen($key = $this->config->encryptionKey) > 0) + if (!isset($this->_key) && self::strlen($key = $this->config->key) > 0) { $this->_key = $key; } @@ -178,6 +190,8 @@ public function __construct($config = null) * * @param array $params Configuration parameters * @return CI_Encryption + * + * @throws \CodeIgniter\Encryption\EncryptionException */ public function initialize(array $params) { @@ -190,11 +204,11 @@ public function initialize(array $params) $this->_driver = $params['driver']; } else { - throw new \Exception("Encryption: Driver '" . $params['driver'] . "' is not available."); + throw new EncryptionException("Driver '" . $params['driver'] . "' is not available."); } } else { - throw new \Exception("Encryption: Unknown driver '" . $params['driver'] . "' cannot be configured."); + throw new EncryptionException("Unknown driver '" . $params['driver'] . "' cannot be configured."); } } @@ -218,6 +232,8 @@ public function initialize(array $params) * * @param array $params Configuration parameters * @return void + * + * @throws \CodeIgniter\Encryption\EncryptionException */ protected function _mcrypt_initialize($params) { @@ -228,7 +244,7 @@ protected function _mcrypt_initialize($params) if (!in_array($params['cipher'], mcrypt_list_algorithms(), TRUE)) { - throw new \Exception('Encryption: MCrypt cipher ' . strtoupper($params['cipher']) . ' is not available.'); + throw new EncryptionException('MCrypt cipher ' . strtoupper($params['cipher']) . ' is not available.'); } else { $this->_cipher = $params['cipher']; @@ -240,7 +256,7 @@ protected function _mcrypt_initialize($params) $params['mode'] = strtolower($params['mode']); if (!isset($this->_modes['mcrypt'][$params['mode']])) { - throw new \Exception('Encryption: MCrypt mode ' . strtoupper($params['cipher']) . ' is not available.'); + throw new EncryptionException('MCrypt mode ' . strtoupper($params['cipher']) . ' is not available.'); } else { $this->_mode = $this->_modes['mcrypt'][$params['mode']]; @@ -261,7 +277,7 @@ protected function _mcrypt_initialize($params) log_message('info', 'Encryption: MCrypt cipher ' . strtoupper($this->_cipher) . ' initialized in ' . strtoupper($this->_mode) . ' mode.'); } else { - throw new \Exception('Encryption: Unable to initialize MCrypt with cipher ' . strtoupper($this->_cipher) . ' in ' . strtoupper($this->_mode) . ' mode.'); + throw new EncryptionException('Unable to initialize MCrypt with cipher ' . strtoupper($this->_cipher) . ' in ' . strtoupper($this->_mode) . ' mode.'); } } } @@ -320,7 +336,7 @@ protected function _openssl_initialize($params) * @param int $length Output length * @return string */ - public function create_key($length) + public function createKey($length) { if (function_exists('random_bytes')) { @@ -394,7 +410,7 @@ protected function _mcrypt_encrypt($data, $params) // The greater-than-1 comparison is mostly a work-around for a bug, // where 1 is returned for ARCFour instead of 0. - $iv = (($iv_size = mcrypt_enc_get_iv_size($params['handle'])) > 1) ? $this->create_key($iv_size) : NULL; + $iv = (($iv_size = mcrypt_enc_get_iv_size($params['handle'])) > 1) ? $this->createKey($iv_size) : NULL; if (mcrypt_generic_init($params['handle'], $params['key'], $iv) < 0) { @@ -453,7 +469,7 @@ protected function _openssl_encrypt($data, $params) return FALSE; } - $iv = ($iv_size = openssl_cipher_iv_length($params['handle'])) ? $this->create_key($iv_size) : NULL; + $iv = ($iv_size = openssl_cipher_iv_length($params['handle'])) ? $this->createKey($iv_size) : NULL; $data = openssl_encrypt( $data, $params['handle'], $params['key'], OPENSSL_RAW_DATA, $iv diff --git a/tests/system/Encryption/EncryptionTest.php b/tests/system/Encryption/EncryptionTest.php index 96b7efbb5840..b28a75418d27 100644 --- a/tests/system/Encryption/EncryptionTest.php +++ b/tests/system/Encryption/EncryptionTest.php @@ -23,12 +23,12 @@ public function test___construct() $this->assertNull($this->encryption->get_key()); // Try with an empty value - $this->ci_set_config('encryption_key'); - $this->encrypt = new MockEncryption(); + $config = new \Config\Encryption(); + $this->encrypt = new MockEncryption($config); $this->assertNull($this->encrypt->get_key()); - $this->ci_set_config('encryption_key', str_repeat("\x0", 16)); - $this->encrypt = new MockEncryption(); + $config->key = str_repeat("\x0", 16); + $this->encrypt = new MockEncryption($config); $this->assertEquals(str_repeat("\x0", 16), $this->encrypt->get_key()); } diff --git a/user_guide_src/source/libraries/encryption.rst b/user_guide_src/source/libraries/encryption.rst index b16511d4d3b2..d3b54c0e4c7d 100644 --- a/user_guide_src/source/libraries/encryption.rst +++ b/user_guide_src/source/libraries/encryption.rst @@ -31,37 +31,28 @@ for proper cryptography. Using the Encryption Library **************************** -Initializing the Class -====================== +Like all services in CodeIgniter, it can be loaded via ``Config\Services``:: -Like most other classes in CodeIgniter, the Encryption library is -initialized in your controller using the ``$this->load->library()`` -method:: - - $this->load->library('encryption'); - -Once loaded, the Encryption library object will be available using:: - - $this->encryption + $encrypter = \Config\Services::encrypter(); Default behavior ================ By default, the Encryption Library will use the AES-128 cipher in CBC -mode, using your configured *encryption_key* and SHA512 HMAC authentication. +mode, using your configured *encryption key* and SHA512 HMAC authentication. .. note:: AES-128 is chosen both because it is proven to be strong and because of its wide availability across different cryptographic software and programming languages' APIs. -However, the *encryption_key* is not used as is. +However, the *encryption key* is not used as is. If you are somewhat familiar with cryptography, you should already know that a HMAC also requires a secret key and using the same key for both encryption and authentication is a bad practice. Because of that, two separate keys are derived from your already configured -*encryption_key*: one for encryption and one for authentication. This is +*encryption key*: one for encryption and one for authentication. This is done via a technique called `HMAC-based Key Derivation Function `_ (HKDF). @@ -96,32 +87,32 @@ different ciphers. The key should be as random as possible and it **must not** be a regular text string, nor the output of a hashing function, etc. In order to create -a proper key, you must use the Encryption library's ``create_key()`` method +a proper key, you must use the Encryption library's ``createKey()`` method :: // $key will be assigned a 16-byte (128-bit) random key - $key = $this->encryption->create_key(16); + $key = $encrypter->createKey(16); -The key can be either stored in your *application/config/config.php*, or +The key can be either stored in your *application/Config/Encryption.php*, or you can design your own storage mechanism and pass the key dynamically when encrypting/decrypting. -To save your key to your *application/config/config.php*, open the file +To save your key to your *application/Config/Encryption.php*, open the file and set:: - $config['encryption_key'] = 'YOUR KEY'; + $key = 'YOUR KEY'; -You'll notice that the ``create_key()`` method outputs binary data, which +You'll notice that the ``createKey()`` method outputs binary data, which is hard to deal with (i.e. a copy-paste may damage it), so you may use ``bin2hex()``, ``hex2bin()`` or Base64-encoding to work with the key in a more friendly manner. For example:: // Get a hex-encoded representation of the key: - $key = bin2hex($this->encryption->create_key(16)); + $encoded = bin2hex($encrypter->createKey(16)); // Put the same value in your config with hex2bin(), // so that it is still passed as binary to the library: - $config['encryption_key'] = hex2bin(); + $key = hex2bin(); .. _ciphers-and-modes: @@ -279,13 +270,13 @@ Cookies, for example, can only hold 4K of information. Configuring the library ======================= -For usability, performance, but also historical reasons tied to our old -:doc:`Encrypt Class `, the Encryption library is designed to +For usability, performance, but also historical reasons tied to earlier +encryption in CodeIgniter, the Encryption library is designed to use repeatedly the same driver, encryption cipher, mode and key. As noted in the "Default behavior" section above, this means using an auto-detected driver (OpenSSL has a higher priority), the AES-128 ciper -in CBC mode, and your ``$config['encryption_key']`` value. +in CBC mode, and your ``$key`` value. If you wish to change that however, you need to use the ``initialize()`` method. It accepts an associative array of parameters, all of which are @@ -303,7 +294,7 @@ key Encryption key For example, if you were to change the encryption algorithm and mode to AES-256 in CTR mode, this is what you should do:: - $this->encryption->initialize( + $encrypter->initialize( array( 'cipher' => 'aes-256', 'mode' => 'ctr', @@ -311,7 +302,7 @@ mode to AES-256 in CTR mode, this is what you should do:: ) ); -Note that we only mentioned that you want to change the ciper and mode, +Note that we only mentioned that you want to change the cipher and mode, but we also included a key in the example. As previously noted, it is important that you choose a key with a proper size for the used algorithm. @@ -319,10 +310,10 @@ There's also the ability to change the driver, if for some reason you have both, but want to use MCrypt instead of OpenSSL:: // Switch to the MCrypt driver - $this->encryption->initialize(array('driver' => 'mcrypt')); + $encrypter->initialize(array('driver' => 'mcrypt')); // Switch back to the OpenSSL driver - $this->encryption->initialize(array('driver' => 'openssl')); + $encrypter->initialize(array('driver' => 'openssl')); Encrypting and decrypting data ============================== @@ -332,10 +323,10 @@ settings is simple. As simple as just passing the string to the ``encrypt()`` and/or ``decrypt()`` methods:: $plain_text = 'This is a plain-text message!'; - $ciphertext = $this->encryption->encrypt($plain_text); + $ciphertext = $encrypter->encrypt($plain_text); // Outputs: This is a plain-text message! - echo $this->encryption->decrypt($ciphertext); + echo $encrypter->decrypt($ciphertext); And that's it! The Encryption library will do everything necessary for the whole process to be cryptographically secure out-of-the-box. @@ -352,7 +343,7 @@ How it works If you must know how the process works, here's what happens under the hood: -- ``$this->encryption->encrypt($plain_text)`` +- ``$encrypter->encrypt($plain_text)`` #. Derive an encryption key and a HMAC key from your configured *encryption_key* via HKDF, using the SHA-512 digest algorithm. @@ -372,7 +363,7 @@ the hood: HMAC key to ensure data integrity and prepend it to the Base64 string. -- ``$this->encryption->decrypt($ciphertext)`` +- ``$encrypter->decrypt($ciphertext)`` #. Derive an encryption key and a HMAC key from your configured *encryption_key* via HKDF, using the SHA-512 digest algorithm. @@ -414,7 +405,7 @@ Here's an example:: // Assume that we have $ciphertext, $key and $hmac_key // from on outside source - $message = $this->encryption->decrypt( + $message = $encrypter->decrypt( $ciphertext, array( 'cipher' => 'blowfish', @@ -491,20 +482,20 @@ appear and become widely available. Class Reference *************** -.. php:class:: CI_Encryption +.. php:class:: CodeIgniter\\Encryption\\Encryption .. php:method:: initialize($params) :param array $params: Configuration parameters - :returns: CI_Encryption instance (method chaining) - :rtype: CI_Encryption + :returns: CodeIgniter\\Encryption\\Encryption instance (method chaining) + :rtype: CodeIgniter\\Encryption\\Encryption Initializes (configures) the library to use a different driver, cipher, mode or key. Example:: - $this->encryption->initialize( + $encrypter->initialize( array('mode' => 'ctr') ); @@ -521,7 +512,7 @@ Class Reference Example:: - $ciphertext = $this->encryption->encrypt('My secret message'); + $ciphertext = $encrypter->encrypt('My secret message'); Please refer to the :ref:`custom-parameters` section for information on the optional parameters. @@ -537,12 +528,12 @@ Class Reference Example:: - echo $this->encryption->decrypt($ciphertext); + echo $encrypter->decrypt($ciphertext); Please refer to the :ref:`custom-parameters` secrion for information on the optional parameters. - .. php:method:: create_key($length) + .. php:method:: createKey($length) :param int $length: Output length :returns: A pseudo-random cryptographic key with the specified length, or FALSE on failure @@ -574,7 +565,7 @@ Class Reference Example:: - $hmac_key = $this->encryption->hkdf( + $hmac_key = $encrypter->hkdf( $key, 'sha512', NULL, diff --git a/user_guide_src/source/libraries/index.rst b/user_guide_src/source/libraries/index.rst index 3574314ef1a1..6f8d945a619a 100644 --- a/user_guide_src/source/libraries/index.rst +++ b/user_guide_src/source/libraries/index.rst @@ -10,9 +10,11 @@ Library Reference caching cli content_negotiation - localization curlrequest + encryption + files incomingrequest + localization message pagination request @@ -21,7 +23,6 @@ Library Reference sessions throttler typography - files uploaded_files uri validation From 03366b1098d68575d18362c5025d03a52f8cbaee Mon Sep 17 00:00:00 2001 From: Master Yoda Date: Wed, 10 May 2017 00:34:24 -0700 Subject: [PATCH 04/18] Oops - fixed formatting --- system/Config/AutoloadConfig.php | 1 + system/Config/Services.php | 58 +++++++++----------- system/Encryption/Encryption.php | 14 ++--- tests/_support/Encryption/MockEncryption.php | 8 ++- tests/system/Encryption/EncryptionTest.php | 43 ++++++--------- 5 files changed, 54 insertions(+), 70 deletions(-) diff --git a/system/Config/AutoloadConfig.php b/system/Config/AutoloadConfig.php index 89633948eb59..2fb2d8dc7c78 100644 --- a/system/Config/AutoloadConfig.php +++ b/system/Config/AutoloadConfig.php @@ -143,6 +143,7 @@ public function __construct() 'CodeIgniter\Debug\Exceptions' => BASEPATH.'Debug/Exceptions.php', 'CodeIgniter\Debug\Timer' => BASEPATH.'Debug/Timer.php', 'CodeIgniter\Debug\Iterator' => BASEPATH.'Debug/Iterator.php', + 'CodeIgniter\Encryption\Encryption' => BASEPATH.'Encryption/Encryption.php', 'CodeIgniter\Events\Events' => BASEPATH.'Events/Events.php', 'CodeIgniter\HTTP\CLIRequest' => BASEPATH.'HTTP/CLIRequest.php', 'CodeIgniter\HTTP\ContentSecurityPolicy' => BASEPATH.'HTTP/ContentSecurityPolicy.php', diff --git a/system/Config/Services.php b/system/Config/Services.php index cb91c848ab90..7621bdd0fb4d 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -1,4 +1,6 @@ -getLocale(); + $locale = !empty($locale) ? $locale : self::request()->getLocale(); return new \CodeIgniter\Language\Language($locale); } @@ -309,13 +306,12 @@ public static function migrations(BaseConfig $config = null, ConnectionInterface //-------------------------------------------------------------------- - /** * The Negotiate class provides the content negotiation features for * working the request to determine correct language, encoding, charset, * and more. */ - public static function negotiator(\CodeIgniter\HTTP\RequestInterface $request=null, $getShared = true) + public static function negotiator(\CodeIgniter\HTTP\RequestInterface $request = null, $getShared = true) { if ($getShared) { @@ -344,7 +340,7 @@ public static function pager($config = null, RendererInterface $view = null, $ge $config = new \Config\Pager(); } - if (! $view instanceof RendererInterface) + if (!$view instanceof RendererInterface) { $view = self::renderer(); } @@ -357,7 +353,7 @@ public static function pager($config = null, RendererInterface $view = null, $ge /** * The Parser is a simple template parser. */ - public static function parser($viewPath = APPPATH.'Views/', $config = null, $getShared = true) + public static function parser($viewPath = APPPATH . 'Views/', $config = null, $getShared = true) { if ($getShared) { @@ -379,7 +375,7 @@ public static function parser($viewPath = APPPATH.'Views/', $config = null, $get * The default View class within CodeIgniter is intentionally simple, but this * service could easily be replaced by a template engine if the user needed to. */ - public static function renderer($viewPath = APPPATH.'Views/', $config = null, $getShared = true) + public static function renderer($viewPath = APPPATH . 'Views/', $config = null, $getShared = true) { if ($getShared) { @@ -406,14 +402,13 @@ public static function request(\Config\App $config = null, $getShared = true) return self::getSharedInstance('request', $config); } - if (! is_object($config)) + if (!is_object($config)) { $config = new \Config\App(); } return new \CodeIgniter\HTTP\IncomingRequest( - $config, - new \CodeIgniter\HTTP\URI() + $config, new \CodeIgniter\HTTP\URI() ); } @@ -429,7 +424,7 @@ public static function response(\Config\App $config = null, $getShared = true) return self::getSharedInstance('response', $config); } - if (! is_object($config)) + if (!is_object($config)) { $config = new \Config\App(); } @@ -487,7 +482,7 @@ public static function security(\Config\App $config = null, $getShared = true) return self::getSharedInstance('security', $config); } - if (! is_object($config)) + if (!is_object($config)) { $config = new \Config\App(); } @@ -510,7 +505,7 @@ public static function session(\Config\App $config = null, $getShared = true) return self::getSharedInstance('session', $config); } - if (! is_object($config)) + if (!is_object($config)) { $config = new \Config\App(); } @@ -568,7 +563,7 @@ public static function toolbar(\Config\App $config = null, $getShared = true) return self::getSharedInstance('toolbar', $config); } - if (! is_object($config)) + if (!is_object($config)) { $config = new \Config\App(); } @@ -643,7 +638,6 @@ public static function typography($getShared = true) } //-------------------------------------------------------------------- - //-------------------------------------------------------------------- // Utility Methods - DO NOT EDIT //-------------------------------------------------------------------- @@ -657,7 +651,7 @@ public static function typography($getShared = true) */ protected static function getSharedInstance(string $key, ...$params) { - if (! isset(static::$instances[$key])) + if (!isset(static::$instances[$key])) { // Make sure $getShared is false array_push($params, false); @@ -688,6 +682,4 @@ public static function __callStatic(string $name, array $arguments) } //-------------------------------------------------------------------- - - } diff --git a/system/Encryption/Encryption.php b/system/Encryption/Encryption.php index 0fe7778755aa..0a1703c749e8 100644 --- a/system/Encryption/Encryption.php +++ b/system/Encryption/Encryption.php @@ -42,8 +42,8 @@ * Encryption exception * */ -class EncryptionException extends \Exception -{ +class EncryptionException extends \Exception { + } /** @@ -152,16 +152,16 @@ class Encryption { * @param array $params Configuration parameters * @return void * - * @throws \CodeIgniter\Encryption\EncryptionException + * @throws \CodeIgniter\Encryption\EncryptionException */ public function __construct($config = null) { if (empty($config)) $config = new \Config\Encryption(); $this->config = $config; - + $params = (array) $this->config; - + $this->_drivers = array( 'mcrypt' => defined('MCRYPT_DEV_URANDOM'), 'openssl' => extension_loaded('openssl') @@ -191,7 +191,7 @@ public function __construct($config = null) * @param array $params Configuration parameters * @return CI_Encryption * - * @throws \CodeIgniter\Encryption\EncryptionException + * @throws \CodeIgniter\Encryption\EncryptionException */ public function initialize(array $params) { @@ -233,7 +233,7 @@ public function initialize(array $params) * @param array $params Configuration parameters * @return void * - * @throws \CodeIgniter\Encryption\EncryptionException + * @throws \CodeIgniter\Encryption\EncryptionException */ protected function _mcrypt_initialize($params) { diff --git a/tests/_support/Encryption/MockEncryption.php b/tests/_support/Encryption/MockEncryption.php index b621e3653e34..386f98282a79 100644 --- a/tests/_support/Encryption/MockEncryption.php +++ b/tests/_support/Encryption/MockEncryption.php @@ -1,4 +1,6 @@ -{'_'.$driver.'_get_handle'}($cipher, $mode); + return $this->{'_' . $driver . '_get_handle'}($cipher, $mode); } -} \ No newline at end of file +} diff --git a/tests/system/Encryption/EncryptionTest.php b/tests/system/Encryption/EncryptionTest.php index b28a75418d27..88db30304740 100644 --- a/tests/system/Encryption/EncryptionTest.php +++ b/tests/system/Encryption/EncryptionTest.php @@ -1,4 +1,6 @@ - "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c", 'length' => 42, 'info' => "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9", - // 'prk' => "\x07\x77\x09\x36\x2c\x2e\x32\xdf\x0d\xdc\x3f\x0d\xc4\x7b\xba\x63\x90\xb6\xc7\x3b\xb5\x0f\x9c\x31\x22\xec\x84\x4a\xd7\xc2\xb3\xe5", + // 'prk' => "\x07\x77\x09\x36\x2c\x2e\x32\xdf\x0d\xdc\x3f\x0d\xc4\x7b\xba\x63\x90\xb6\xc7\x3b\xb5\x0f\x9c\x31\x22\xec\x84\x4a\xd7\xc2\xb3\xe5", 'okm' => "\x3c\xb2\x5f\x25\xfa\xac\xd5\x7a\x90\x43\x4f\x64\xd0\x36\x2f\x2a\x2d\x2d\x0a\x90\xcf\x1a\x5a\x4c\x5d\xb0\x2d\x56\xec\xc4\xc5\xbf\x34\x00\x72\x08\xd5\xb8\x87\x18\x58\x65" ), // A.2: Test with SHA-256 and longer inputs/outputs @@ -67,7 +69,7 @@ public function test_hkdf() 'salt' => "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf", 'length' => 82, 'info' => "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff", - // 'prk' => "\x06\xa6\xb8\x8c\x58\x53\x36\x1a\x06\x10\x4c\x9c\xeb\x35\xb4\x5c\xef\x76\x00\x14\x90\x46\x71\x01\x4a\x19\x3f\x40\xc1\x5f\xc2\x44", + // 'prk' => "\x06\xa6\xb8\x8c\x58\x53\x36\x1a\x06\x10\x4c\x9c\xeb\x35\xb4\x5c\xef\x76\x00\x14\x90\x46\x71\x01\x4a\x19\x3f\x40\xc1\x5f\xc2\x44", 'okm' => "\xb1\x1e\x39\x8d\xc8\x03\x27\xa1\xc8\xe7\xf7\x8c\x59\x6a\x49\x34\x4f\x01\x2e\xda\x2d\x4e\xfa\xd8\xa0\x50\xcc\x4c\x19\xaf\xa9\x7c\x59\x04\x5a\x99\xca\xc7\x82\x72\x71\xcb\x41\xc6\x5e\x59\x0e\x09\xda\x32\x75\x60\x0c\x2f\x09\xb8\x36\x77\x93\xa9\xac\xa3\xdb\x71\xcc\x30\xc5\x81\x79\xec\x3e\x87\xc1\x4c\x01\xd5\xc1\xf3\x43\x4f\x1d\x87", ), // A.3: Test with SHA-256 and zero-length salt/info @@ -77,7 +79,7 @@ public function test_hkdf() 'salt' => '', 'length' => 42, 'info' => '', - // 'prk' => "\x19\xef\x24\xa3\x2c\x71\x7b\x16\x7f\x33\xa9\x1d\x6f\x64\x8b\xdf\x96\x59\x67\x76\xaf\xdb\x63\x77\xac\x43\x4c\x1c\x29\x3c\xcb\x04", + // 'prk' => "\x19\xef\x24\xa3\x2c\x71\x7b\x16\x7f\x33\xa9\x1d\x6f\x64\x8b\xdf\x96\x59\x67\x76\xaf\xdb\x63\x77\xac\x43\x4c\x1c\x29\x3c\xcb\x04", 'okm' => "\x8d\xa4\xe7\x75\xa5\x63\xc1\x8f\x71\x5f\x80\x2a\x06\x3c\x5a\x31\xb8\xa1\x1f\x5c\x5e\xe1\x87\x9e\xc3\x45\x4e\x5f\x3c\x73\x8d\x2d\x9d\x20\x13\x95\xfa\xa4\xb6\x1a\x96\xc8", ) ); @@ -85,33 +87,22 @@ public function test_hkdf() foreach ($vectors as $test) { $this->assertEquals( - $test['okm'], - $this->encryption->hkdf( - $test['ikm'], - $test['digest'], - $test['salt'], - $test['length'], - $test['info'] - ) + $test['okm'], $this->encryption->hkdf( + $test['ikm'], $test['digest'], $test['salt'], $test['length'], $test['info'] + ) ); } // Test default length, it must match the digest size $hkdf_result = $this->encryption->hkdf('foobar', 'sha512'); $this->assertEquals( - 64, - defined('MB_OVERLOAD_STRING') - ? mb_strlen($hkdf_result, '8bit') - : strlen($hkdf_result) + 64, defined('MB_OVERLOAD_STRING') ? mb_strlen($hkdf_result, '8bit') : strlen($hkdf_result) ); // Test maximum length (RFC5869 says that it must be up to 255 times the digest size) $hkdf_result = $this->encryption->hkdf('foobar', 'sha384', NULL, 48 * 255); $this->assertEquals( - 12240, - defined('MB_OVERLOAD_STRING') - ? mb_strlen($hkdf_result, '8bit') - : strlen($hkdf_result) + 12240, defined('MB_OVERLOAD_STRING') ? mb_strlen($hkdf_result, '8bit') : strlen($hkdf_result) ); $this->assertFalse($this->encryption->hkdf('foobar', 'sha224', NULL, 28 * 255 + 1)); @@ -254,8 +245,7 @@ public function test__mcrypt_get_handle() if ($this->encryption->drivers['mcrypt'] === FALSE) { return $this->markTestSkipped('Cannot test MCrypt because it is not available.'); - } - elseif (version_compare(PHP_VERSION, '7.1.0-alpha', '>=')) + } elseif (version_compare(PHP_VERSION, '7.1.0-alpha', '>=')) { return $this->markTestSkipped('ext/mcrypt is deprecated since PHP 7.1 and will generate notices here.'); } @@ -288,12 +278,11 @@ public function test__openssl_mcrypt_get_handle() */ public function test_portability() { - if ( ! $this->encryption->drivers['mcrypt'] OR ! $this->encryption->drivers['openssl']) + if (!$this->encryption->drivers['mcrypt'] OR ! $this->encryption->drivers['openssl']) { $this->markTestSkipped('Both MCrypt and OpenSSL support are required for portability tests.'); return; - } - elseif (version_compare(PHP_VERSION, '7.1.0-alpha', '>=')) + } elseif (version_compare(PHP_VERSION, '7.1.0-alpha', '>=')) { return $this->markTestSkipped('ext/mcrypt is deprecated since PHP 7.1 and will generate notices here.'); } @@ -366,7 +355,7 @@ public function test_portability() foreach ($portable as &$test) { // Add some randomness to the selected driver - $driver = mt_rand(0,1); + $driver = mt_rand(0, 1); $params = array( 'driver' => $driver_index[$driver], 'cipher' => $test[0], @@ -377,7 +366,7 @@ public function test_portability() $this->encryption->initialize($params); $ciphertext = $this->encryption->encrypt($message); - $driver = (int) ! $driver; + $driver = (int) !$driver; $params['driver'] = $driver_index[$driver]; $this->encryption->initialize($params); From 9619e58aed29e5d76c5daccbf2c67105330cb96a Mon Sep 17 00:00:00 2001 From: Master Yoda Date: Wed, 10 May 2017 10:01:03 -0700 Subject: [PATCH 05/18] Refactor & reformat Encryption --- system/Config/AutoloadConfig.php | 28 +- system/Config/Services.php | 29 +- system/Encryption/Encryption.php | 406 +++++++++--------- system/Encryption/Handlers/BaseHandler.php | 60 +++ system/Encryption/Handlers/OpenSSLHandler.php | 43 ++ tests/_support/Encryption/MockEncryption.php | 15 +- tests/system/Encryption/EncryptionTest.php | 247 +++++------ 7 files changed, 468 insertions(+), 360 deletions(-) create mode 100644 system/Encryption/Handlers/BaseHandler.php create mode 100644 system/Encryption/Handlers/OpenSSLHandler.php diff --git a/system/Config/AutoloadConfig.php b/system/Config/AutoloadConfig.php index 2fb2d8dc7c78..1c249ed7d5d0 100644 --- a/system/Config/AutoloadConfig.php +++ b/system/Config/AutoloadConfig.php @@ -137,20 +137,20 @@ public function __construct() 'CodeIgniter\Database\Database' => BASEPATH.'Database/Database.php', 'CodeIgniter\Database\Query' => BASEPATH.'Database/Query.php', 'CodeIgniter\Database\QueryInterface' => BASEPATH.'Database/QueryInterface.php', - 'CodeIgniter\Database\ResultInterface' => BASEPATH.'Database/ResultInterface.php', - 'CodeIgniter\Database\Migration' => BASEPATH.'Database/Migration.php', - 'CodeIgniter\Database\MigrationRunner' => BASEPATH.'Database/MigrationRunner.php', - 'CodeIgniter\Debug\Exceptions' => BASEPATH.'Debug/Exceptions.php', - 'CodeIgniter\Debug\Timer' => BASEPATH.'Debug/Timer.php', - 'CodeIgniter\Debug\Iterator' => BASEPATH.'Debug/Iterator.php', - 'CodeIgniter\Encryption\Encryption' => BASEPATH.'Encryption/Encryption.php', - 'CodeIgniter\Events\Events' => BASEPATH.'Events/Events.php', - 'CodeIgniter\HTTP\CLIRequest' => BASEPATH.'HTTP/CLIRequest.php', - 'CodeIgniter\HTTP\ContentSecurityPolicy' => BASEPATH.'HTTP/ContentSecurityPolicy.php', - 'CodeIgniter\HTTP\CURLRequest' => BASEPATH.'HTTP/CURLRequest.php', - 'CodeIgniter\HTTP\IncomingRequest' => BASEPATH.'HTTP/IncomingRequest.php', - 'CodeIgniter\HTTP\Message' => BASEPATH.'HTTP/Message.php', - 'CodeIgniter\HTTP\Negotiate' => BASEPATH.'HTTP/Negotiate.php', + 'CodeIgniter\Database\ResultInterface' => BASEPATH.'Database/ResultInterface.php', + 'CodeIgniter\Database\Migration' => BASEPATH.'Database/Migration.php', + 'CodeIgniter\Database\MigrationRunner' => BASEPATH.'Database/MigrationRunner.php', + 'CodeIgniter\Debug\Exceptions' => BASEPATH.'Debug/Exceptions.php', + 'CodeIgniter\Debug\Timer' => BASEPATH.'Debug/Timer.php', + 'CodeIgniter\Debug\Iterator' => BASEPATH.'Debug/Iterator.php', + 'CodeIgniter\Encryption\Encryption' => BASEPATH.'Encryption/Encryption.php', + 'CodeIgniter\Events\Events' => BASEPATH.'Events/Events.php', + 'CodeIgniter\HTTP\CLIRequest' => BASEPATH.'HTTP/CLIRequest.php', + 'CodeIgniter\HTTP\ContentSecurityPolicy' => BASEPATH.'HTTP/ContentSecurityPolicy.php', + 'CodeIgniter\HTTP\CURLRequest' => BASEPATH.'HTTP/CURLRequest.php', + 'CodeIgniter\HTTP\IncomingRequest' => BASEPATH.'HTTP/IncomingRequest.php', + 'CodeIgniter\HTTP\Message' => BASEPATH.'HTTP/Message.php', + 'CodeIgniter\HTTP\Negotiate' => BASEPATH.'HTTP/Negotiate.php', 'CodeIgniter\HTTP\Request' => BASEPATH.'HTTP/Request.php', 'CodeIgniter\HTTP\RequestInterface' => BASEPATH.'HTTP/RequestInterface.php', 'CodeIgniter\HTTP\Response' => BASEPATH.'HTTP/Response.php', diff --git a/system/Config/Services.php b/system/Config/Services.php index 7621bdd0fb4d..2e60619ed525 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -58,7 +58,8 @@ * @see http://blog.ircmaxell.com/2015/11/simple-easy-risk-and-change.html * @see http://www.infoq.com/presentations/Simple-Made-Easy */ -class Services { +class Services +{ /** * Cache for instance of any services that @@ -97,7 +98,7 @@ public static function cache(\Config\Cache $config = null, $getShared = true) return self::getSharedInstance('cache', $config); } - if (!is_object($config)) + if ( ! is_object($config)) { $config = new \Config\Cache(); } @@ -118,7 +119,7 @@ public static function clirequest(\Config\App $config = null, $getShared = true) return self::getSharedInstance('clirequest', $config); } - if (!is_object($config)) + if ( ! is_object($config)) { $config = new \Config\App(); } @@ -141,12 +142,12 @@ public static function curlrequest(array $options = [], $response = null, \Confi return self::getSharedInstance('curlrequest', $options, $response, $config); } - if (!is_object($config)) + if ( ! is_object($config)) { $config = new \Config\App(); } - if (!is_object($response)) + if ( ! is_object($response)) { $response = new \CodeIgniter\HTTP\Response($config); } @@ -168,7 +169,7 @@ public static function encrypter(\Config\Encryption $config = null) return self::getSharedInstance('encrypter', $config); } - if (!is_object($config)) + if ( ! is_object($config)) { $config = new \Config\Encryption(); } @@ -252,7 +253,7 @@ public static function language(string $locale = null, $getShared = true) return self::getSharedInstance('language', $locale); } - $locale = !empty($locale) ? $locale : self::request()->getLocale(); + $locale = ! empty($locale) ? $locale : self::request()->getLocale(); return new \CodeIgniter\Language\Language($locale); } @@ -340,7 +341,7 @@ public static function pager($config = null, RendererInterface $view = null, $ge $config = new \Config\Pager(); } - if (!$view instanceof RendererInterface) + if ( ! $view instanceof RendererInterface) { $view = self::renderer(); } @@ -402,7 +403,7 @@ public static function request(\Config\App $config = null, $getShared = true) return self::getSharedInstance('request', $config); } - if (!is_object($config)) + if ( ! is_object($config)) { $config = new \Config\App(); } @@ -424,7 +425,7 @@ public static function response(\Config\App $config = null, $getShared = true) return self::getSharedInstance('response', $config); } - if (!is_object($config)) + if ( ! is_object($config)) { $config = new \Config\App(); } @@ -482,7 +483,7 @@ public static function security(\Config\App $config = null, $getShared = true) return self::getSharedInstance('security', $config); } - if (!is_object($config)) + if ( ! is_object($config)) { $config = new \Config\App(); } @@ -505,7 +506,7 @@ public static function session(\Config\App $config = null, $getShared = true) return self::getSharedInstance('session', $config); } - if (!is_object($config)) + if ( ! is_object($config)) { $config = new \Config\App(); } @@ -563,7 +564,7 @@ public static function toolbar(\Config\App $config = null, $getShared = true) return self::getSharedInstance('toolbar', $config); } - if (!is_object($config)) + if ( ! is_object($config)) { $config = new \Config\App(); } @@ -651,7 +652,7 @@ public static function typography($getShared = true) */ protected static function getSharedInstance(string $key, ...$params) { - if (!isset(static::$instances[$key])) + if ( ! isset(static::$instances[$key])) { // Make sure $getShared is false array_push($params, false); diff --git a/system/Encryption/Encryption.php b/system/Encryption/Encryption.php index 0a1703c749e8..6b09ea0937a2 100644 --- a/system/Encryption/Encryption.php +++ b/system/Encryption/Encryption.php @@ -42,7 +42,8 @@ * Encryption exception * */ -class EncryptionException extends \Exception { +class EncryptionException extends \Exception +{ } @@ -51,57 +52,58 @@ class EncryptionException extends \Exception { * * Provides two-way keyed encryption via PHP's MCrypt and/or OpenSSL extensions. */ -class Encryption { +class Encryption +{ /** * Encryption cipher * * @var string */ - protected $_cipher = 'aes-128'; + protected $cipher = 'aes-128'; /** * Cipher mode * * @var string */ - protected $_mode = 'cbc'; + protected $mode = 'cbc'; /** * Cipher handle * * @var mixed */ - protected $_handle; + protected $handle; /** * Encryption key * * @var string */ - protected $_key; + protected $key; /** * PHP extension to be used * * @var string */ - protected $_driver; + protected $handler; /** - * List of usable drivers (PHP extensions) + * List of usable handlers (PHP extensions) * * @var array */ - protected $_drivers = array(); + protected $handlers = []; /** * List of available modes * * @var array */ - protected $_modes = array( - 'mcrypt' => array( + protected $modes = [ + 'mcrypt' => [ 'cbc' => 'cbc', 'ecb' => 'ecb', 'ofb' => 'nofb', @@ -110,8 +112,8 @@ class Encryption { 'cfb8' => 'cfb', 'ctr' => 'ctr', 'stream' => 'stream' - ), - 'openssl' => array( + ], + 'openssl' => [ 'cbc' => 'cbc', 'ecb' => 'ecb', 'ofb' => 'ofb', @@ -120,8 +122,8 @@ class Encryption { 'ctr' => 'ctr', 'stream' => '', 'xts' => 'xts' - ) - ); + ] + ]; /** * List of supported HMAC algorithms @@ -130,19 +132,12 @@ class Encryption { * * @var array */ - protected $_digests = array( + protected $digests = [ 'sha224' => 28, 'sha256' => 32, 'sha384' => 48, 'sha512' => 64 - ); - - /** - * mbstring.func_overload flag - * - * @var bool - */ - protected static $func_overload; + ]; // -------------------------------------------------------------------- @@ -162,22 +157,21 @@ public function __construct($config = null) $params = (array) $this->config; - $this->_drivers = array( + $this->handlers = [ 'mcrypt' => defined('MCRYPT_DEV_URANDOM'), 'openssl' => extension_loaded('openssl') - ); + ]; - if (!$this->_drivers['mcrypt'] && !$this->_drivers['openssl']) + if ( ! $this->handlers['mcrypt'] && ! $this->handlers['openssl']) { - throw new EncryptionException('Unable to find an available encryption driver.'); + throw new EncryptionException('Unable to find an available encryption handler.'); } - isset(self::$func_overload) OR self::$func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload')); $this->initialize($params); - if (!isset($this->_key) && self::strlen($key = $this->config->key) > 0) + if ( ! isset($this->key) && self::strlen($key = $this->config->key) > 0) { - $this->_key = $key; + $this->key = $key; } log_message('info', 'Encryption Class Initialized'); @@ -195,33 +189,35 @@ public function __construct($config = null) */ public function initialize(array $params) { - if (!empty($params['driver'])) + if ( ! empty($params['handler'])) { - if (isset($this->_drivers[$params['driver']])) + if (isset($this->handlers[$params['handler']])) { - if ($this->_drivers[$params['driver']]) + if ($this->handlers[$params['handler']]) { - $this->_driver = $params['driver']; - } else + $this->handler = $params['handler']; + } + else { - throw new EncryptionException("Driver '" . $params['driver'] . "' is not available."); + throw new EncryptionException("Driver '" . $params['handler'] . "' is not available."); } - } else + } + else { - throw new EncryptionException("Unknown driver '" . $params['driver'] . "' cannot be configured."); + throw new EncryptionException("Unknown handler '" . $params['handler'] . "' cannot be configured."); } } - if (empty($this->_driver)) + if (empty($this->handler)) { - $this->_driver = ($this->_drivers['openssl'] === TRUE) ? 'openssl' : 'mcrypt'; + $this->handler = ($this->handlers['openssl'] === true) ? 'openssl' : 'mcrypt'; - log_message('debug', "Encryption: Auto-configured driver '" . $this->_driver . "'."); + log_message('debug', "Encryption: Auto-configured handler '" . $this->handler . "'."); } - empty($params['cipher']) && $params['cipher'] = $this->_cipher; - empty($params['key']) OR $this->_key = $params['key']; - $this->{'_' . $this->_driver . '_initialize'}($params); + empty($params['cipher']) && $params['cipher'] = $this->cipher; + empty($params['key']) OR $this->key = $params['key']; + $this->{$this->handler . 'Initialize'}($params); return $this; } @@ -235,49 +231,52 @@ public function initialize(array $params) * * @throws \CodeIgniter\Encryption\EncryptionException */ - protected function _mcrypt_initialize($params) + protected function mcryptInitialize($params) { - if (!empty($params['cipher'])) + if ( ! empty($params['cipher'])) { $params['cipher'] = strtolower($params['cipher']); - $this->_cipher_alias($params['cipher']); + $this->cipherAlias($params['cipher']); - if (!in_array($params['cipher'], mcrypt_list_algorithms(), TRUE)) + if ( ! in_array($params['cipher'], mcrypt_list_algorithms(), true)) { throw new EncryptionException('MCrypt cipher ' . strtoupper($params['cipher']) . ' is not available.'); - } else + } + else { - $this->_cipher = $params['cipher']; + $this->cipher = $params['cipher']; } } - if (!empty($params['mode'])) + if ( ! empty($params['mode'])) { $params['mode'] = strtolower($params['mode']); - if (!isset($this->_modes['mcrypt'][$params['mode']])) + if ( ! isset($this->modes['mcrypt'][$params['mode']])) { throw new EncryptionException('MCrypt mode ' . strtoupper($params['cipher']) . ' is not available.'); - } else + } + else { - $this->_mode = $this->_modes['mcrypt'][$params['mode']]; + $this->mode = $this->modes['mcrypt'][$params['mode']]; } } - if (isset($this->_cipher, $this->_mode)) + if (isset($this->cipher, $this->mode)) { - if (is_resource($this->_handle) && (strtolower(mcrypt_enc_get_algorithms_name($this->_handle)) !== $this->_cipher - OR strtolower(mcrypt_enc_get_modes_name($this->_handle)) !== $this->_mode) + if (is_resource($this->handle) && (strtolower(mcrypt_enc_get_algorithms_name($this->handle)) !== $this->cipher + OR strtolower(mcrypt_enc_getmodes_name($this->handle)) !== $this->mode) ) { - mcrypt_module_close($this->_handle); + mcrypt_module_close($this->handle); } - if ($this->_handle = mcrypt_module_open($this->_cipher, '', $this->_mode, '')) + if ($this->handle = mcrypt_module_open($this->cipher, '', $this->mode, '')) { - log_message('info', 'Encryption: MCrypt cipher ' . strtoupper($this->_cipher) . ' initialized in ' . strtoupper($this->_mode) . ' mode.'); - } else + log_message('info', 'Encryption: MCrypt cipher ' . strtoupper($this->cipher) . ' initialized in ' . strtoupper($this->mode) . ' mode.'); + } + else { - throw new EncryptionException('Unable to initialize MCrypt with cipher ' . strtoupper($this->_cipher) . ' in ' . strtoupper($this->_mode) . ' mode.'); + throw new EncryptionException('Unable to initialize MCrypt with cipher ' . strtoupper($this->cipher) . ' in ' . strtoupper($this->mode) . ' mode.'); } } } @@ -290,39 +289,41 @@ protected function _mcrypt_initialize($params) * @param array $params Configuration parameters * @return void */ - protected function _openssl_initialize($params) + protected function opensslInitialize($params) { - if (!empty($params['cipher'])) + if ( ! empty($params['cipher'])) { $params['cipher'] = strtolower($params['cipher']); - $this->_cipher_alias($params['cipher']); - $this->_cipher = $params['cipher']; + $this->cipherAlias($params['cipher']); + $this->cipher = $params['cipher']; } - if (!empty($params['mode'])) + if ( ! empty($params['mode'])) { $params['mode'] = strtolower($params['mode']); - if (!isset($this->_modes['openssl'][$params['mode']])) + if ( ! isset($this->modes['openssl'][$params['mode']])) { log_message('error', 'Encryption: OpenSSL mode ' . strtoupper($params['mode']) . ' is not available.'); - } else + } + else { - $this->_mode = $this->_modes['openssl'][$params['mode']]; + $this->mode = $this->modes['openssl'][$params['mode']]; } } - if (isset($this->_cipher, $this->_mode)) + if (isset($this->cipher, $this->mode)) { // This is mostly for the stream mode, which doesn't get suffixed in OpenSSL - $handle = empty($this->_mode) ? $this->_cipher : $this->_cipher . '-' . $this->_mode; + $handle = empty($this->mode) ? $this->cipher : $this->cipher . '-' . $this->mode; - if (!in_array($handle, openssl_get_cipher_methods(), TRUE)) + if ( ! in_array($handle, openssl_get_cipher_methods(), true)) { - $this->_handle = NULL; + $this->handle = null; log_message('error', 'Encryption: Unable to initialize OpenSSL with method ' . strtoupper($handle) . '.'); - } else + } + else { - $this->_handle = $handle; + $this->handle = $handle; log_message('info', 'Encryption: OpenSSL initialized with method ' . strtoupper($handle) . '.'); } } @@ -338,24 +339,18 @@ protected function _openssl_initialize($params) */ public function createKey($length) { - if (function_exists('random_bytes')) + try { - try - { - return random_bytes((int) $length); - } catch (Exception $e) - { - log_message('error', $e->getMessage()); - return FALSE; - } - } elseif (defined('MCRYPT_DEV_URANDOM')) + return random_bytes((int) $length); + } catch (Exception $e) { - return mcrypt_create_iv($length, MCRYPT_DEV_URANDOM); + log_message('error', $e->getMessage()); + return false; } - $is_secure = NULL; + $is_secure = null; $key = openssl_random_pseudo_bytes($length, $is_secure); - return ($is_secure === TRUE) ? $key : FALSE; + return ($is_secure === true) ? $key : false; } // -------------------------------------------------------------------- @@ -367,26 +362,26 @@ public function createKey($length) * @param array $params Input parameters * @return string */ - public function encrypt($data, array $params = NULL) + public function encrypt($data, array $params = null) { - if (($params = $this->_get_params($params)) === FALSE) + if (($params = $this->getParams($params)) === false) { - return FALSE; + return false; } - isset($params['key']) OR $params['key'] = $this->hkdf($this->_key, 'sha512', NULL, self::strlen($this->_key), 'encryption'); + isset($params['key']) OR $params['key'] = $this->hkdf($this->key, 'sha512', null, self::strlen($this->key), 'encryption'); - if (($data = $this->{'_' . $this->_driver . '_encrypt'}($data, $params)) === FALSE) + if (($data = $this->{$this->handler . 'Encrypt'}($data, $params)) === false) { - return FALSE; + return false; } $params['base64'] && $data = base64_encode($data); if (isset($params['hmac_digest'])) { - isset($params['hmac_key']) OR $params['hmac_key'] = $this->hkdf($this->_key, 'sha512', NULL, NULL, 'authentication'); - return hash_hmac($params['hmac_digest'], $data, $params['hmac_key'], !$params['base64']) . $data; + isset($params['hmackey']) OR $params['hmackey'] = $this->hkdf($this->key, 'sha512', null, null, 'authentication'); + return hash_hmac($params['hmac_digest'], $data, $params['hmackey'], ! $params['base64']) . $data; } return $data; @@ -401,30 +396,30 @@ public function encrypt($data, array $params = NULL) * @param array $params Input parameters * @return string */ - protected function _mcrypt_encrypt($data, $params) + protected function mcryptEncrypt($data, $params) { - if (!is_resource($params['handle'])) + if ( ! is_resource($params['handle'])) { - return FALSE; + return false; } // The greater-than-1 comparison is mostly a work-around for a bug, // where 1 is returned for ARCFour instead of 0. - $iv = (($iv_size = mcrypt_enc_get_iv_size($params['handle'])) > 1) ? $this->createKey($iv_size) : NULL; + $iv = (($iv_size = mcrypt_enc_get_iv_size($params['handle'])) > 1) ? $this->createKey($iv_size) : null; if (mcrypt_generic_init($params['handle'], $params['key'], $iv) < 0) { - if ($params['handle'] !== $this->_handle) + if ($params['handle'] !== $this->handle) { mcrypt_module_close($params['handle']); } - return FALSE; + return false; } // Use PKCS#7 padding in order to ensure compatibility with OpenSSL // and other implementations outside of PHP. - if (in_array(strtolower(mcrypt_enc_get_modes_name($params['handle'])), array('cbc', 'ecb'), TRUE)) + if (in_array(strtolower(mcrypt_enc_getmodes_name($params['handle'])), ['cbc', 'ecb'], true)) { $block_size = mcrypt_enc_get_block_size($params['handle']); $pad = $block_size - (self::strlen($data) % $block_size); @@ -442,10 +437,10 @@ protected function _mcrypt_encrypt($data, $params) // This probably would've been fine (even though still wasteful), // but OpenSSL isn't that dumb and we need to make the process // portable, so ... - $data = (mcrypt_enc_get_modes_name($params['handle']) !== 'ECB') ? $iv . mcrypt_generic($params['handle'], $data) : mcrypt_generic($params['handle'], $data); + $data = (mcrypt_enc_getmodes_name($params['handle']) !== 'ECB') ? $iv . mcrypt_generic($params['handle'], $data) : mcrypt_generic($params['handle'], $data); mcrypt_generic_deinit($params['handle']); - if ($params['handle'] !== $this->_handle) + if ($params['handle'] !== $this->handle) { mcrypt_module_close($params['handle']); } @@ -462,22 +457,22 @@ protected function _mcrypt_encrypt($data, $params) * @param array $params Input parameters * @return string */ - protected function _openssl_encrypt($data, $params) + protected function opensslEncrypt($data, $params) { if (empty($params['handle'])) { - return FALSE; + return false; } - $iv = ($iv_size = openssl_cipher_iv_length($params['handle'])) ? $this->createKey($iv_size) : NULL; + $iv = ($iv_size = opensslcipher_iv_length($params['handle'])) ? $this->createKey($iv_size) : null; $data = openssl_encrypt( $data, $params['handle'], $params['key'], OPENSSL_RAW_DATA, $iv ); - if ($data === FALSE) + if ($data === false) { - return FALSE; + return false; } return $iv . $data; @@ -492,40 +487,40 @@ protected function _openssl_encrypt($data, $params) * @param array $params Input parameters * @return string */ - public function decrypt($data, array $params = NULL) + public function decrypt($data, array $params = null) { - if (($params = $this->_get_params($params)) === FALSE) + if (($params = $this->getParams($params)) === false) { - return FALSE; + return false; } if (isset($params['hmac_digest'])) { // This might look illogical, but it is done during encryption as well ... // The 'base64' value is effectively an inverted "raw data" parameter - $digest_size = ($params['base64']) ? $this->_digests[$params['hmac_digest']] * 2 : $this->_digests[$params['hmac_digest']]; + $digest_size = ($params['base64']) ? $this->digests[$params['hmac_digest']] * 2 : $this->digests[$params['hmac_digest']]; if (self::strlen($data) <= $digest_size) { - return FALSE; + return false; } $hmac_input = self::substr($data, 0, $digest_size); $data = self::substr($data, $digest_size); - isset($params['hmac_key']) OR $params['hmac_key'] = $this->hkdf($this->_key, 'sha512', NULL, NULL, 'authentication'); - $hmac_check = hash_hmac($params['hmac_digest'], $data, $params['hmac_key'], !$params['base64']); + isset($params['hmackey']) OR $params['hmackey'] = $this->hkdf($this->key, 'sha512', null, null, 'authentication'); + $hmac_check = hash_hmac($params['hmac_digest'], $data, $params['hmackey'], ! $params['base64']); // Time-attack-safe comparison $diff = 0; - for ($i = 0; $i < $digest_size; $i++) + for ($i = 0; $i < $digest_size; $i ++ ) { $diff |= ord($hmac_input[$i]) ^ ord($hmac_check[$i]); } if ($diff !== 0) { - return FALSE; + return false; } } @@ -534,9 +529,9 @@ public function decrypt($data, array $params = NULL) $data = base64_decode($data); } - isset($params['key']) OR $params['key'] = $this->hkdf($this->_key, 'sha512', NULL, self::strlen($this->_key), 'encryption'); + isset($params['key']) OR $params['key'] = $this->hkdf($this->key, 'sha512', null, self::strlen($this->key), 'encryption'); - return $this->{'_' . $this->_driver . '_decrypt'}($data, $params); + return $this->{$this->handler . 'Decrypt'}($data, $params); } // -------------------------------------------------------------------- @@ -548,50 +543,52 @@ public function decrypt($data, array $params = NULL) * @param array $params Input parameters * @return string */ - protected function _mcrypt_decrypt($data, $params) + protected function mcryptDecrypt($data, $params) { - if (!is_resource($params['handle'])) + if ( ! is_resource($params['handle'])) { - return FALSE; + return false; } // The greater-than-1 comparison is mostly a work-around for a bug, // where 1 is returned for ARCFour instead of 0. if (($iv_size = mcrypt_enc_get_iv_size($params['handle'])) > 1) { - if (mcrypt_enc_get_modes_name($params['handle']) !== 'ECB') + if (mcrypt_enc_getmodes_name($params['handle']) !== 'ECB') { $iv = self::substr($data, 0, $iv_size); $data = self::substr($data, $iv_size); - } else + } + else { // MCrypt is dumb and this is ignored, only size matters $iv = str_repeat("\x0", $iv_size); } - } else + } + else { - $iv = NULL; + $iv = null; } if (mcrypt_generic_init($params['handle'], $params['key'], $iv) < 0) { - if ($params['handle'] !== $this->_handle) + if ($params['handle'] !== $this->handle) { mcrypt_module_close($params['handle']); } - return FALSE; + return false; } $data = mdecrypt_generic($params['handle'], $data); // Remove PKCS#7 padding, if necessary - if (in_array(strtolower(mcrypt_enc_get_modes_name($params['handle'])), array('cbc', 'ecb'), TRUE)) + if (in_array(strtolower(mcrypt_enc_getmodes_name($params['handle'])), ['cbc', 'ecb'], true)) { $data = self::substr($data, 0, -ord($data[self::strlen($data) - 1])); } mcrypt_generic_deinit($params['handle']); - if ($params['handle'] !== $this->_handle) + if ($params['handle'] !== $this->handle) { mcrypt_module_close($params['handle']); } @@ -608,18 +605,19 @@ protected function _mcrypt_decrypt($data, $params) * @param array $params Input parameters * @return string */ - protected function _openssl_decrypt($data, $params) + protected function opensslDecrypt($data, $params) { - if ($iv_size = openssl_cipher_iv_length($params['handle'])) + if ($iv_size = opensslcipher_iv_length($params['handle'])) { $iv = self::substr($data, 0, $iv_size); $data = self::substr($data, $iv_size); - } else + } + else { - $iv = NULL; + $iv = null; } - return empty($params['handle']) ? FALSE : openssl_decrypt( + return empty($params['handle']) ? false : openssl_decrypt( $data, $params['handle'], $params['key'], OPENSSL_RAW_DATA, $iv ); } @@ -632,69 +630,74 @@ protected function _openssl_decrypt($data, $params) * @param array $params Input parameters * @return array */ - protected function _get_params($params) + protected function getParams($params) { if (empty($params)) { - return isset($this->_cipher, $this->_mode, $this->_key, $this->_handle) ? array( - 'handle' => $this->_handle, - 'cipher' => $this->_cipher, - 'mode' => $this->_mode, - 'key' => NULL, - 'base64' => TRUE, + return isset($this->cipher, $this->mode, $this->key, $this->handle) ? [ + 'handle' => $this->handle, + 'cipher' => $this->cipher, + 'mode' => $this->mode, + 'key' => null, + 'base64' => true, 'hmac_digest' => 'sha512', - 'hmac_key' => NULL - ) : FALSE; - } elseif (!isset($params['cipher'], $params['mode'], $params['key'])) + 'hmackey' => null + ] : false; + } + elseif ( ! isset($params['cipher'], $params['mode'], $params['key'])) { - return FALSE; + return false; } if (isset($params['mode'])) { $params['mode'] = strtolower($params['mode']); - if (!isset($this->_modes[$this->_driver][$params['mode']])) + if ( ! isset($this->modes[$this->handler][$params['mode']])) { - return FALSE; - } else + return false; + } + else { - $params['mode'] = $this->_modes[$this->_driver][$params['mode']]; + $params['mode'] = $this->modes[$this->handler][$params['mode']]; } } - if (isset($params['hmac']) && $params['hmac'] === FALSE) + if (isset($params['hmac']) && $params['hmac'] === false) { - $params['hmac_digest'] = $params['hmac_key'] = NULL; - } else + $params['hmac_digest'] = $params['hmackey'] = null; + } + else { - if (!isset($params['hmac_key'])) + if ( ! isset($params['hmackey'])) { - return FALSE; - } elseif (isset($params['hmac_digest'])) + return false; + } + elseif (isset($params['hmac_digest'])) { $params['hmac_digest'] = strtolower($params['hmac_digest']); - if (!isset($this->_digests[$params['hmac_digest']])) + if ( ! isset($this->digests[$params['hmac_digest']])) { - return FALSE; + return false; } - } else + } + else { $params['hmac_digest'] = 'sha512'; } } - $params = array( - 'handle' => NULL, + $params = [ + 'handle' => null, 'cipher' => $params['cipher'], 'mode' => $params['mode'], 'key' => $params['key'], - 'base64' => isset($params['raw_data']) ? !$params['raw_data'] : FALSE, + 'base64' => isset($params['raw_data']) ? ! $params['raw_data'] : false, 'hmac_digest' => $params['hmac_digest'], - 'hmac_key' => $params['hmac_key'] - ); + 'hmackey' => $params['hmackey'] + ]; - $this->_cipher_alias($params['cipher']); - $params['handle'] = ($params['cipher'] !== $this->_cipher OR $params['mode'] !== $this->_mode) ? $this->{'_' . $this->_driver . '_get_handle'}($params['cipher'], $params['mode']) : $this->_handle; + $this->cipherAlias($params['cipher']); + $params['handle'] = ($params['cipher'] !== $this->cipher OR $params['mode'] !== $this->mode) ? $this->{$this->handler . 'Gethandle'}($params['cipher'], $params['mode']) : $this->handle; return $params; } @@ -708,7 +711,7 @@ protected function _get_params($params) * @param string $mode Encryption mode * @return resource */ - protected function _mcrypt_get_handle($cipher, $mode) + protected function mcryptGetHandle($cipher, $mode) { return mcrypt_module_open($cipher, '', $mode, ''); } @@ -722,7 +725,7 @@ protected function _mcrypt_get_handle($cipher, $mode) * @param string $mode Encryption mode * @return string */ - protected function _openssl_get_handle($cipher, $mode) + protected function opensslGetHandle($cipher, $mode) { // OpenSSL methods aren't suffixed with '-stream' for this mode return ($mode === 'stream') ? $cipher : $cipher . '-' . $mode; @@ -738,14 +741,14 @@ protected function _openssl_get_handle($cipher, $mode) * @param string $cipher Cipher name * @return void */ - protected function _cipher_alias(&$cipher) + protected function cipherAlias(&$cipher) { static $dictionary; if (empty($dictionary)) { - $dictionary = array( - 'mcrypt' => array( + $dictionary = [ + 'mcrypt' => [ 'aes-128' => 'rijndael-128', 'aes-192' => 'rijndael-128', 'aes-256' => 'rijndael-128', @@ -754,16 +757,16 @@ protected function _cipher_alias(&$cipher) 'cast5' => 'cast-128', 'rc4' => 'arcfour', 'rc4-40' => 'arcfour' - ), - 'openssl' => array( + ], + 'openssl' => [ 'rijndael-128' => 'aes-128', 'tripledes' => 'des-ede3', 'blowfish' => 'bf', 'cast-128' => 'cast5', 'arcfour' => 'rc4-40', 'rc4' => 'rc4-40' - ) - ); + ] + ]; // Notes: // @@ -797,9 +800,9 @@ protected function _cipher_alias(&$cipher) // confirms that it is MCrypt's fault. } - if (isset($dictionary[$this->_driver][$cipher])) + if (isset($dictionary[$this->handler][$cipher])) { - $cipher = $dictionary[$this->_driver][$cipher]; + $cipher = $dictionary[$this->handler][$cipher]; } } @@ -816,28 +819,29 @@ protected function _cipher_alias(&$cipher) * @param $info Optional context/application-specific info * @return string A pseudo-random key */ - public function hkdf($key, $digest = 'sha512', $salt = NULL, $length = NULL, $info = '') + public function hkdf($key, $digest = 'sha512', $salt = null, $length = null, $info = '') { - if (!isset($this->_digests[$digest])) + if ( ! isset($this->digests[$digest])) { - return FALSE; + return false; } if (empty($length) OR ! is_int($length)) { - $length = $this->_digests[$digest]; - } elseif ($length > (255 * $this->_digests[$digest])) + $length = $this->digests[$digest]; + } + elseif ($length > (255 * $this->digests[$digest])) { - return FALSE; + return false; } - self::strlen($salt) OR $salt = str_repeat("\0", $this->_digests[$digest]); + self::strlen($salt) OR $salt = str_repeat("\0", $this->digests[$digest]); - $prk = hash_hmac($digest, $key, $salt, TRUE); + $prk = hash_hmac($digest, $key, $salt, true); $key = ''; - for ($key_block = '', $block_index = 1; self::strlen($key) < $length; $block_index++) + for ($key_block = '', $block_index = 1; self::strlen($key) < $length; $block_index ++ ) { - $key_block = hash_hmac($digest, $key_block . $info . chr($block_index), $prk, TRUE); + $key_block = hash_hmac($digest, $key_block . $info . chr($block_index), $prk, true); $key .= $key_block; } @@ -857,13 +861,14 @@ public function __get($key) // Because aliases if ($key === 'mode') { - return array_search($this->_mode, $this->_modes[$this->_driver], TRUE); - } elseif (in_array($key, array('cipher', 'driver', 'drivers', 'digests'), TRUE)) + return array_search($this->mode, $this->modes[$this->handler], true); + } + elseif (in_array($key, ['cipher', 'handler', 'handlers', 'digests'], true)) { - return $this->{'_' . $key}; + return $this->{$key}; } - return NULL; + return null; } // -------------------------------------------------------------------- @@ -876,7 +881,7 @@ public function __get($key) */ protected static function strlen($str) { - return (self::$func_overload) ? mb_strlen($str, '8bit') : strlen($str); + return mb_strlen($str, '8bit'); } // -------------------------------------------------------------------- @@ -889,14 +894,9 @@ protected static function strlen($str) * @param int $length * @return string */ - protected static function substr($str, $start, $length = NULL) + protected static function substr($str, $start, $length = null) { - if (self::$func_overload) - { - return mb_substr($str, $start, $length, '8bit'); - } - - return isset($length) ? substr($str, $start, $length) : substr($str, $start); + return mb_substr($str, $start, $length, '8bit'); } } diff --git a/system/Encryption/Handlers/BaseHandler.php b/system/Encryption/Handlers/BaseHandler.php new file mode 100644 index 000000000000..0430980e29a9 --- /dev/null +++ b/system/Encryption/Handlers/BaseHandler.php @@ -0,0 +1,60 @@ +_get_params($params); + return $this->getParams($params); } // -------------------------------------------------------------------- @@ -21,7 +22,7 @@ public function __get_params($params) * * Allows checking for key changes. */ - public function get_key() + public function getKey() { return $this->_key; } @@ -31,11 +32,11 @@ public function get_key() /** * __driver_get_handle() * - * Allows checking for _mcrypt_get_handle(), _openssl_get_handle() + * Allows checking for _openssl_get_handle() */ - public function __driver_get_handle($driver, $cipher, $mode) + public function handlerGetHandle($driver, $cipher, $mode) { - return $this->{'_' . $driver . '_get_handle'}($cipher, $mode); + return $this->{$driver . 'GetHandle'}($cipher, $mode); } } diff --git a/tests/system/Encryption/EncryptionTest.php b/tests/system/Encryption/EncryptionTest.php index 88db30304740..12be5070dd02 100644 --- a/tests/system/Encryption/EncryptionTest.php +++ b/tests/system/Encryption/EncryptionTest.php @@ -5,7 +5,8 @@ use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Encryption\MockEncryption; -class EncryptionTest extends CIUnitTestCase { +class EncryptionTest extends CIUnitTestCase +{ public function setUp() { @@ -19,19 +20,19 @@ public function setUp() * * Covers behavior with config encryptionKey set or not */ - public function test___construct() + public function testConstructor() { // Assume no configuration from set_up() - $this->assertNull($this->encryption->get_key()); + $this->assertNull($this->encryption->getKey()); // Try with an empty value $config = new \Config\Encryption(); $this->encrypt = new MockEncryption($config); - $this->assertNull($this->encrypt->get_key()); + $this->assertNull($this->encrypt->getKey()); $config->key = str_repeat("\x0", 16); $this->encrypt = new MockEncryption($config); - $this->assertEquals(str_repeat("\x0", 16), $this->encrypt->get_key()); + $this->assertEquals(str_repeat("\x0", 16), $this->encrypt->getKey()); } // -------------------------------------------------------------------- @@ -49,11 +50,11 @@ public function test___construct() * * @link https://tools.ietf.org/rfc/rfc5869.txt */ - public function test_hkdf() + public function testHkdf() { - $vectors = array( + $vectors = [ // A.1: Basic test case with SHA-256 - array( + [ 'digest' => 'sha256', 'ikm' => "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b", 'salt' => "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c", @@ -61,9 +62,9 @@ public function test_hkdf() 'info' => "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9", // 'prk' => "\x07\x77\x09\x36\x2c\x2e\x32\xdf\x0d\xdc\x3f\x0d\xc4\x7b\xba\x63\x90\xb6\xc7\x3b\xb5\x0f\x9c\x31\x22\xec\x84\x4a\xd7\xc2\xb3\xe5", 'okm' => "\x3c\xb2\x5f\x25\xfa\xac\xd5\x7a\x90\x43\x4f\x64\xd0\x36\x2f\x2a\x2d\x2d\x0a\x90\xcf\x1a\x5a\x4c\x5d\xb0\x2d\x56\xec\xc4\xc5\xbf\x34\x00\x72\x08\xd5\xb8\x87\x18\x58\x65" - ), + ], // A.2: Test with SHA-256 and longer inputs/outputs - array( + [ 'digest' => 'sha256', 'ikm' => "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f", 'salt' => "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf", @@ -71,9 +72,9 @@ public function test_hkdf() 'info' => "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff", // 'prk' => "\x06\xa6\xb8\x8c\x58\x53\x36\x1a\x06\x10\x4c\x9c\xeb\x35\xb4\x5c\xef\x76\x00\x14\x90\x46\x71\x01\x4a\x19\x3f\x40\xc1\x5f\xc2\x44", 'okm' => "\xb1\x1e\x39\x8d\xc8\x03\x27\xa1\xc8\xe7\xf7\x8c\x59\x6a\x49\x34\x4f\x01\x2e\xda\x2d\x4e\xfa\xd8\xa0\x50\xcc\x4c\x19\xaf\xa9\x7c\x59\x04\x5a\x99\xca\xc7\x82\x72\x71\xcb\x41\xc6\x5e\x59\x0e\x09\xda\x32\x75\x60\x0c\x2f\x09\xb8\x36\x77\x93\xa9\xac\xa3\xdb\x71\xcc\x30\xc5\x81\x79\xec\x3e\x87\xc1\x4c\x01\xd5\xc1\xf3\x43\x4f\x1d\x87", - ), + ], // A.3: Test with SHA-256 and zero-length salt/info - array( + [ 'digest' => 'sha256', 'ikm' => "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b", 'salt' => '', @@ -81,8 +82,8 @@ public function test_hkdf() 'info' => '', // 'prk' => "\x19\xef\x24\xa3\x2c\x71\x7b\x16\x7f\x33\xa9\x1d\x6f\x64\x8b\xdf\x96\x59\x67\x76\xaf\xdb\x63\x77\xac\x43\x4c\x1c\x29\x3c\xcb\x04", 'okm' => "\x8d\xa4\xe7\x75\xa5\x63\xc1\x8f\x71\x5f\x80\x2a\x06\x3c\x5a\x31\xb8\xa1\x1f\x5c\x5e\xe1\x87\x9e\xc3\x45\x4e\x5f\x3c\x73\x8d\x2d\x9d\x20\x13\x95\xfa\xa4\xb6\x1a\x96\xc8", - ) - ); + ] + ]; foreach ($vectors as $test) { @@ -100,11 +101,11 @@ public function test_hkdf() ); // Test maximum length (RFC5869 says that it must be up to 255 times the digest size) - $hkdf_result = $this->encryption->hkdf('foobar', 'sha384', NULL, 48 * 255); + $hkdf_result = $this->encryption->hkdf('foobar', 'sha384', null, 48 * 255); $this->assertEquals( 12240, defined('MB_OVERLOAD_STRING') ? mb_strlen($hkdf_result, '8bit') : strlen($hkdf_result) ); - $this->assertFalse($this->encryption->hkdf('foobar', 'sha224', NULL, 28 * 255 + 1)); + $this->assertFalse($this->encryption->hkdf('foobar', 'sha224', null, 28 * 255 + 1)); // CI-specific test for an invalid digest $this->assertFalse($this->encryption->hkdf('fobar', 'sha1')); @@ -115,52 +116,52 @@ public function test_hkdf() /** * _get_params() test */ - public function test__get_params() + public function testGetParams() { $key = str_repeat("\x0", 16); // Invalid custom parameters - $params = array( + $params = [ // No cipher, mode or key - array('cipher' => 'aes-128', 'mode' => 'cbc'), - array('cipher' => 'aes-128', 'key' => $key), - array('mode' => 'cbc', 'key' => $key), + ['cipher' => 'aes-128', 'mode' => 'cbc'], + ['cipher' => 'aes-128', 'key' => $key], + ['mode' => 'cbc', 'key' => $key], // No HMAC key or not a valid digest - array('cipher' => 'aes-128', 'mode' => 'cbc', 'key' => $key), - array('cipher' => 'aes-128', 'mode' => 'cbc', 'key' => $key, 'hmac_digest' => 'sha1', 'hmac_key' => $key), + ['cipher' => 'aes-128', 'mode' => 'cbc', 'key' => $key], + ['cipher' => 'aes-128', 'mode' => 'cbc', 'key' => $key, 'hmac_digest' => 'sha1', 'hmac_key' => $key], // Invalid mode - array('cipher' => 'aes-128', 'mode' => 'foo', 'key' => $key, 'hmac_digest' => 'sha256', 'hmac_key' => $key) - ); + ['cipher' => 'aes-128', 'mode' => 'foo', 'key' => $key, 'hmac_digest' => 'sha256', 'hmac_key' => $key] + ]; - for ($i = 0, $c = count($params); $i < $c; $i++) + for ($i = 0, $c = count($params); $i < $c; $i ++ ) { - $this->assertFalse($this->encryption->__get_params($params[$i])); + $this->assertFalse($this->encryption->__getParams($params[$i])); } // Valid parameters - $params = array( + $params = [ 'cipher' => 'aes-128', 'mode' => 'cbc', 'key' => str_repeat("\x0", 16), 'hmac_key' => str_repeat("\x0", 16) - ); + ]; - $this->assertTrue(is_array($this->encryption->__get_params($params))); + $this->assertTrue(is_array($this->encryption->__getParams($params))); $params['base64'] = TRUE; $params['hmac_digest'] = 'sha512'; // Including all parameters - $params = array( + $params = [ 'cipher' => 'aes-128', 'mode' => 'cbc', 'key' => str_repeat("\x0", 16), 'raw_data' => TRUE, 'hmac_key' => str_repeat("\x0", 16), 'hmac_digest' => 'sha256' - ); + ]; - $output = $this->encryption->__get_params($params); + $output = $this->encryption->__getParams($params); unset($output['handle'], $output['cipher'], $params['raw_data'], $params['cipher']); $params['base64'] = FALSE; $this->assertEquals($params, $output); @@ -169,10 +170,10 @@ public function test__get_params() unset($params['hmac_key'], $params['hmac_digest']); $params['hmac'] = $params['raw_data'] = FALSE; $params['cipher'] = 'aes-128'; - $output = $this->encryption->__get_params($params); + $output = $this->encryption->__getParams($params); unset($output['handle'], $output['cipher'], $params['hmac'], $params['raw_data'], $params['cipher']); $params['base64'] = TRUE; - $params['hmac_digest'] = $params['hmac_key'] = NULL; + $params['hmac_digest'] = $params['hmac_key'] = null; $this->assertEquals($params, $output); } @@ -188,7 +189,7 @@ public function test__get_params() * @depends test_hkdf * @depends test__get_params */ - public function test_initialize_encrypt_decrypt() + public function testInitializeEncryptDecrypt() { $message = 'This is a plain-text message.'; $key = "\xd0\xc9\x08\xc4\xde\x52\x12\x6e\xf8\xcc\xdb\x03\xea\xa0\x3a\x5c"; @@ -197,7 +198,7 @@ public function test_initialize_encrypt_decrypt() $this->encryption->initialize(array('key' => $key)); // Was the key properly set? - $this->assertEquals($key, $this->encryption->get_key()); + $this->assertEquals($key, $this->encryption->getKey()); $this->assertEquals($message, $this->encryption->decrypt($this->encryption->encrypt($message))); @@ -213,7 +214,7 @@ public function test_initialize_encrypt_decrypt() * * @depends test___get_params */ - public function test_encrypt_decrypt_custom() + public function testEncryptDecryptCustom() { $message = 'Another plain-text message.'; @@ -222,13 +223,13 @@ public function test_encrypt_decrypt_custom() $this->assertFalse($this->encryption->decrypt($message, array('foo'))); // No HMAC, binary output - $params = array( + $params = [ 'cipher' => 'tripledes', 'mode' => 'cfb', 'key' => str_repeat("\x1", 16), 'base64' => FALSE, 'hmac' => FALSE - ); + ]; $ciphertext = $this->encryption->encrypt($message, $params); @@ -240,17 +241,18 @@ public function test_encrypt_decrypt_custom() /** * _mcrypt_get_handle() test */ - public function test__mcrypt_get_handle() + public function testMcryptGetHandle() { - if ($this->encryption->drivers['mcrypt'] === FALSE) + if ($this->encryption->handlers['mcrypt'] === FALSE) { return $this->markTestSkipped('Cannot test MCrypt because it is not available.'); - } elseif (version_compare(PHP_VERSION, '7.1.0-alpha', '>=')) + } + elseif (version_compare(PHP_VERSION, '7.1.0-alpha', '>=')) { return $this->markTestSkipped('ext/mcrypt is deprecated since PHP 7.1 and will generate notices here.'); } - $this->assertTrue(is_resource($this->encryption->__driver_get_handle('mcrypt', 'rijndael-128', 'cbc'))); + $this->assertTrue(is_resource($this->encryption->handlerGetHandle('mcrypt', 'rijndael-128', 'cbc'))); } // -------------------------------------------------------------------- @@ -258,15 +260,15 @@ public function test__mcrypt_get_handle() /** * _openssl_get_handle() test */ - public function test__openssl_mcrypt_get_handle() + public function testOpensslMcryptGetHandle() { - if ($this->encryption->drivers['openssl'] === FALSE) + if ($this->encryption->handlers['openssl'] === FALSE) { return $this->markTestSkipped('Cannot test OpenSSL because it is not available.'); } - $this->assertEquals('aes-128-cbc', $this->encryption->__driver_get_handle('openssl', 'aes-128', 'cbc')); - $this->assertEquals('rc4-40', $this->encryption->__driver_get_handle('openssl', 'rc4-40', 'stream')); + $this->assertEquals('aes-128-cbc', $this->encryption->handlerGetHandle('openssl', 'aes-128', 'cbc')); + $this->assertEquals('rc4-40', $this->encryption->handlerGetHandle('openssl', 'rc4-40', 'stream')); } // -------------------------------------------------------------------- @@ -276,13 +278,14 @@ public function test__openssl_mcrypt_get_handle() * * Amongst the obvious stuff, _cipher_alias() is also tested here. */ - public function test_portability() + public function testPortability() { - if (!$this->encryption->drivers['mcrypt'] OR ! $this->encryption->drivers['openssl']) + if ( ! $this->encryption->handlers['mcrypt'] OR ! $this->encryption->handlers['openssl']) { $this->markTestSkipped('Both MCrypt and OpenSSL support are required for portability tests.'); return; - } elseif (version_compare(PHP_VERSION, '7.1.0-alpha', '>=')) + } + elseif (version_compare(PHP_VERSION, '7.1.0-alpha', '>=')) { return $this->markTestSkipped('ext/mcrypt is deprecated since PHP 7.1 and will generate notices here.'); } @@ -290,84 +293,84 @@ public function test_portability() $message = 'This is a message encrypted via MCrypt and decrypted via OpenSSL, or vice-versa.'; // Format is: , , - $portable = array( - array('aes-128', 'cbc', 16), - array('aes-128', 'cfb', 16), - array('aes-128', 'cfb8', 16), - array('aes-128', 'ofb', 16), - array('aes-128', 'ecb', 16), - array('aes-128', 'ctr', 16), - array('aes-192', 'cbc', 24), - array('aes-192', 'cfb', 24), - array('aes-192', 'cfb8', 24), - array('aes-192', 'ofb', 24), - array('aes-192', 'ecb', 24), - array('aes-192', 'ctr', 24), - array('aes-256', 'cbc', 32), - array('aes-256', 'cfb', 32), - array('aes-256', 'cfb8', 32), - array('aes-256', 'ofb', 32), - array('aes-256', 'ecb', 32), - array('aes-256', 'ctr', 32), - array('des', 'cbc', 7), - array('des', 'cfb', 7), - array('des', 'cfb8', 7), - array('des', 'ofb', 7), - array('des', 'ecb', 7), - array('tripledes', 'cbc', 7), - array('tripledes', 'cfb', 7), - array('tripledes', 'cfb8', 7), - array('tripledes', 'ofb', 7), - array('tripledes', 'cbc', 14), - array('tripledes', 'cfb', 14), - array('tripledes', 'cfb8', 14), - array('tripledes', 'ofb', 14), - array('tripledes', 'cbc', 21), - array('tripledes', 'cfb', 21), - array('tripledes', 'cfb8', 21), - array('tripledes', 'ofb', 21), - array('blowfish', 'cbc', 16), - array('blowfish', 'cfb', 16), - array('blowfish', 'ofb', 16), - array('blowfish', 'ecb', 16), - array('blowfish', 'cbc', 56), - array('blowfish', 'cfb', 56), - array('blowfish', 'ofb', 56), - array('blowfish', 'ecb', 56), - array('cast5', 'cbc', 11), - array('cast5', 'cfb', 11), - array('cast5', 'ofb', 11), - array('cast5', 'ecb', 11), - array('cast5', 'cbc', 16), - array('cast5', 'cfb', 16), - array('cast5', 'ofb', 16), - array('cast5', 'ecb', 16), - array('rc4', 'stream', 5), - array('rc4', 'stream', 8), - array('rc4', 'stream', 16), - array('rc4', 'stream', 32), - array('rc4', 'stream', 64), - array('rc4', 'stream', 128), - array('rc4', 'stream', 256) - ); - $driver_index = array('mcrypt', 'openssl'); + $portable = [ + ['aes-128', 'cbc', 16], + ['aes-128', 'cfb', 16], + ['aes-128', 'cfb8', 16], + ['aes-128', 'ofb', 16], + ['aes-128', 'ecb', 16], + ['aes-128', 'ctr', 16], + ['aes-192', 'cbc', 24], + ['aes-192', 'cfb', 24], + ['aes-192', 'cfb8', 24], + ['aes-192', 'ofb', 24], + ['aes-192', 'ecb', 24], + ['aes-192', 'ctr', 24], + ['aes-256', 'cbc', 32], + ['aes-256', 'cfb', 32], + ['aes-256', 'cfb8', 32], + ['aes-256', 'ofb', 32], + ['aes-256', 'ecb', 32], + ['aes-256', 'ctr', 32], + ['des', 'cbc', 7], + ['des', 'cfb', 7], + ['des', 'cfb8', 7], + ['des', 'ofb', 7], + ['des', 'ecb', 7], + ['tripledes', 'cbc', 7], + ['tripledes', 'cfb', 7], + ['tripledes', 'cfb8', 7], + ['tripledes', 'ofb', 7], + ['tripledes', 'cbc', 14], + ['tripledes', 'cfb', 14], + ['tripledes', 'cfb8', 14], + ['tripledes', 'ofb', 14], + ['tripledes', 'cbc', 21], + ['tripledes', 'cfb', 21], + ['tripledes', 'cfb8', 21], + ['tripledes', 'ofb', 21], + ['blowfish', 'cbc', 16], + ['blowfish', 'cfb', 16], + ['blowfish', 'ofb', 16], + ['blowfish', 'ecb', 16], + ['blowfish', 'cbc', 56], + ['blowfish', 'cfb', 56], + ['blowfish', 'ofb', 56], + ['blowfish', 'ecb', 56], + ['cast5', 'cbc', 11], + ['cast5', 'cfb', 11], + ['cast5', 'ofb', 11], + ['cast5', 'ecb', 11], + ['cast5', 'cbc', 16], + ['cast5', 'cfb', 16], + ['cast5', 'ofb', 16], + ['cast5', 'ecb', 16], + ['rc4', 'stream', 5], + ['rc4', 'stream', 8], + ['rc4', 'stream', 16], + ['rc4', 'stream', 32], + ['rc4', 'stream', 64], + ['rc4', 'stream', 128], + ['rc4', 'stream', 256] + ]; + $handler_index = ['mcrypt', 'openssl']; foreach ($portable as &$test) { - // Add some randomness to the selected driver - $driver = mt_rand(0, 1); - $params = array( - 'driver' => $driver_index[$driver], + // Add some randomness to the selected handler + $handler = mt_rand(0, 1); + $params = [ + 'handler' => $handler_index[$handler], 'cipher' => $test[0], 'mode' => $test[1], 'key' => openssl_random_pseudo_bytes($test[2]) - ); + ]; $this->encryption->initialize($params); $ciphertext = $this->encryption->encrypt($message); - $driver = (int) !$driver; - $params['driver'] = $driver_index[$driver]; + $handler = (int) ! $handler; + $params['handler'] = $handler_index[$handler]; $this->encryption->initialize($params); $this->assertEquals($message, $this->encryption->decrypt($ciphertext)); @@ -379,13 +382,13 @@ public function test_portability() /** * __get() test */ - public function test_magic_get() + public function testMagicGet() { $this->assertNull($this->encryption->foo); - $this->assertEquals(array('mcrypt', 'openssl'), array_keys($this->encryption->drivers)); + $this->assertEquals(['mcrypt', 'openssl'], array_keys($this->encryption->handlers)); // 'stream' mode is translated into an empty string for OpenSSL - $this->encryption->initialize(array('cipher' => 'rc4', 'mode' => 'stream')); + $this->encryption->initialize(['cipher' => 'rc4', 'mode' => 'stream']); $this->assertEquals('stream', $this->encryption->mode); } From 1fabbc55864f58aa95f8296a2119e81cff78ac0c Mon Sep 17 00:00:00 2001 From: Master Yoda Date: Thu, 11 May 2017 11:20:13 -0700 Subject: [PATCH 06/18] Services::encrypter --- system/Config/Services.php | 12 ++- system/Encryption/EncrypterInterface.php | 97 ++++++++++++++++++++++ system/Encryption/Encryption.php | 16 +++- system/Encryption/Handlers/BaseHandler.php | 52 +++++++++++- 4 files changed, 170 insertions(+), 7 deletions(-) create mode 100644 system/Encryption/EncrypterInterface.php diff --git a/system/Config/Services.php b/system/Config/Services.php index 2e60619ed525..dfe6b8aa0737 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -161,8 +161,10 @@ public static function curlrequest(array $options = [], $response = null, \Confi /** * The Encryption class provides two-way encryption. + * + * @return \CodeIgniter\Encryption\EncrypterInterfrace Encryption handler */ - public static function encrypter(\Config\Encryption $config = null) + public static function encrypter(\Config\Encryption $config = null, array $params = [], $getShared = true) { if ($getShared === true) { @@ -174,7 +176,13 @@ public static function encrypter(\Config\Encryption $config = null) $config = new \Config\Encryption(); } - return new \CodeIgniter\Encryption\Encryption($config); + $encryption = new \CodeIgniter\Encryption\Encryption($config); + + $logger = self::logger(true); + $encryption->setLogger($logger); + + $encrypter = $encryption->initialize($params); + return $encrypter; } //-------------------------------------------------------------------- diff --git a/system/Encryption/EncrypterInterface.php b/system/Encryption/EncrypterInterface.php new file mode 100644 index 000000000000..97c29a4ada3d --- /dev/null +++ b/system/Encryption/EncrypterInterface.php @@ -0,0 +1,97 @@ + 64 ]; + /** + * Logger instance to record error messages and warnings. + * @var \PSR\Log\LoggerInterface + */ + protected $logger; + // -------------------------------------------------------------------- /** @@ -344,8 +353,7 @@ public function createKey($length) return random_bytes((int) $length); } catch (Exception $e) { - log_message('error', $e->getMessage()); - return false; + throw new EncryptionException('Key creation error: ' . $e->getMessage()); } $is_secure = null; @@ -513,7 +521,7 @@ public function decrypt($data, array $params = null) // Time-attack-safe comparison $diff = 0; - for ($i = 0; $i < $digest_size; $i ++ ) + for ($i = 0; $i < $digest_size; $i ++) { $diff |= ord($hmac_input[$i]) ^ ord($hmac_check[$i]); } @@ -839,7 +847,7 @@ public function hkdf($key, $digest = 'sha512', $salt = null, $length = null, $in $prk = hash_hmac($digest, $key, $salt, true); $key = ''; - for ($key_block = '', $block_index = 1; self::strlen($key) < $length; $block_index ++ ) + for ($key_block = '', $block_index = 1; self::strlen($key) < $length; $block_index ++) { $key_block = hash_hmac($digest, $key_block . $info . chr($block_index), $prk, true); $key .= $key_block; diff --git a/system/Encryption/Handlers/BaseHandler.php b/system/Encryption/Handlers/BaseHandler.php index 0430980e29a9..a264e0099614 100644 --- a/system/Encryption/Handlers/BaseHandler.php +++ b/system/Encryption/Handlers/BaseHandler.php @@ -42,7 +42,7 @@ /** * Base class for session handling */ -abstract class BaseHandler +abstract class BaseHandler implements \CodeIgniter\Encryption\EncrypterInterface { use LoggerAwareTrait; @@ -57,4 +57,54 @@ public function __construct($config) { } + + /** + * Initialize + * + * @param array $params Configuration parameters + * @return CI_Encryption + * + * @throws \CodeIgniter\Encryption\EncryptionException + */ + public function initialize(array $params); + + /** + * Create a random key + * + * @param int $length Output length + * @return string + */ + public function createKey($length); + + /** + * Encrypt + * + * @param string $data Input data + * @param array $params Input parameters + * @return string + */ + public function encrypt($data, array $params = null); + + /** + * Decrypt + * + * @param string $data Encrypted data + * @param array $params Input parameters + * @return string + */ + public function decrypt($data, array $params = null); + + /** + * HKDF + * + * @link https://tools.ietf.org/rfc/rfc5869.txt + * @param $key Input key + * @param $digest A SHA-2 hashing algorithm + * @param $salt Optional salt + * @param $length Output length (defaults to the selected digest size) + * @param $info Optional context/application-specific info + * @return string A pseudo-random key + */ + public function hkdf($key, $digest = 'sha512', $salt = null, $length = null, $info = ''); } + From 224f94a8eeba52c5cdf9b9a3b6ebbefaf0ed6d5d Mon Sep 17 00:00:00 2001 From: Master Yoda Date: Fri, 12 May 2017 03:21:27 -0700 Subject: [PATCH 07/18] Tease Encryption apart into handlers --- system/Encryption/EncrypterInterface.php | 9 +- system/Encryption/Encryption.php | 742 +----------------- system/Encryption/Handlers/BaseHandler.php | 346 +++++++- system/Encryption/Handlers/McryptHandler.php | 290 +++++++ system/Encryption/Handlers/OpenSSLHandler.php | 168 +++- 5 files changed, 800 insertions(+), 755 deletions(-) create mode 100644 system/Encryption/Handlers/McryptHandler.php diff --git a/system/Encryption/EncrypterInterface.php b/system/Encryption/EncrypterInterface.php index 97c29a4ada3d..09acca1e5c99 100644 --- a/system/Encryption/EncrypterInterface.php +++ b/system/Encryption/EncrypterInterface.php @@ -47,7 +47,8 @@ interface EncrypterInterface { /** - * Initialize + * Initialize or re-initialize an encryption handler, + * possibly with different parameters * * @param array $params Configuration parameters * @return CI_Encryption @@ -65,7 +66,7 @@ public function initialize(array $params); public function createKey($length); /** - * Encrypt + * Encrypt - convert plaintext into ciphertext * * @param string $data Input data * @param array $params Input parameters @@ -74,7 +75,7 @@ public function createKey($length); public function encrypt($data, array $params = null); /** - * Decrypt + * Decrypt - convert ciphertext into plaintext * * @param string $data Encrypted data * @param array $params Input parameters @@ -83,7 +84,7 @@ public function encrypt($data, array $params = null); public function decrypt($data, array $params = null); /** - * HKDF + * Create an HKDF random key * * @link https://tools.ietf.org/rfc/rfc5869.txt * @param $key Input key diff --git a/system/Encryption/Encryption.php b/system/Encryption/Encryption.php index 52d9adfd4aa6..93bb91fb4be7 100644 --- a/system/Encryption/Encryption.php +++ b/system/Encryption/Encryption.php @@ -58,33 +58,6 @@ class Encryption use LoggerAwareTrait; - /** - * Encryption cipher - * - * @var string - */ - protected $cipher = 'aes-128'; - - /** - * Cipher mode - * - * @var string - */ - protected $mode = 'cbc'; - - /** - * Cipher handle - * - * @var mixed - */ - protected $handle; - - /** - * Encryption key - * - * @var string - */ - protected $key; /** * PHP extension to be used @@ -100,48 +73,6 @@ class Encryption */ protected $handlers = []; - /** - * List of available modes - * - * @var array - */ - protected $modes = [ - 'mcrypt' => [ - 'cbc' => 'cbc', - 'ecb' => 'ecb', - 'ofb' => 'nofb', - 'ofb8' => 'ofb', - 'cfb' => 'ncfb', - 'cfb8' => 'cfb', - 'ctr' => 'ctr', - 'stream' => 'stream' - ], - 'openssl' => [ - 'cbc' => 'cbc', - 'ecb' => 'ecb', - 'ofb' => 'ofb', - 'cfb' => 'cfb', - 'cfb8' => 'cfb8', - 'ctr' => 'ctr', - 'stream' => '', - 'xts' => 'xts' - ] - ]; - - /** - * List of supported HMAC algorithms - * - * name => digest size pairs - * - * @var array - */ - protected $digests = [ - 'sha224' => 28, - 'sha256' => 32, - 'sha384' => 48, - 'sha512' => 64 - ]; - /** * Logger instance to record error messages and warnings. * @var \PSR\Log\LoggerInterface @@ -176,6 +107,7 @@ public function __construct($config = null) throw new EncryptionException('Unable to find an available encryption handler.'); } + $this->initialize($params); if ( ! isset($this->key) && self::strlen($key = $this->config->key) > 0) @@ -183,677 +115,7 @@ public function __construct($config = null) $this->key = $key; } - log_message('info', 'Encryption Class Initialized'); - } - - // -------------------------------------------------------------------- - - /** - * Initialize - * - * @param array $params Configuration parameters - * @return CI_Encryption - * - * @throws \CodeIgniter\Encryption\EncryptionException - */ - public function initialize(array $params) - { - if ( ! empty($params['handler'])) - { - if (isset($this->handlers[$params['handler']])) - { - if ($this->handlers[$params['handler']]) - { - $this->handler = $params['handler']; - } - else - { - throw new EncryptionException("Driver '" . $params['handler'] . "' is not available."); - } - } - else - { - throw new EncryptionException("Unknown handler '" . $params['handler'] . "' cannot be configured."); - } - } - - if (empty($this->handler)) - { - $this->handler = ($this->handlers['openssl'] === true) ? 'openssl' : 'mcrypt'; - - log_message('debug', "Encryption: Auto-configured handler '" . $this->handler . "'."); - } - - empty($params['cipher']) && $params['cipher'] = $this->cipher; - empty($params['key']) OR $this->key = $params['key']; - $this->{$this->handler . 'Initialize'}($params); - return $this; - } - - // -------------------------------------------------------------------- - - /** - * Initialize MCrypt - * - * @param array $params Configuration parameters - * @return void - * - * @throws \CodeIgniter\Encryption\EncryptionException - */ - protected function mcryptInitialize($params) - { - if ( ! empty($params['cipher'])) - { - $params['cipher'] = strtolower($params['cipher']); - $this->cipherAlias($params['cipher']); - - if ( ! in_array($params['cipher'], mcrypt_list_algorithms(), true)) - { - throw new EncryptionException('MCrypt cipher ' . strtoupper($params['cipher']) . ' is not available.'); - } - else - { - $this->cipher = $params['cipher']; - } - } - - if ( ! empty($params['mode'])) - { - $params['mode'] = strtolower($params['mode']); - if ( ! isset($this->modes['mcrypt'][$params['mode']])) - { - throw new EncryptionException('MCrypt mode ' . strtoupper($params['cipher']) . ' is not available.'); - } - else - { - $this->mode = $this->modes['mcrypt'][$params['mode']]; - } - } - - if (isset($this->cipher, $this->mode)) - { - if (is_resource($this->handle) && (strtolower(mcrypt_enc_get_algorithms_name($this->handle)) !== $this->cipher - OR strtolower(mcrypt_enc_getmodes_name($this->handle)) !== $this->mode) - ) - { - mcrypt_module_close($this->handle); - } - - if ($this->handle = mcrypt_module_open($this->cipher, '', $this->mode, '')) - { - log_message('info', 'Encryption: MCrypt cipher ' . strtoupper($this->cipher) . ' initialized in ' . strtoupper($this->mode) . ' mode.'); - } - else - { - throw new EncryptionException('Unable to initialize MCrypt with cipher ' . strtoupper($this->cipher) . ' in ' . strtoupper($this->mode) . ' mode.'); - } - } - } - - // -------------------------------------------------------------------- - - /** - * Initialize OpenSSL - * - * @param array $params Configuration parameters - * @return void - */ - protected function opensslInitialize($params) - { - if ( ! empty($params['cipher'])) - { - $params['cipher'] = strtolower($params['cipher']); - $this->cipherAlias($params['cipher']); - $this->cipher = $params['cipher']; - } - - if ( ! empty($params['mode'])) - { - $params['mode'] = strtolower($params['mode']); - if ( ! isset($this->modes['openssl'][$params['mode']])) - { - log_message('error', 'Encryption: OpenSSL mode ' . strtoupper($params['mode']) . ' is not available.'); - } - else - { - $this->mode = $this->modes['openssl'][$params['mode']]; - } - } - - if (isset($this->cipher, $this->mode)) - { - // This is mostly for the stream mode, which doesn't get suffixed in OpenSSL - $handle = empty($this->mode) ? $this->cipher : $this->cipher . '-' . $this->mode; - - if ( ! in_array($handle, openssl_get_cipher_methods(), true)) - { - $this->handle = null; - log_message('error', 'Encryption: Unable to initialize OpenSSL with method ' . strtoupper($handle) . '.'); - } - else - { - $this->handle = $handle; - log_message('info', 'Encryption: OpenSSL initialized with method ' . strtoupper($handle) . '.'); - } - } - } - - // -------------------------------------------------------------------- - - /** - * Create a random key - * - * @param int $length Output length - * @return string - */ - public function createKey($length) - { - try - { - return random_bytes((int) $length); - } catch (Exception $e) - { - throw new EncryptionException('Key creation error: ' . $e->getMessage()); - } - - $is_secure = null; - $key = openssl_random_pseudo_bytes($length, $is_secure); - return ($is_secure === true) ? $key : false; - } - - // -------------------------------------------------------------------- - - /** - * Encrypt - * - * @param string $data Input data - * @param array $params Input parameters - * @return string - */ - public function encrypt($data, array $params = null) - { - if (($params = $this->getParams($params)) === false) - { - return false; - } - - isset($params['key']) OR $params['key'] = $this->hkdf($this->key, 'sha512', null, self::strlen($this->key), 'encryption'); - - if (($data = $this->{$this->handler . 'Encrypt'}($data, $params)) === false) - { - return false; - } - - $params['base64'] && $data = base64_encode($data); - - if (isset($params['hmac_digest'])) - { - isset($params['hmackey']) OR $params['hmackey'] = $this->hkdf($this->key, 'sha512', null, null, 'authentication'); - return hash_hmac($params['hmac_digest'], $data, $params['hmackey'], ! $params['base64']) . $data; - } - - return $data; - } - - // -------------------------------------------------------------------- - - /** - * Encrypt via MCrypt - * - * @param string $data Input data - * @param array $params Input parameters - * @return string - */ - protected function mcryptEncrypt($data, $params) - { - if ( ! is_resource($params['handle'])) - { - return false; - } - - // The greater-than-1 comparison is mostly a work-around for a bug, - // where 1 is returned for ARCFour instead of 0. - $iv = (($iv_size = mcrypt_enc_get_iv_size($params['handle'])) > 1) ? $this->createKey($iv_size) : null; - - if (mcrypt_generic_init($params['handle'], $params['key'], $iv) < 0) - { - if ($params['handle'] !== $this->handle) - { - mcrypt_module_close($params['handle']); - } - - return false; - } - - // Use PKCS#7 padding in order to ensure compatibility with OpenSSL - // and other implementations outside of PHP. - if (in_array(strtolower(mcrypt_enc_getmodes_name($params['handle'])), ['cbc', 'ecb'], true)) - { - $block_size = mcrypt_enc_get_block_size($params['handle']); - $pad = $block_size - (self::strlen($data) % $block_size); - $data .= str_repeat(chr($pad), $pad); - } - - // Work-around for yet another strange behavior in MCrypt. - // - // When encrypting in ECB mode, the IV is ignored. Yet - // mcrypt_enc_get_iv_size() returns a value larger than 0 - // even if ECB is used AND mcrypt_generic_init() complains - // if you don't pass an IV with length equal to the said - // return value. - // - // This probably would've been fine (even though still wasteful), - // but OpenSSL isn't that dumb and we need to make the process - // portable, so ... - $data = (mcrypt_enc_getmodes_name($params['handle']) !== 'ECB') ? $iv . mcrypt_generic($params['handle'], $data) : mcrypt_generic($params['handle'], $data); - - mcrypt_generic_deinit($params['handle']); - if ($params['handle'] !== $this->handle) - { - mcrypt_module_close($params['handle']); - } - - return $data; - } - - // -------------------------------------------------------------------- - - /** - * Encrypt via OpenSSL - * - * @param string $data Input data - * @param array $params Input parameters - * @return string - */ - protected function opensslEncrypt($data, $params) - { - if (empty($params['handle'])) - { - return false; - } - - $iv = ($iv_size = opensslcipher_iv_length($params['handle'])) ? $this->createKey($iv_size) : null; - - $data = openssl_encrypt( - $data, $params['handle'], $params['key'], OPENSSL_RAW_DATA, $iv - ); - - if ($data === false) - { - return false; - } - - return $iv . $data; - } - - // -------------------------------------------------------------------- - - /** - * Decrypt - * - * @param string $data Encrypted data - * @param array $params Input parameters - * @return string - */ - public function decrypt($data, array $params = null) - { - if (($params = $this->getParams($params)) === false) - { - return false; - } - - if (isset($params['hmac_digest'])) - { - // This might look illogical, but it is done during encryption as well ... - // The 'base64' value is effectively an inverted "raw data" parameter - $digest_size = ($params['base64']) ? $this->digests[$params['hmac_digest']] * 2 : $this->digests[$params['hmac_digest']]; - - if (self::strlen($data) <= $digest_size) - { - return false; - } - - $hmac_input = self::substr($data, 0, $digest_size); - $data = self::substr($data, $digest_size); - - isset($params['hmackey']) OR $params['hmackey'] = $this->hkdf($this->key, 'sha512', null, null, 'authentication'); - $hmac_check = hash_hmac($params['hmac_digest'], $data, $params['hmackey'], ! $params['base64']); - - // Time-attack-safe comparison - $diff = 0; - for ($i = 0; $i < $digest_size; $i ++) - { - $diff |= ord($hmac_input[$i]) ^ ord($hmac_check[$i]); - } - - if ($diff !== 0) - { - return false; - } - } - - if ($params['base64']) - { - $data = base64_decode($data); - } - - isset($params['key']) OR $params['key'] = $this->hkdf($this->key, 'sha512', null, self::strlen($this->key), 'encryption'); - - return $this->{$this->handler . 'Decrypt'}($data, $params); - } - - // -------------------------------------------------------------------- - - /** - * Decrypt via MCrypt - * - * @param string $data Encrypted data - * @param array $params Input parameters - * @return string - */ - protected function mcryptDecrypt($data, $params) - { - if ( ! is_resource($params['handle'])) - { - return false; - } - - // The greater-than-1 comparison is mostly a work-around for a bug, - // where 1 is returned for ARCFour instead of 0. - if (($iv_size = mcrypt_enc_get_iv_size($params['handle'])) > 1) - { - if (mcrypt_enc_getmodes_name($params['handle']) !== 'ECB') - { - $iv = self::substr($data, 0, $iv_size); - $data = self::substr($data, $iv_size); - } - else - { - // MCrypt is dumb and this is ignored, only size matters - $iv = str_repeat("\x0", $iv_size); - } - } - else - { - $iv = null; - } - - if (mcrypt_generic_init($params['handle'], $params['key'], $iv) < 0) - { - if ($params['handle'] !== $this->handle) - { - mcrypt_module_close($params['handle']); - } - - return false; - } - - $data = mdecrypt_generic($params['handle'], $data); - // Remove PKCS#7 padding, if necessary - if (in_array(strtolower(mcrypt_enc_getmodes_name($params['handle'])), ['cbc', 'ecb'], true)) - { - $data = self::substr($data, 0, -ord($data[self::strlen($data) - 1])); - } - - mcrypt_generic_deinit($params['handle']); - if ($params['handle'] !== $this->handle) - { - mcrypt_module_close($params['handle']); - } - - return $data; - } - - // -------------------------------------------------------------------- - - /** - * Decrypt via OpenSSL - * - * @param string $data Encrypted data - * @param array $params Input parameters - * @return string - */ - protected function opensslDecrypt($data, $params) - { - if ($iv_size = opensslcipher_iv_length($params['handle'])) - { - $iv = self::substr($data, 0, $iv_size); - $data = self::substr($data, $iv_size); - } - else - { - $iv = null; - } - - return empty($params['handle']) ? false : openssl_decrypt( - $data, $params['handle'], $params['key'], OPENSSL_RAW_DATA, $iv - ); - } - - // -------------------------------------------------------------------- - - /** - * Get params - * - * @param array $params Input parameters - * @return array - */ - protected function getParams($params) - { - if (empty($params)) - { - return isset($this->cipher, $this->mode, $this->key, $this->handle) ? [ - 'handle' => $this->handle, - 'cipher' => $this->cipher, - 'mode' => $this->mode, - 'key' => null, - 'base64' => true, - 'hmac_digest' => 'sha512', - 'hmackey' => null - ] : false; - } - elseif ( ! isset($params['cipher'], $params['mode'], $params['key'])) - { - return false; - } - - if (isset($params['mode'])) - { - $params['mode'] = strtolower($params['mode']); - if ( ! isset($this->modes[$this->handler][$params['mode']])) - { - return false; - } - else - { - $params['mode'] = $this->modes[$this->handler][$params['mode']]; - } - } - - if (isset($params['hmac']) && $params['hmac'] === false) - { - $params['hmac_digest'] = $params['hmackey'] = null; - } - else - { - if ( ! isset($params['hmackey'])) - { - return false; - } - elseif (isset($params['hmac_digest'])) - { - $params['hmac_digest'] = strtolower($params['hmac_digest']); - if ( ! isset($this->digests[$params['hmac_digest']])) - { - return false; - } - } - else - { - $params['hmac_digest'] = 'sha512'; - } - } - - $params = [ - 'handle' => null, - 'cipher' => $params['cipher'], - 'mode' => $params['mode'], - 'key' => $params['key'], - 'base64' => isset($params['raw_data']) ? ! $params['raw_data'] : false, - 'hmac_digest' => $params['hmac_digest'], - 'hmackey' => $params['hmackey'] - ]; - - $this->cipherAlias($params['cipher']); - $params['handle'] = ($params['cipher'] !== $this->cipher OR $params['mode'] !== $this->mode) ? $this->{$this->handler . 'Gethandle'}($params['cipher'], $params['mode']) : $this->handle; - - return $params; - } - - // -------------------------------------------------------------------- - - /** - * Get MCrypt handle - * - * @param string $cipher Cipher name - * @param string $mode Encryption mode - * @return resource - */ - protected function mcryptGetHandle($cipher, $mode) - { - return mcrypt_module_open($cipher, '', $mode, ''); - } - - // -------------------------------------------------------------------- - - /** - * Get OpenSSL handle - * - * @param string $cipher Cipher name - * @param string $mode Encryption mode - * @return string - */ - protected function opensslGetHandle($cipher, $mode) - { - // OpenSSL methods aren't suffixed with '-stream' for this mode - return ($mode === 'stream') ? $cipher : $cipher . '-' . $mode; - } - - // -------------------------------------------------------------------- - - /** - * Cipher alias - * - * Tries to translate cipher names between MCrypt and OpenSSL's "dialects". - * - * @param string $cipher Cipher name - * @return void - */ - protected function cipherAlias(&$cipher) - { - static $dictionary; - - if (empty($dictionary)) - { - $dictionary = [ - 'mcrypt' => [ - 'aes-128' => 'rijndael-128', - 'aes-192' => 'rijndael-128', - 'aes-256' => 'rijndael-128', - 'des3-ede3' => 'tripledes', - 'bf' => 'blowfish', - 'cast5' => 'cast-128', - 'rc4' => 'arcfour', - 'rc4-40' => 'arcfour' - ], - 'openssl' => [ - 'rijndael-128' => 'aes-128', - 'tripledes' => 'des-ede3', - 'blowfish' => 'bf', - 'cast-128' => 'cast5', - 'arcfour' => 'rc4-40', - 'rc4' => 'rc4-40' - ] - ]; - - // Notes: - // - // - Rijndael-128 is, at the same time all three of AES-128, - // AES-192 and AES-256. The only difference between them is - // the key size. Rijndael-192, Rijndael-256 on the other hand - // also have different block sizes and are NOT AES-compatible. - // - // - Blowfish is said to be supporting key sizes between - // 4 and 56 bytes, but it appears that between MCrypt and - // OpenSSL, only those of 16 and more bytes are compatible. - // Also, don't know what MCrypt's 'blowfish-compat' is. - // - // - CAST-128/CAST5 produces a longer cipher when encrypted via - // OpenSSL, but (strangely enough) can be decrypted by either - // extension anyway. - // Also, it appears that OpenSSL uses 16 rounds regardless of - // the key size, while RFC2144 says that for key sizes lower - // than 11 bytes, only 12 rounds should be used. This makes - // it portable only with keys of between 11 and 16 bytes. - // - // - RC4 (ARCFour) has a strange implementation under OpenSSL. - // Its 'rc4-40' cipher method seems to work flawlessly, yet - // there's another one, 'rc4' that only works with a 16-byte key. - // - // - DES is compatible, but doesn't need an alias. - // - // Other seemingly matching ciphers between MCrypt, OpenSSL: - // - // - RC2 is NOT compatible and only an obscure forum post - // confirms that it is MCrypt's fault. - } - - if (isset($dictionary[$this->handler][$cipher])) - { - $cipher = $dictionary[$this->handler][$cipher]; - } - } - - // -------------------------------------------------------------------- - - /** - * HKDF - * - * @link https://tools.ietf.org/rfc/rfc5869.txt - * @param $key Input key - * @param $digest A SHA-2 hashing algorithm - * @param $salt Optional salt - * @param $length Output length (defaults to the selected digest size) - * @param $info Optional context/application-specific info - * @return string A pseudo-random key - */ - public function hkdf($key, $digest = 'sha512', $salt = null, $length = null, $info = '') - { - if ( ! isset($this->digests[$digest])) - { - return false; - } - - if (empty($length) OR ! is_int($length)) - { - $length = $this->digests[$digest]; - } - elseif ($length > (255 * $this->digests[$digest])) - { - return false; - } - - self::strlen($salt) OR $salt = str_repeat("\0", $this->digests[$digest]); - - $prk = hash_hmac($digest, $key, $salt, true); - $key = ''; - for ($key_block = '', $block_index = 1; self::strlen($key) < $length; $block_index ++) - { - $key_block = hash_hmac($digest, $key_block . $info . chr($block_index), $prk, true); - $key .= $key_block; - } - - return self::substr($key, 0, $length); + log_message('info', 'Encryption class Initialized'); } // -------------------------------------------------------------------- diff --git a/system/Encryption/Handlers/BaseHandler.php b/system/Encryption/Handlers/BaseHandler.php index a264e0099614..40f129130673 100644 --- a/system/Encryption/Handlers/BaseHandler.php +++ b/system/Encryption/Handlers/BaseHandler.php @@ -1,4 +1,6 @@ - digest size pairs + * + * @var array + */ + protected $digests = [ + 'sha224' => 28, + 'sha256' => 32, + 'sha384' => 48, + 'sha512' => 64 + ]; + + /** + * Logger instance to record error messages and warnings. + * @var \PSR\Log\LoggerInterface + */ + protected $logger; + +//-------------------------------------------------------------------- /** * Constructor @@ -55,9 +111,32 @@ abstract class BaseHandler implements \CodeIgniter\Encryption\EncrypterInterface */ public function __construct($config) { + if (empty($config)) + $config = new \Config\Encryption(); + $this->config = $config; + + $params = (array) $this->config; + + $this->handlers = [ + 'mcrypt' => defined('MCRYPT_DEV_URANDOM'), + 'openssl' => extension_loaded('openssl') + ]; + + if ( ! $this->handlers['mcrypt'] && ! $this->handlers['openssl']) + { + throw new EncryptionException('Unable to find an available encryption handler.'); + } + + $this->initialize($params); + + if ( ! isset($this->key) && self::strlen($key = $this->config->key) > 0) + { + $this->key = $key; + } + + log_message('info', 'Encryption handler Initialized'); } - /** * Initialize * @@ -66,7 +145,41 @@ public function __construct($config) * * @throws \CodeIgniter\Encryption\EncryptionException */ - public function initialize(array $params); + public function initialize(array $params) + { + if ( ! empty($params['handler'])) + { + if (isset($this->handlers[$params['handler']])) + { + if ($this->handlers[$params['handler']]) + { + $this->handler = $params['handler']; + } + else + { + throw new EncryptionException("Driver '" . $params['handler'] . "' is not available."); + } + } + else + { + throw new EncryptionException("Unknown handler '" . $params['handler'] . "' cannot be configured."); + } + } + + if (empty($this->handler)) + { + $this->handler = ($this->handlers['openssl'] === true) ? 'openssl' : 'mcrypt'; + + log_message('debug', "Encryption: Auto-configured handler '" . $this->handler . "'."); + } + + empty($params['cipher']) && $params['cipher'] = $this->cipher; + empty($params['key']) OR $this->key = $params['key']; + $this->initializeIt($params); + return $this; + } + +// -------------------------------------------------------------------- /** * Create a random key @@ -74,7 +187,20 @@ public function initialize(array $params); * @param int $length Output length * @return string */ - public function createKey($length); + public function createKey($length) + { + try + { + return random_bytes((int) $length); + } catch (Exception $e) + { + throw new EncryptionException('Key creation error: ' . $e->getMessage()); + } + + $is_secure = null; + $key = openssl_random_pseudo_bytes($length, $is_secure); + return ($is_secure === true) ? $key : false; + } /** * Encrypt @@ -83,7 +209,30 @@ public function createKey($length); * @param array $params Input parameters * @return string */ - public function encrypt($data, array $params = null); + public function encrypt($data, array $params = null) + { + if (($params = $this->getParams($params)) === false) + { + return false; + } + + isset($params['key']) OR $params['key'] = $this->hkdf($this->key, 'sha512', null, self::strlen($this->key), 'encryption'); + + if (($data = $this->encryptIt($data, $params)) === false) + { + return false; + } + + $params['base64'] && $data = base64_encode($data); + + if (isset($params['hmac_digest'])) + { + isset($params['hmackey']) OR $params['hmackey'] = $this->hkdf($this->key, 'sha512', null, null, 'authentication'); + return hash_hmac($params['hmac_digest'], $data, $params['hmackey'], ! $params['base64']) . $data; + } + + return $data; + } /** * Decrypt @@ -92,7 +241,146 @@ public function encrypt($data, array $params = null); * @param array $params Input parameters * @return string */ - public function decrypt($data, array $params = null); + public function decrypt($data, array $params = null) + { + if (($params = $this->getParams($params)) === false) + { + return false; + } + + if (isset($params['hmac_digest'])) + { + // This might look illogical, but it is done during encryption as well ... + // The 'base64' value is effectively an inverted "raw data" parameter + $digest_size = ($params['base64']) ? $this->digests[$params['hmac_digest']] * 2 : $this->digests[$params['hmac_digest']]; + + if (self::strlen($data) <= $digest_size) + { + return false; + } + + $hmac_input = self::substr($data, 0, $digest_size); + $data = self::substr($data, $digest_size); + + isset($params['hmackey']) OR $params['hmackey'] = $this->hkdf($this->key, 'sha512', null, null, 'authentication'); + $hmac_check = hash_hmac($params['hmac_digest'], $data, $params['hmackey'], ! $params['base64']); + + // Time-attack-safe comparison + $diff = 0; + for ($i = 0; $i < $digest_size; $i ++ ) + { + $diff |= ord($hmac_input[$i]) ^ ord($hmac_check[$i]); + } + + if ($diff !== 0) + { + return false; + } + } + + if ($params['base64']) + { + $data = base64_decode($data); + } + + isset($params['key']) OR $params['key'] = $this->hkdf($this->key, 'sha512', null, self::strlen($this->key), 'encryption'); + + return $this->decryptIt($data, $params); + } + +// -------------------------------------------------------------------- + + /** + * Get params + * + * @param array $params Input parameters + * @return array + */ + protected function getParams($params) + { + if (empty($params)) + { + return isset($this->cipher, $this->mode, $this->key, $this->handle) ? [ + 'handle' => $this->handle, + 'cipher' => $this->cipher, + 'mode' => $this->mode, + 'key' => null, + 'base64' => true, + 'hmac_digest' => 'sha512', + 'hmackey' => null + ] : false; + } + elseif ( ! isset($params['cipher'], $params['mode'], $params['key'])) + { + return false; + } + + if (isset($params['mode'])) + { + $params['mode'] = strtolower($params['mode']); + if ( ! isset($this->modes[$params['mode']])) + { + return false; + } + else + { + $params['mode'] = $this->modes[$params['mode']]; + } + } + + if (isset($params['hmac']) && $params['hmac'] === false) + { + $params['hmac_digest'] = $params['hmackey'] = null; + } + else + { + if ( ! isset($params['hmackey'])) + { + return false; + } + elseif (isset($params['hmac_digest'])) + { + $params['hmac_digest'] = strtolower($params['hmac_digest']); + if ( ! isset($this->digests[$params['hmac_digest']])) + { + return false; + } + } + else + { + $params['hmac_digest'] = 'sha512'; + } + } + + $params = [ + 'handle' => null, + 'cipher' => $params['cipher'], + 'mode' => $params['mode'], + 'key' => $params['key'], + 'base64' => isset($params['raw_data']) ? ! $params['raw_data'] : false, + 'hmac_digest' => $params['hmac_digest'], + 'hmackey' => $params['hmackey'] + ]; + + $this->cipherAlias($params['cipher']); + $params['handle'] = ($params['cipher'] !== $this->cipher OR $params['mode'] !== $this->mode) ? $this->getHandle($params['cipher'], $params['mode']) : $this->handle; + + return $params; + } + +// -------------------------------------------------------------------- + + /** + * Get handler's handle + * + * @param string $cipher Cipher name + * @param string $mode Encryption mode + * @return string + */ + abstract protected function getHandle($cipher, $mode); + + +// -------------------------------------------------------------------- /** * HKDF @@ -105,6 +393,44 @@ public function decrypt($data, array $params = null); * @param $info Optional context/application-specific info * @return string A pseudo-random key */ - public function hkdf($key, $digest = 'sha512', $salt = null, $length = null, $info = ''); -} + public function hkdf($key, $digest = 'sha512', $salt = null, $length = null, $info = '') + { + if ( ! isset($this->digests[$digest])) + { + return false; + } + + if (empty($length) OR ! is_int($length)) + { + $length = $this->digests[$digest]; + } + elseif ($length > (255 * $this->digests[$digest])) + { + return false; + } + + self::strlen($salt) OR $salt = str_repeat("\0", $this->digests[$digest]); + + $prk = hash_hmac($digest, $key, $salt, true); + $key = ''; + for ($key_block = '', $block_index = 1; self::strlen($key) < $length; $block_index ++ ) + { + $key_block = hash_hmac($digest, $key_block . $info . chr($block_index), $prk, true); + $key .= $key_block; + } + + return self::substr($key, 0, $length); + } +// -------------------------------------------------------------------- + + /** + * Cipher alias + * + * Tries to translate cipher names as appropriate for this handler + * + * @param string $cipher Cipher name + * @return void + */ + abstract protected function cipherAlias(&$cipher); +} diff --git a/system/Encryption/Handlers/McryptHandler.php b/system/Encryption/Handlers/McryptHandler.php new file mode 100644 index 000000000000..2040e39a959d --- /dev/null +++ b/system/Encryption/Handlers/McryptHandler.php @@ -0,0 +1,290 @@ + 'cbc', + 'ecb' => 'ecb', + 'ofb' => 'nofb', + 'ofb8' => 'ofb', + 'cfb' => 'ncfb', + 'cfb8' => 'cfb', + 'ctr' => 'ctr', + 'stream' => 'stream' + ]; + + // -------------------------------------------------------------------- + + /** + * Initialize MCrypt + * + * @param array $params Configuration parameters + * @return void + * + * @throws \CodeIgniter\Encryption\EncryptionException + */ + protected function initializeIt($params) + { + if ( ! empty($params['cipher'])) + { + $params['cipher'] = strtolower($params['cipher']); + $this->cipherAlias($params['cipher']); + + if ( ! in_array($params['cipher'], mcrypt_list_algorithms(), true)) + { + throw new EncryptionException('MCrypt cipher ' . strtoupper($params['cipher']) . ' is not available.'); + } + else + { + $this->cipher = $params['cipher']; + } + } + + if ( ! empty($params['mode'])) + { + $params['mode'] = strtolower($params['mode']); + if ( ! isset($this->modes[$params['mode']])) + { + throw new EncryptionException('MCrypt mode ' . strtoupper($params['cipher']) . ' is not available.'); + } + else + { + $this->mode = $this->modes[$params['mode']]; + } + } + + if (isset($this->cipher, $this->mode)) + { + if (is_resource($this->handle) && (strtolower(mcrypt_enc_get_algorithms_name($this->handle)) !== $this->cipher + OR strtolower(mcrypt_enc_getmodes_name($this->handle)) !== $this->mode) + ) + { + mcrypt_module_close($this->handle); + } + + if ($this->handle = mcrypt_module_open($this->cipher, '', $this->mode, '')) + { + log_message('info', 'Encryption: MCrypt cipher ' . strtoupper($this->cipher) . ' initialized in ' . strtoupper($this->mode) . ' mode.'); + } + else + { + throw new EncryptionException('Unable to initialize MCrypt with cipher ' . strtoupper($this->cipher) . ' in ' . strtoupper($this->mode) . ' mode.'); + } + } + } + + /** + * Encrypt + * + * @param string $data Input data + * @param array $params Input parameters + * @return string + */ + public function encryptIt($data, array $params = null) + { + + if ( ! is_resource($params['handle'])) + { + return false; + } + + // The greater-than-1 comparison is mostly a work-around for a bug, + // where 1 is returned for ARCFour instead of 0. + $iv = (($iv_size = mcrypt_enc_get_iv_size($params['handle'])) > 1) ? $this->createKey($iv_size) : null; + + if (mcrypt_generic_init($params['handle'], $params['key'], $iv) < 0) + { + if ($params['handle'] !== $this->handle) + { + mcrypt_module_close($params['handle']); + } + + return false; + } + + // Use PKCS#7 padding in order to ensure compatibility with OpenSSL + // and other implementations outside of PHP. + if (in_array(strtolower(mcrypt_enc_getmodes_name($params['handle'])), ['cbc', 'ecb'], true)) + { + $block_size = mcrypt_enc_get_block_size($params['handle']); + $pad = $block_size - (self::strlen($data) % $block_size); + $data .= str_repeat(chr($pad), $pad); + } + + // Work-around for yet another strange behavior in MCrypt. + // + // When encrypting in ECB mode, the IV is ignored. Yet + // mcrypt_enc_get_iv_size() returns a value larger than 0 + // even if ECB is used AND mcrypt_generic_init() complains + // if you don't pass an IV with length equal to the said + // return value. + // + // This probably would've been fine (even though still wasteful), + // but OpenSSL isn't that dumb and we need to make the process + // portable, so ... + $data = (mcrypt_enc_getmodes_name($params['handle']) !== 'ECB') ? $iv . mcrypt_generic($params['handle'], $data) : mcrypt_generic($params['handle'], $data); + + mcrypt_generic_deinit($params['handle']); + if ($params['handle'] !== $this->handle) + { + mcrypt_module_close($params['handle']); + } + + return $data; + } + + // -------------------------------------------------------------------- + + /** + * Decrypt + * + * @param string $data Encrypted data + * @param array $params Input parameters + * @return string + */ + public function decryptIt($data, array $params = null) + { + + if ( ! is_resource($params['handle'])) + { + return false; + } + + // The greater-than-1 comparison is mostly a work-around for a bug, + // where 1 is returned for ARCFour instead of 0. + if (($iv_size = mcrypt_enc_get_iv_size($params['handle'])) > 1) + { + if (mcrypt_enc_getmodes_name($params['handle']) !== 'ECB') + { + $iv = self::substr($data, 0, $iv_size); + $data = self::substr($data, $iv_size); + } + else + { + // MCrypt is dumb and this is ignored, only size matters + $iv = str_repeat("\x0", $iv_size); + } + } + else + { + $iv = null; + } + + if (mcrypt_generic_init($params['handle'], $params['key'], $iv) < 0) + { + if ($params['handle'] !== $this->handle) + { + mcrypt_module_close($params['handle']); + } + + return false; + } + + $data = mdecrypt_generic($params['handle'], $data); + // Remove PKCS#7 padding, if necessary + if (in_array(strtolower(mcrypt_enc_getmodes_name($params['handle'])), ['cbc', 'ecb'], true)) + { + $data = self::substr($data, 0, -ord($data[self::strlen($data) - 1])); + } + + mcrypt_generic_deinit($params['handle']); + if ($params['handle'] !== $this->handle) + { + mcrypt_module_close($params['handle']); + } + + return $data; + } + + // -------------------------------------------------------------------- + + /** + * Get MCrypt handle + * + * @param string $cipher Cipher name + * @param string $mode Encryption mode + * @return resource + */ + protected function getHandle($cipher, $mode) + { + return mcrypt_module_open($cipher, '', $mode, ''); + } + + // -------------------------------------------------------------------- + + /** + * Cipher alias + * + * Tries to translate cipher names as appropriate for this handler + * + * @param string $cipher Cipher name + * @return void + */ + protected function cipherAlias(&$cipher) + { + static $dictionary; + + if (empty($dictionary)) + { + $dictionary = [ + 'aes-128' => 'rijndael-128', + 'aes-192' => 'rijndael-128', + 'aes-256' => 'rijndael-128', + 'des3-ede3' => 'tripledes', + 'bf' => 'blowfish', + 'cast5' => 'cast-128', + 'rc4' => 'arcfour', + 'rc4-40' => 'arcfour' + ]; + } + + if (isset($dictionary[$cipher])) + { + $cipher = $dictionary[$cipher]; + } + } + +} diff --git a/system/Encryption/Handlers/OpenSSLHandler.php b/system/Encryption/Handlers/OpenSSLHandler.php index 1b913afe27b5..d527babf5f54 100644 --- a/system/Encryption/Handlers/OpenSSLHandler.php +++ b/system/Encryption/Handlers/OpenSSLHandler.php @@ -39,5 +39,171 @@ */ class OpenSSLHandler extends BaseHandler { - + + /** + * List of available modes + * + * @var array + */ + protected $modes = [ + 'cbc' => 'cbc', + 'ecb' => 'ecb', + 'ofb' => 'ofb', + 'cfb' => 'cfb', + 'cfb8' => 'cfb8', + 'ctr' => 'ctr', + 'stream' => '', + 'xts' => 'xts' + ]; + + // -------------------------------------------------------------------- + + /** + * Initialize OpenSSL + * + * @param array $params Configuration parameters + * @return void + */ + protected function initializeIt($params) + { + if ( ! empty($params['cipher'])) + { + $params['cipher'] = strtolower($params['cipher']); + $this->cipherAlias($params['cipher']); + $this->cipher = $params['cipher']; + } + + if ( ! empty($params['mode'])) + { + $params['mode'] = strtolower($params['mode']); + if ( ! isset($this->modes[$params['mode']])) + { + log_message('error', 'Encryption: OpenSSL mode ' . strtoupper($params['mode']) . ' is not available.'); + } + else + { + $this->mode = $this->modes[$params['mode']]; + } + } + + if (isset($this->cipher, $this->mode)) + { + // This is mostly for the stream mode, which doesn't get suffixed in OpenSSL + $handle = empty($this->mode) ? $this->cipher : $this->cipher . '-' . $this->mode; + + if ( ! in_array($handle, openssl_get_cipher_methods(), true)) + { + $this->handle = null; + log_message('error', 'Encryption: Unable to initialize OpenSSL with method ' . strtoupper($handle) . '.'); + } + else + { + $this->handle = $handle; + log_message('info', 'Encryption: OpenSSL initialized with method ' . strtoupper($handle) . '.'); + } + } + } + + /** + * Encrypt + * + * @param string $data Input data + * @param array $params Input parameters + * @return string + */ + public function encryptIt($data, array $params = null) + { + if (empty($params['handle'])) + { + return false; + } + + $iv = ($iv_size = opensslcipher_iv_length($params['handle'])) ? $this->createKey($iv_size) : null; + + $data = openssl_encrypt( + $data, $params['handle'], $params['key'], OPENSSL_RAW_DATA, $iv + ); + + if ($data === false) + { + return false; + } + + return $iv . $data; + } + + // -------------------------------------------------------------------- + + /** + * Decrypt + * + * @param string $data Encrypted data + * @param array $params Input parameters + * @return string + */ + public function decryptIt($data, array $params = null) + { + + if ($iv_size = opensslcipher_iv_length($params['handle'])) + { + $iv = self::substr($data, 0, $iv_size); + $data = self::substr($data, $iv_size); + } + else + { + $iv = null; + } + + return empty($params['handle']) ? false : openssl_decrypt( + $data, $params['handle'], $params['key'], OPENSSL_RAW_DATA, $iv + ); + } + + // -------------------------------------------------------------------- + + /** + * Get OpenSSL handle + * + * @param string $cipher Cipher name + * @param string $mode Encryption mode + * @return string + */ + protected function getHandle($cipher, $mode) + { + // OpenSSL methods aren't suffixed with '-stream' for this mode + return ($mode === 'stream') ? $cipher : $cipher . '-' . $mode; + } + + // -------------------------------------------------------------------- + + /** + * Cipher alias + * + * Tries to translate cipher names as appropriate for this handler + * + * @param string $cipher Cipher name + * @return void + */ + protected function cipherAlias(&$cipher) + { + static $dictionary; + + if (empty($dictionary)) + { + $dictionary = [ + 'rijndael-128' => 'aes-128', + 'tripledes' => 'des-ede3', + 'blowfish' => 'bf', + 'cast-128' => 'cast5', + 'arcfour' => 'rc4-40', + 'rc4' => 'rc4-40' + ]; + } + + if (isset($dictionary[$cipher])) + { + $cipher = $dictionary[$cipher]; + } + } + } From 6f0a9e88ce5f8a7acff2223b28f1af12534f8206 Mon Sep 17 00:00:00 2001 From: Master Yoda Date: Mon, 15 May 2017 08:21:41 -0700 Subject: [PATCH 08/18] Refactor to use Logger --- system/Encryption/Encryption.php | 34 ++++++++++++++++--- system/Encryption/Handlers/BaseHandler.php | 6 ++-- system/Encryption/Handlers/McryptHandler.php | 2 +- system/Encryption/Handlers/OpenSSLHandler.php | 6 ++-- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/system/Encryption/Encryption.php b/system/Encryption/Encryption.php index 93bb91fb4be7..9cc643ee837d 100644 --- a/system/Encryption/Encryption.php +++ b/system/Encryption/Encryption.php @@ -60,7 +60,14 @@ class Encryption /** - * PHP extension to be used + * The encrypter we create + * + * @var string + */ + protected $encrypter; + + /** + * The PHP extension we plan to use * * @var string */ @@ -72,6 +79,16 @@ class Encryption * @var array */ protected $handlers = []; + + /** + * Map of handlers to handler classes + * + * @var array + */ + protected $drivers = [ + 'mcrypt' => 'Mcrypt', + 'openssl' => 'OpenSSL' + ]; /** * Logger instance to record error messages and warnings. @@ -89,13 +106,16 @@ class Encryption * * @throws \CodeIgniter\Encryption\EncryptionException */ - public function __construct($config = null) + public function __construct(\Config\Encryption $config = null, array $params = [], $getShared = true) { + $this->logger = \Config\Services::logger(true); + if (empty($config)) $config = new \Config\Encryption(); $this->config = $config; - $params = (array) $this->config; + if ($params == null) $params = (array) $this->config; + else $params = array_merge($params,(array)$config); $this->handlers = [ 'mcrypt' => defined('MCRYPT_DEV_URANDOM'), @@ -107,15 +127,19 @@ public function __construct($config = null) throw new EncryptionException('Unable to find an available encryption handler.'); } + $this->handler = $params['driver'] ?? 'openSSL'; + $this->driver = $this->drivers[$this->handler]; + $theone = 'Handlers\\'.$this->handler.'Handler'; + $this->encrypter = new $theone(); - $this->initialize($params); + $this->encrypter->initialize($params); if ( ! isset($this->key) && self::strlen($key = $this->config->key) > 0) { $this->key = $key; } - log_message('info', 'Encryption class Initialized'); + $this->logger->info('Encryption class Initialized'); } // -------------------------------------------------------------------- diff --git a/system/Encryption/Handlers/BaseHandler.php b/system/Encryption/Handlers/BaseHandler.php index 40f129130673..f597e3ddeb87 100644 --- a/system/Encryption/Handlers/BaseHandler.php +++ b/system/Encryption/Handlers/BaseHandler.php @@ -111,6 +111,8 @@ abstract class BaseHandler implements \CodeIgniter\Encryption\EncrypterInterface */ public function __construct($config) { + $this->logger = \Config\Services::logger(true); + if (empty($config)) $config = new \Config\Encryption(); $this->config = $config; @@ -134,7 +136,7 @@ public function __construct($config) $this->key = $key; } - log_message('info', 'Encryption handler Initialized'); + $this->logger->info('Encryption handler Initialized'); } /** @@ -170,7 +172,7 @@ public function initialize(array $params) { $this->handler = ($this->handlers['openssl'] === true) ? 'openssl' : 'mcrypt'; - log_message('debug', "Encryption: Auto-configured handler '" . $this->handler . "'."); + $this->logger->debug("Encryption: Auto-configured handler '" . $this->handler . "'."); } empty($params['cipher']) && $params['cipher'] = $this->cipher; diff --git a/system/Encryption/Handlers/McryptHandler.php b/system/Encryption/Handlers/McryptHandler.php index 2040e39a959d..de98c790907a 100644 --- a/system/Encryption/Handlers/McryptHandler.php +++ b/system/Encryption/Handlers/McryptHandler.php @@ -107,7 +107,7 @@ protected function initializeIt($params) if ($this->handle = mcrypt_module_open($this->cipher, '', $this->mode, '')) { - log_message('info', 'Encryption: MCrypt cipher ' . strtoupper($this->cipher) . ' initialized in ' . strtoupper($this->mode) . ' mode.'); + $this->logger->info('Encryption: MCrypt cipher ' . strtoupper($this->cipher) . ' initialized in ' . strtoupper($this->mode) . ' mode.'); } else { diff --git a/system/Encryption/Handlers/OpenSSLHandler.php b/system/Encryption/Handlers/OpenSSLHandler.php index d527babf5f54..49b8f0952bcb 100644 --- a/system/Encryption/Handlers/OpenSSLHandler.php +++ b/system/Encryption/Handlers/OpenSSLHandler.php @@ -78,7 +78,7 @@ protected function initializeIt($params) $params['mode'] = strtolower($params['mode']); if ( ! isset($this->modes[$params['mode']])) { - log_message('error', 'Encryption: OpenSSL mode ' . strtoupper($params['mode']) . ' is not available.'); + $this->logger->error('Encryption: OpenSSL mode ' . strtoupper($params['mode']) . ' is not available.'); } else { @@ -94,12 +94,12 @@ protected function initializeIt($params) if ( ! in_array($handle, openssl_get_cipher_methods(), true)) { $this->handle = null; - log_message('error', 'Encryption: Unable to initialize OpenSSL with method ' . strtoupper($handle) . '.'); + $this->logger->error('Encryption: Unable to initialize OpenSSL with method ' . strtoupper($handle) . '.'); } else { $this->handle = $handle; - log_message('info', 'Encryption: OpenSSL initialized with method ' . strtoupper($handle) . '.'); + $this->logger->info('Encryption: OpenSSL initialized with method ' . strtoupper($handle) . '.'); } } } From b26d81afaef6ea892f7018f0ad30b4f7fc9d7fdb Mon Sep 17 00:00:00 2001 From: Master Yoda Date: Tue, 16 May 2017 22:03:49 -0700 Subject: [PATCH 09/18] Refactor encryption config --- application/Config/App.php | 10 -- application/Config/Encryption.php | 49 +++++++++- system/Config/Services.php | 4 - system/Encryption/Encryption.php | 75 ++++++++++----- system/Encryption/Handlers/BaseHandler.php | 23 ----- .../source/libraries/encryption.rst | 91 ++++++++++--------- 6 files changed, 146 insertions(+), 106 deletions(-) diff --git a/application/Config/App.php b/application/Config/App.php index 7c270de791ff..21f682bf4fd3 100644 --- a/application/Config/App.php +++ b/application/Config/App.php @@ -292,16 +292,6 @@ class App extends BaseConfig */ public $errorViewPath = APPPATH.'Views/errors'; - /* - |-------------------------------------------------------------------------- - | Encryption Key - |-------------------------------------------------------------------------- - | - | If you use the Encryption class you must set - | an encryption key. See the user guide for more info. - */ - public $encryptionKey = ''; - /* |-------------------------------------------------------------------------- | Application Salt diff --git a/application/Config/Encryption.php b/application/Config/Encryption.php index 7ea3165da9f0..faaa5fa4f00e 100644 --- a/application/Config/Encryption.php +++ b/application/Config/Encryption.php @@ -4,10 +4,53 @@ use CodeIgniter\Config\BaseConfig; -class Encryption extends BaseConfig { +/** + * Encryption configuration. + * + * These are the settings used for encryption, if you don't pass a parameter + * array to the encrypter for creation/initialization. + * + */ +class Encryption extends BaseConfig +{ + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | If you use the Encryption class you must set an encryption key. + | You need to ensure it is long enough for the cipher and mode you plan to use. + | See the user guide for more info. + */ - // If you use the Encryption class, you must set an encryption key. - // See the user guide for more info. public $key = ''; + /* + |-------------------------------------------------------------------------- + | Encryption driver to use + |-------------------------------------------------------------------------- + | + | One of the supported drivers, eg 'openssl' or 'mcrypt'. + | The default driver, if you don't specify one, is 'openssl'. + */ + public $driver = 'openssl'; + + /* + |-------------------------------------------------------------------------- + | Encryption Cipher + |-------------------------------------------------------------------------- + | + | Name of the encryption cipher to use, eg 'aes-128' or 'blowfish' + */ + public $cipher = 'aes-128'; + + /* + |-------------------------------------------------------------------------- + | Encryption mode + |-------------------------------------------------------------------------- + | + | The encryption mode to use, eg 'cbc' or 'stream' + */ + public $mode = 'cbc'; + } diff --git a/system/Config/Services.php b/system/Config/Services.php index dfe6b8aa0737..72ed0d7bb308 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -177,10 +177,6 @@ public static function encrypter(\Config\Encryption $config = null, array $param } $encryption = new \CodeIgniter\Encryption\Encryption($config); - - $logger = self::logger(true); - $encryption->setLogger($logger); - $encrypter = $encryption->initialize($params); return $encrypter; } diff --git a/system/Encryption/Encryption.php b/system/Encryption/Encryption.php index 9cc643ee837d..510f09677886 100644 --- a/system/Encryption/Encryption.php +++ b/system/Encryption/Encryption.php @@ -49,16 +49,17 @@ class EncryptionException extends \Exception } /** - * CodeIgniter Encryption Class + * CodeIgniter Encryption Manager * * Provides two-way keyed encryption via PHP's MCrypt and/or OpenSSL extensions. + * This class determines the driver, cipher, and mode to use, and then + * initializes the appropriate encryption handler. */ class Encryption { use LoggerAwareTrait; - /** * The encrypter we create * @@ -66,6 +67,11 @@ class Encryption */ protected $encrypter; + /** + * Our configuration + */ + protected $config; + /** * The PHP extension we plan to use * @@ -79,15 +85,15 @@ class Encryption * @var array */ protected $handlers = []; - + /** - * Map of handlers to handler classes + * Map of drivers to handler classes, in preference order * * @var array */ protected $drivers = [ + 'openssl' => 'OpenSSL', 'mcrypt' => 'Mcrypt', - 'openssl' => 'OpenSSL' ]; /** @@ -106,42 +112,67 @@ class Encryption * * @throws \CodeIgniter\Encryption\EncryptionException */ - public function __construct(\Config\Encryption $config = null, array $params = [], $getShared = true) + public function __construct(array $params = []) { $this->logger = \Config\Services::logger(true); - - if (empty($config)) - $config = new \Config\Encryption(); - $this->config = $config; + $this->config = new \Config\Encryption(); - if ($params == null) $params = (array) $this->config; - else $params = array_merge($params,(array)$config); + if ($params == null) + // use config if no parameters given + $params = (array) $this->config; + else + // override config with passed parameters + $params = array_merge((array) $config, $params); + + // determine what is installed $this->handlers = [ + 'openssl' => extension_loaded('openssl'), 'mcrypt' => defined('MCRYPT_DEV_URANDOM'), - 'openssl' => extension_loaded('openssl') ]; if ( ! $this->handlers['mcrypt'] && ! $this->handlers['openssl']) - { throw new EncryptionException('Unable to find an available encryption handler.'); - } - $this->handler = $params['driver'] ?? 'openSSL'; - $this->driver = $this->drivers[$this->handler]; - $theone = 'Handlers\\'.$this->handler.'Handler'; - $this->encrypter = new $theone(); - + // how should this be handled? + $this->driver = $params['driver'] ?? 'OpenSSL'; + $this->handler = $this->drivers[$this->handler]; + $handlerName = 'Handlers\\' . $this->handler . 'Handler'; + $this->encrypter = new $handlerName(); + $this->encrypter->initialize($params); + // use config key if initialization didn't create one if ( ! isset($this->key) && self::strlen($key = $this->config->key) > 0) - { $this->key = $key; - } $this->logger->info('Encryption class Initialized'); } +// -------------------------------------------------------------------- + + /** + * Create a random key + * + * @param int $length Output length + * @return string + */ + public static function createKey($length) + { + try + { + return random_bytes((int) $length); + } catch (Exception $e) + { + throw new EncryptionException('Key creation error: ' . $e->getMessage()); + } + + //FIXME Is this even reachable? + $is_secure = null; + $key = openssl_random_pseudo_bytes($length, $is_secure); + return ($is_secure === true) ? $key : false; + } + // -------------------------------------------------------------------- /** diff --git a/system/Encryption/Handlers/BaseHandler.php b/system/Encryption/Handlers/BaseHandler.php index f597e3ddeb87..4c1aa456ae03 100644 --- a/system/Encryption/Handlers/BaseHandler.php +++ b/system/Encryption/Handlers/BaseHandler.php @@ -181,29 +181,6 @@ public function initialize(array $params) return $this; } -// -------------------------------------------------------------------- - - /** - * Create a random key - * - * @param int $length Output length - * @return string - */ - public function createKey($length) - { - try - { - return random_bytes((int) $length); - } catch (Exception $e) - { - throw new EncryptionException('Key creation error: ' . $e->getMessage()); - } - - $is_secure = null; - $key = openssl_random_pseudo_bytes($length, $is_secure); - return ($is_secure === true) ? $key : false; - } - /** * Encrypt * diff --git a/user_guide_src/source/libraries/encryption.rst b/user_guide_src/source/libraries/encryption.rst index d3b54c0e4c7d..e14312784ed2 100644 --- a/user_guide_src/source/libraries/encryption.rst +++ b/user_guide_src/source/libraries/encryption.rst @@ -20,6 +20,9 @@ If neither of the above dependencies is met, we simply cannot offer you a good enough implementation to meet the high standards required for proper cryptography. +.. important:: Mcrypt is being deprecated, as of PHP7.2, and we don't recommend it. + + .. contents:: :local: @@ -38,17 +41,17 @@ Like all services in CodeIgniter, it can be loaded via ``Config\Services``:: Default behavior ================ -By default, the Encryption Library will use the AES-128 cipher in CBC -mode, using your configured *encryption key* and SHA512 HMAC authentication. +By default, the Encryption Library will use the OpenSSL handler, with +the AES-128 cipher in CBC mode, +using your configured *encryption key* and SHA512 HMAC authentication. -.. note:: AES-128 is chosen both because it is proven to be strong and - because of its wide availability across different cryptographic - software and programming languages' APIs. +AES-128 is chosen both because it is proven to be strong and +because of its wide availability across different cryptographic +software and programming languages' APIs. However, the *encryption key* is not used as is. - -If you are somewhat familiar with cryptography, you should already know -that a HMAC also requires a secret key and using the same key for both +Keyed-hash message authentication (HMAC) requires a secret key, + and using the same key for both encryption and authentication is a bad practice. Because of that, two separate keys are derived from your already configured @@ -87,11 +90,11 @@ different ciphers. The key should be as random as possible and it **must not** be a regular text string, nor the output of a hashing function, etc. In order to create -a proper key, you must use the Encryption library's ``createKey()`` method +a proper key, you can use the Encryption library's ``createKey()`` method :: // $key will be assigned a 16-byte (128-bit) random key - $key = $encrypter->createKey(16); + $key = Encryption::createKey(16); The key can be either stored in your *application/Config/Encryption.php*, or you can design your own storage mechanism and pass the key dynamically @@ -124,11 +127,9 @@ Supported encryption ciphers and modes Portable ciphers ---------------- -Because MCrypt and OpenSSL (also called drivers throughout this document) -each support different sets of encryption algorithms and often implement -them in different ways, our Encryption library is designed to use them in -a portable fashion, or in other words - it enables you to use them -interchangeably, at least for the ciphers supported by both drivers. +Different encryption drivers support different sets of encryption algorithms and often implement +them in different ways. Our Encryption library is designed to use them in +a portable fashion - interchangeably, for the ciphers supported by both drivers. It is also implemented in a way that aims to match the standard implementations in other programming languages and libraries. @@ -143,11 +144,11 @@ Cipher name CodeIgniter name Key lengths (bits / bytes) Support AES-128 / Rijndael-128 aes-128 128 / 16 CBC, CTR, CFB, CFB8, OFB, ECB AES-192 aes-192 192 / 24 CBC, CTR, CFB, CFB8, OFB, ECB AES-256 aes-256 256 / 32 CBC, CTR, CFB, CFB8, OFB, ECB -DES des 56 / 7 CBC, CFB, CFB8, OFB, ECB -TripleDES tripledes 56 / 7, 112 / 14, 168 / 21 CBC, CFB, CFB8, OFB Blowfish blowfish 128-448 / 16-56 CBC, CFB, OFB, ECB CAST5 / CAST-128 cast5 88-128 / 11-16 CBC, CFB, OFB, ECB +DES des 56 / 7 CBC, CFB, CFB8, OFB, ECB RC4 / ARCFour rc4 40-2048 / 5-256 Stream +TripleDES tripledes 56 / 7, 112 / 14, 168 / 21 CBC, CFB, CFB8, OFB ======================== ================== ============================ =============================== .. important:: Because of how MCrypt works, if you fail to provide a key @@ -159,12 +160,12 @@ RC4 / ARCFour rc4 40-2048 / 5-256 Stream shown ranges is valid, although in bit terms that only happens in 8-bit increments. -.. note:: Even though CAST5 supports key lengths lower than 128 bits + Even though CAST5 supports key lengths lower than 128 bits (16 bytes), in fact they will just be zero-padded to the maximum length, as specified in `RFC 2144 `_. -.. note:: Blowfish supports key lengths as small as 32 bits (4 bytes), but + Blowfish supports key lengths as small as 32 bits (4 bytes), but our tests have shown that only lengths of 128 bits (16 bytes) or higher are properly supported by both MCrypt and OpenSSL. It is also a bad practice to use such low-length keys anyway. @@ -172,10 +173,10 @@ RC4 / ARCFour rc4 40-2048 / 5-256 Stream Driver-specific ciphers ----------------------- -As noted above, MCrypt and OpenSSL support different sets of encryption +As noted above, the encryption drivers support different sets of encryption ciphers. For portability reasons and because we haven't tested them -properly, we do not advise you to use the ones that are driver-specific, -but regardless, here's a list of most of them: +properly, we do not advise you to use the ones that are driver-specific. +For reference, here's a list of most of them: ============== ========= ============================== ========================================= @@ -184,40 +185,40 @@ Cipher name Driver Key lengths (bits / bytes) Supported modes AES-128 OpenSSL 128 / 16 CBC, CTR, CFB, CFB8, OFB, ECB, XTS AES-192 OpenSSL 192 / 24 CBC, CTR, CFB, CFB8, OFB, ECB, XTS AES-256 OpenSSL 256 / 32 CBC, CTR, CFB, CFB8, OFB, ECB, XTS -Rijndael-128 MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB -Rijndael-192 MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB -Rijndael-256 MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB -GOST MCrypt 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB -Twofish MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB +Camellia-128 OpenSSL 128 / 16 CBC, CFB, CFB8, OFB, ECB +Camellia-192 OpenSSL 192 / 24 CBC, CFB, CFB8, OFB, ECB +Camellia-256 OpenSSL 256 / 32 CBC, CFB, CFB8, OFB, ECB +RC2 OpenSSL 8-1024 / 1-128 CBC, CFB, OFB, ECB +Seed OpenSSL 128 / 16 CBC, CFB, OFB, ECB CAST-128 MCrypt 40-128 / 5-16 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB CAST-256 MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB +GOST MCrypt 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB Loki97 MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB +RC2 MCrypt 8-1024 / 1-128 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB +Rijndael-128 MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB +Rijndael-192 MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB +Rijndael-256 MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB SaferPlus MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB Serpent MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB +Twofish MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB XTEA MCrypt 128 / 16 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB -RC2 MCrypt 8-1024 / 1-128 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB -RC2 OpenSSL 8-1024 / 1-128 CBC, CFB, OFB, ECB -Camellia-128 OpenSSL 128 / 16 CBC, CFB, CFB8, OFB, ECB -Camellia-192 OpenSSL 192 / 24 CBC, CFB, CFB8, OFB, ECB -Camellia-256 OpenSSL 256 / 32 CBC, CFB, CFB8, OFB, ECB -Seed OpenSSL 128 / 16 CBC, CFB, OFB, ECB ============== ========= ============================== ========================================= .. note:: If you wish to use one of those ciphers, you'd have to pass its name in lower-case to the Encryption library. -.. note:: You've probably noticed that all AES cipers (and Rijndael-128) + You've probably noticed that all AES cipers (and Rijndael-128) are also listed in the portable ciphers list. This is because drivers support different modes for these ciphers. Also, it is important to note that AES-128 and Rijndael-128 are actually the same cipher, but **only** when used with a 128-bit key. -.. note:: CAST-128 / CAST-5 is also listed in both the portable and + CAST-128 / CAST-5 is also listed in both the portable and driver-specific ciphers list. This is because OpenSSL's implementation doesn't appear to be working correctly with key sizes of 80 bits and lower. -.. note:: RC2 is listed as supported by both MCrypt and OpenSSL. + RC2 is listed as supported by both MCrypt and OpenSSL. However, both drivers implement them differently and they are not portable. It is probably worth noting that we only found one obscure source confirming that it is MCrypt that @@ -240,15 +241,15 @@ general purposes. =========== ================== ================= =================================================================================================================================================== Mode name CodeIgniter name Driver support Additional info =========== ================== ================= =================================================================================================================================================== -CBC cbc MCrypt, OpenSSL A safe default choice -CTR ctr MCrypt, OpenSSL Considered as theoretically better than CBC, but not as widely available -CFB cfb MCrypt, OpenSSL N/A -CFB8 cfb8 MCrypt, OpenSSL Same as CFB, but operates in 8-bit mode (not recommended). -OFB ofb MCrypt, OpenSSL N/A +CBC cbc OpenSSL, MCrypt A safe default choice +CFB cfb OpenSSL, MCrypt N/A +CFB8 cfb8 OpenSSL, MCrypt Same as CFB, but operates in 8-bit mode (not recommended). +CTR ctr OpenSSL, MCrypt Considered as theoretically better than CBC, but not as widely available +ECB ecb OpenSSL, MCrypt Ignores IV (not recommended). +OFB ofb OpenSSL, MCrypt N/A OFB8 ofb8 MCrypt Same as OFB, but operates in 8-bit mode (not recommended). -ECB ecb MCrypt, OpenSSL Ignores IV (not recommended). XTS xts OpenSSL Usually used for encrypting random access data such as RAM or hard-disk storage. -Stream stream MCrypt, OpenSSL This is not actually a mode, it just says that a stream cipher is being used. Required because of the general cipher+mode initialization process. +Stream stream OpenSSL, MCrypt This is not actually a mode, it just says that a stream cipher is being used. Required because of the general cipher+mode initialization process. =========== ================== ================= =================================================================================================================================================== Message Length @@ -278,6 +279,8 @@ As noted in the "Default behavior" section above, this means using an auto-detected driver (OpenSSL has a higher priority), the AES-128 ciper in CBC mode, and your ``$key`` value. +You can pass an array of parameters to the Services... + If you wish to change that however, you need to use the ``initialize()`` method. It accepts an associative array of parameters, all of which are optional: @@ -285,7 +288,7 @@ optional: ======== =============================================== Option Possible values ======== =============================================== -driver 'mcrypt', 'openssl' +driver 'openssl', 'mcrypt' cipher Cipher name (see :ref:`ciphers-and-modes`) mode Encryption mode (see :ref:`encryption-modes`) key Encryption key From 4d69f20744b2f9379a75439937a8f5e581f31546 Mon Sep 17 00:00:00 2001 From: Master Yoda Date: Sat, 20 May 2017 08:24:18 -0700 Subject: [PATCH 10/18] Fix the writeup, improve the rest --- system/Config/Services.php | 6 +- system/Encryption/EncrypterInterface.php | 12 +- system/Encryption/Encryption.php | 91 ++++++++---- system/Encryption/Handlers/BaseHandler.php | 98 ++++++------ system/Encryption/Handlers/McryptHandler.php | 6 +- system/Encryption/Handlers/OpenSSLHandler.php | 6 +- system/HTTP/ContentSecurityPolicy.php | 92 +++++------- tests/_support/Encryption/MockEncryption.php | 4 +- tests/system/Encryption/EncryptionTest.php | 18 +-- .../source/libraries/encryption.rst | 140 ++++++++++-------- 10 files changed, 250 insertions(+), 223 deletions(-) diff --git a/system/Config/Services.php b/system/Config/Services.php index 72ed0d7bb308..93779df65a80 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -1,6 +1,4 @@ -logger = \Config\Services::logger(true); $this->config = new \Config\Encryption(); @@ -121,9 +119,14 @@ public function __construct(array $params = []) if ($params == null) // use config if no parameters given $params = (array) $this->config; - else - // override config with passed parameters - $params = array_merge((array) $config, $params); + elseif (is_object($params)) + { + // treat the paramater as a Config object + $params = (array) $params; + } + + // override base config with passed parameters + $params = array_merge((array) $this->config, $params); // determine what is installed $this->handlers = [ @@ -134,19 +137,60 @@ public function __construct(array $params = []) if ( ! $this->handlers['mcrypt'] && ! $this->handlers['openssl']) throw new EncryptionException('Unable to find an available encryption handler.'); - // how should this be handled? - $this->driver = $params['driver'] ?? 'OpenSSL'; - $this->handler = $this->drivers[$this->handler]; - $handlerName = 'Handlers\\' . $this->handler . 'Handler'; - $this->encrypter = new $handlerName(); + $this->logger->info('Encryption class Initialized'); + } - $this->encrypter->initialize($params); + /** + * Initialize + * + * @param array $params Configuration parameters + * @return \CodeIgniter\Encryption\EncrypterInterface + * + * @throws \CodeIgniter\Encryption\EncryptionException + */ + public function initialize(array $params) + { + // how should this be handled? + $this->driver = $params['driver'] ?? 'openssl'; + $this->handler = $this->drivers[$this->driver]; // use config key if initialization didn't create one if ( ! isset($this->key) && self::strlen($key = $this->config->key) > 0) $this->key = $key; + + if ( ! empty($params['handler'])) + { + if (isset($this->handlers[$params['handler']])) + { + if ($this->handlers[$params['handler']]) + { + $this->handler = $params['handler']; + } + else + { + throw new EncryptionException("Driver '" . $params['handler'] . "' is not available."); + } + } + else + { + throw new EncryptionException("Unknown handler '" . $params['handler'] . "' cannot be configured."); + } + } - $this->logger->info('Encryption class Initialized'); + if (empty($this->handler)) + { + $this->handler = ($this->handlers['openssl'] === true) ? 'openssl' : 'mcrypt'; + + $this->logger->debug("Encryption: Auto-configured handler '" . $this->handler . "'."); + } + + empty($params['cipher']) && $params['cipher'] = $this->cipher; + empty($params['key']) OR $this->key = $params['key']; + + $handlerName = 'CodeIgniter\\Encryption\\Handlers\\' . $this->handler . 'Handler'; + $this->encrypter = new $handlerName(); + $this->encrypter->initialize($params); + return $this->encrypter; } // -------------------------------------------------------------------- @@ -176,7 +220,7 @@ public static function createKey($length) // -------------------------------------------------------------------- /** - * __get() magic + * __get() magic, providing readonly access to some of our properties * * @param string $key Property name * @return mixed @@ -209,19 +253,4 @@ protected static function strlen($str) return mb_strlen($str, '8bit'); } - // -------------------------------------------------------------------- - - /** - * Byte-safe substr() - * - * @param string $str - * @param int $start - * @param int $length - * @return string - */ - protected static function substr($str, $start, $length = null) - { - return mb_substr($str, $start, $length, '8bit'); - } - } diff --git a/system/Encryption/Handlers/BaseHandler.php b/system/Encryption/Handlers/BaseHandler.php index 4c1aa456ae03..d17bbd4a368b 100644 --- a/system/Encryption/Handlers/BaseHandler.php +++ b/system/Encryption/Handlers/BaseHandler.php @@ -1,6 +1,4 @@ -logger = \Config\Services::logger(true); @@ -120,8 +118,8 @@ public function __construct($config) $params = (array) $this->config; $this->handlers = [ + 'openssl' => extension_loaded('openssl'), 'mcrypt' => defined('MCRYPT_DEV_URANDOM'), - 'openssl' => extension_loaded('openssl') ]; if ( ! $this->handlers['mcrypt'] && ! $this->handlers['openssl']) @@ -139,48 +137,6 @@ public function __construct($config) $this->logger->info('Encryption handler Initialized'); } - /** - * Initialize - * - * @param array $params Configuration parameters - * @return CI_Encryption - * - * @throws \CodeIgniter\Encryption\EncryptionException - */ - public function initialize(array $params) - { - if ( ! empty($params['handler'])) - { - if (isset($this->handlers[$params['handler']])) - { - if ($this->handlers[$params['handler']]) - { - $this->handler = $params['handler']; - } - else - { - throw new EncryptionException("Driver '" . $params['handler'] . "' is not available."); - } - } - else - { - throw new EncryptionException("Unknown handler '" . $params['handler'] . "' cannot be configured."); - } - } - - if (empty($this->handler)) - { - $this->handler = ($this->handlers['openssl'] === true) ? 'openssl' : 'mcrypt'; - - $this->logger->debug("Encryption: Auto-configured handler '" . $this->handler . "'."); - } - - empty($params['cipher']) && $params['cipher'] = $this->cipher; - empty($params['key']) OR $this->key = $params['key']; - $this->initializeIt($params); - return $this; - } - /** * Encrypt * @@ -412,4 +368,52 @@ public function hkdf($key, $digest = 'sha512', $salt = null, $length = null, $in * @return void */ abstract protected function cipherAlias(&$cipher); + + /** + * Byte-safe strlen() + * + * @param string $str + * @return int + */ + protected static function strlen($str) + { + return mb_strlen($str, '8bit'); + } + + // -------------------------------------------------------------------- + + /** + * Byte-safe substr() + * + * @param string $str + * @param int $start + * @param int $length + * @return string + */ + protected static function substr($str, $start, $length = null) + { + return mb_substr($str, $start, $length, '8bit'); + } + + /** + * __get() magic, providing readonly access to some of our properties + * + * @param string $key Property name + * @return mixed + */ + public function __get($key) + { + // Because aliases + if ($key === 'mode') + { + return array_search($this->mode, $this->modes[$this->handler], true); + } + elseif (in_array($key, ['cipher', 'mode', 'key', 'handler', 'handlers', 'digests'], true)) + { + return $this->{$key}; + } + + return null; + } + } diff --git a/system/Encryption/Handlers/McryptHandler.php b/system/Encryption/Handlers/McryptHandler.php index de98c790907a..3200b3258517 100644 --- a/system/Encryption/Handlers/McryptHandler.php +++ b/system/Encryption/Handlers/McryptHandler.php @@ -1,6 +1,4 @@ -baseURI = [(string)$uri => $reportOnly]; + $this->baseURI = [(string) $uri => $reportOnly]; return $this; } @@ -310,7 +310,7 @@ public function addConnectSrc($uri, bool $reportOnly = false) */ public function setDefaultSrc($uri, bool $reportOnly = false) { - $this->defaultSrc = [(string)$uri => $reportOnly]; + $this->defaultSrc = [(string) $uri => $reportOnly]; return $this; } @@ -471,7 +471,7 @@ public function addPluginType($mime, bool $reportOnly = false) */ public function setReportURI($uri) { - $this->reportURI = (string)$uri; + $this->reportURI = (string) $uri; return $this; } @@ -561,7 +561,6 @@ public function upgradeInsecureRequests(bool $value = true) } //-------------------------------------------------------------------- - //-------------------------------------------------------------------- // Utility //-------------------------------------------------------------------- @@ -605,37 +604,34 @@ protected function generateNonces(ResponseInterface &$response) { $body = $response->getBody(); - if (empty($body)) return; + if (empty($body)) + return; - if (! is_array($this->styleSrc)) $this->styleSrc = [$this->styleSrc]; - if (! is_array($this->scriptSrc)) $this->scriptSrc = [$this->scriptSrc]; + if ( ! is_array($this->styleSrc)) + $this->styleSrc = [$this->styleSrc]; + if ( ! is_array($this->scriptSrc)) + $this->scriptSrc = [$this->scriptSrc]; // Replace style placeholders with nonces $body = preg_replace_callback( - '/{csp-style-nonce}/', - function ($matches) - { - $nonce = bin2hex(random_bytes(12)); + '/{csp-style-nonce}/', function ($matches) { + $nonce = bin2hex(random_bytes(12)); - $this->styleSrc[] = 'nonce-'. $nonce; + $this->styleSrc[] = 'nonce-' . $nonce; - return 'nonce='.$nonce; - }, - $body + return 'nonce=' . $nonce; + }, $body ); // Replace script placeholders with nonces $body = preg_replace_callback( - '/{csp-script-nonce}/', - function ($matches) - { - $nonce = bin2hex(random_bytes(12)); + '/{csp-script-nonce}/', function ($matches) { + $nonce = bin2hex(random_bytes(12)); - $this->scriptSrc[] = 'nonce-'. $nonce; + $this->scriptSrc[] = 'nonce-' . $nonce; - return 'nonce='.$nonce; - }, - $body + return 'nonce=' . $nonce; + }, $body ); $response->setBody($body); @@ -658,20 +654,20 @@ protected function buildHeaders(ResponseInterface &$response) $directives = [ 'base-uri' => 'baseURI', - 'child-src' => 'childSrc', - 'connect-src' => 'connectSrc', - 'default-src' => 'defaultSrc', - 'font-src' => 'fontSrc', - 'form-action' => 'formAction', - 'frame-ancestors' => 'frameAncestors', - 'img-src' => 'imageSrc', - 'media-src' => 'mediaSrc', - 'object-src' => 'objectSrc', - 'plugin-types' => 'pluginTypes', - 'script-src' => 'scriptSrc', - 'style-src' => 'styleSrc', - 'sandbox' => 'sandbox', - 'report-uri' => 'reportURI' + 'child-src' => 'childSrc', + 'connect-src' => 'connectSrc', + 'default-src' => 'defaultSrc', + 'font-src' => 'fontSrc', + 'form-action' => 'formAction', + 'frame-ancestors' => 'frameAncestors', + 'img-src' => 'imageSrc', + 'media-src' => 'mediaSrc', + 'object-src' => 'objectSrc', + 'plugin-types' => 'pluginTypes', + 'script-src' => 'scriptSrc', + 'style-src' => 'styleSrc', + 'sandbox' => 'sandbox', + 'report-uri' => 'reportURI' ]; foreach ($directives as $name => $property) @@ -682,7 +678,6 @@ protected function buildHeaders(ResponseInterface &$response) $this->addToHeader($name, $this->{$property}, $response); } } - } //-------------------------------------------------------------------- @@ -698,7 +693,7 @@ protected function buildHeaders(ResponseInterface &$response) */ protected function addToHeader(string $name, $values = null, ResponseInterface &$response) { - if ( empty($values)) + if (empty($values)) { // It's possible that directives like 'sandbox' will not // have any values passed in, so add them to the main policy. @@ -711,22 +706,20 @@ protected function addToHeader(string $name, $values = null, ResponseInterface & $values = [$values => 0]; } - $sources = []; + $sources = []; $reportSources = []; foreach ($values as $value => $reportOnly) { if (is_numeric($value) && is_string($reportOnly) && ! empty($reportOnly)) { - $value = $reportOnly; + $value = $reportOnly; $reportOnly = 0; } if ($reportOnly === true) { - $reportSources[] = in_array($value, $this->validSources) - ? "'{$value}'" - : $value; + $reportSources[] = in_array($value, $this->validSources) ? "'{$value}'" : $value; } else { @@ -736,24 +729,21 @@ protected function addToHeader(string $name, $values = null, ResponseInterface & } else { - $sources[] = in_array($value, $this->validSources) - ? "'{$value}'" - : $value; + $sources[] = in_array($value, $this->validSources) ? "'{$value}'" : $value; } } } if (count($sources)) { - $response->appendHeader('Content-Security-Policy', $name.' '.implode(' ', $sources)); + $response->appendHeader('Content-Security-Policy', $name . ' ' . implode(' ', $sources)); } if (count($reportSources)) { - $response->appendHeader('Content-Security-Policy-Report-Only', $name.' '.implode(' ', $reportSources)); + $response->appendHeader('Content-Security-Policy-Report-Only', $name . ' ' . implode(' ', $reportSources)); } } //-------------------------------------------------------------------- - } diff --git a/tests/_support/Encryption/MockEncryption.php b/tests/_support/Encryption/MockEncryption.php index 0601d7097a7c..b94417abb8d6 100644 --- a/tests/_support/Encryption/MockEncryption.php +++ b/tests/_support/Encryption/MockEncryption.php @@ -1,6 +1,4 @@ -encrypter = \Config\Services::encrypter(); + foreach ($vectors as $test) { $this->assertEquals( - $test['okm'], $this->encryption->hkdf( + $test['okm'], $this->encrypter->hkdf( $test['ikm'], $test['digest'], $test['salt'], $test['length'], $test['info'] ) ); } // Test default length, it must match the digest size - $hkdf_result = $this->encryption->hkdf('foobar', 'sha512'); + $hkdf_result = $this->encrypter->hkdf('foobar', 'sha512'); $this->assertEquals( 64, defined('MB_OVERLOAD_STRING') ? mb_strlen($hkdf_result, '8bit') : strlen($hkdf_result) ); // Test maximum length (RFC5869 says that it must be up to 255 times the digest size) - $hkdf_result = $this->encryption->hkdf('foobar', 'sha384', null, 48 * 255); + $hkdf_result = $this->encrypter->hkdf('foobar', 'sha384', null, 48 * 255); $this->assertEquals( 12240, defined('MB_OVERLOAD_STRING') ? mb_strlen($hkdf_result, '8bit') : strlen($hkdf_result) ); - $this->assertFalse($this->encryption->hkdf('foobar', 'sha224', null, 28 * 255 + 1)); + $this->assertFalse($this->encrypter->hkdf('foobar', 'sha224', null, 28 * 255 + 1)); // CI-specific test for an invalid digest - $this->assertFalse($this->encryption->hkdf('fobar', 'sha1')); + $this->assertFalse($this->encrypter->hkdf('fobar', 'sha1')); } // -------------------------------------------------------------------- @@ -385,7 +385,7 @@ public function testPortability() public function testMagicGet() { $this->assertNull($this->encryption->foo); - $this->assertEquals(['mcrypt', 'openssl'], array_keys($this->encryption->handlers)); + $this->assertEquals(['openssl', 'mcrypt'], array_keys($this->encryption->handlers)); // 'stream' mode is translated into an empty string for OpenSSL $this->encryption->initialize(['cipher' => 'rc4', 'mode' => 'stream']); diff --git a/user_guide_src/source/libraries/encryption.rst b/user_guide_src/source/libraries/encryption.rst index e14312784ed2..34db3ff36b7c 100644 --- a/user_guide_src/source/libraries/encryption.rst +++ b/user_guide_src/source/libraries/encryption.rst @@ -51,7 +51,7 @@ software and programming languages' APIs. However, the *encryption key* is not used as is. Keyed-hash message authentication (HMAC) requires a secret key, - and using the same key for both +and using the same key for both encryption and authentication is a bad practice. Because of that, two separate keys are derived from your already configured @@ -59,7 +59,7 @@ Because of that, two separate keys are derived from your already configured done via a technique called `HMAC-based Key Derivation Function `_ (HKDF). -Setting your encryption_key +Setting your encryption key =========================== An *encryption key* is a piece of information that controls the @@ -83,8 +83,8 @@ your server is not totally under your control it's impossible to ensure key security so you may want to think carefully before using it for anything that requires high security, like storing credit card numbers. -Your encryption key **must** be as long as the encyption algorithm in use -allows. For AES-128, that's 128 bits or 16 bytes (charcters) long. +Your encryption key **must** be as long as the encryption algorithm in use +allows. For AES-128, that's 128 bits or 16 bytes (characters) long. You will find a table below that shows the supported key lengths of different ciphers. @@ -271,62 +271,89 @@ Cookies, for example, can only hold 4K of information. Configuring the library ======================= -For usability, performance, but also historical reasons tied to earlier -encryption in CodeIgniter, the Encryption library is designed to +The Encryption library is designed to use repeatedly the same driver, encryption cipher, mode and key. As noted in the "Default behavior" section above, this means using an auto-detected driver (OpenSSL has a higher priority), the AES-128 ciper in CBC mode, and your ``$key`` value. -You can pass an array of parameters to the Services... - -If you wish to change that however, you need to use the ``initialize()`` -method. It accepts an associative array of parameters, all of which are -optional: +Encryption configuration settings are normally set in +application/config/Encryption.php. ======== =============================================== Option Possible values ======== =============================================== -driver 'openssl', 'mcrypt' +driver Preferred handler: 'openssl', 'mcrypt' cipher Cipher name (see :ref:`ciphers-and-modes`) mode Encryption mode (see :ref:`encryption-modes`) key Encryption key ======== =============================================== +You can over-ride any of those settings by passing your own ``Config`` object, +or an associative array of parameters, to the Services:: + + $encrypter = \Config\Services::encrypter($params); + +These will replace any same-named settings in ``Config/Encryption``. + +Using the Encryption manager directly +===================================== + +Instead of, or in addition to, using the `Services` described +at the beginning of this page, you can use the encryption manager +directly, to create an `Encrypter`` or to change the settings +of the current one. + + $encryption = new \Encryption\Encryption(); + $encrypter = $encryption->initialize($params); + For example, if you were to change the encryption algorithm and mode to AES-256 in CTR mode, this is what you should do:: - $encrypter->initialize( - array( - 'cipher' => 'aes-256', - 'mode' => 'ctr', - 'key' => '' - ) - ); + $encryption = new \Encryption\Encryption(); + $encrypter = $encryption->initialize([ + 'cipher' => 'aes-256', + 'mode' => 'ctr', + 'key' => '' + ]); Note that we only mentioned that you want to change the cipher and mode, but we also included a key in the example. As previously noted, it is important that you choose a key with a proper size for the used algorithm. -There's also the ability to change the driver, if for some reason you -have both, but want to use MCrypt instead of OpenSSL:: +If you want to change the driver, for instance switching between +MCrypt and OpenSSL, you could go through the Services:: // Switch to the MCrypt driver - $encrypter->initialize(array('driver' => 'mcrypt')); + $encrypter= \Config\Services::encrypter(['driver' => 'mcrypt']);; + // encrypt data using MCrypt // Switch back to the OpenSSL driver - $encrypter->initialize(array('driver' => 'openssl')); + $encrypter= \Config\Services::encrypter(['driver' => 'openssl']);; + // now encrypt data using OpenSSL + +Alternately, you could use the encryption manager directly: + + $encryption = new \Encryption\Encryption(); + + // Switch to the MCrypt driver + $encrypter= $encryption->initialize(['driver' => 'mcrypt']);; + // encrypt data using MCrypt + + // Switch back to the OpenSSL driver + $encrypter= $encrypter= $encryption->initialize(['driver' => 'openssl']);; + // now encrypt data using OpenSSL Encrypting and decrypting data ============================== Encrypting and decrypting data with the already configured library -settings is simple. As simple as just passing the string to the +settings is simple - pass the appropriate string to the ``encrypt()`` and/or ``decrypt()`` methods:: $plain_text = 'This is a plain-text message!'; - $ciphertext = $encrypter->encrypt($plain_text); + $ciphertext = $encrypter->encrypt($plaintext); // Outputs: This is a plain-text message! echo $encrypter->decrypt($ciphertext); @@ -343,10 +370,9 @@ You don't need to worry about it. How it works ------------ -If you must know how the process works, here's what happens under -the hood: +Here's what happens under the hood: -- ``$encrypter->encrypt($plain_text)`` +- ``$encrypter->encrypt($plaintext)`` #. Derive an encryption key and a HMAC key from your configured *encryption_key* via HKDF, using the SHA-512 digest algorithm. @@ -389,12 +415,9 @@ the hood: Using custom parameters ----------------------- -Let's say you have to interact with another system that is out -of your control and uses another method to encrypt data. A -method that will most certainly not match the above-described -sequence and probably not use all of the steps either. - -The Encryption library allows you to change how its encryption +If you have to interact with another system that is out +of your control and uses another method to encrypt data, +you can change how the encryption and decryption processes work, so that you can easily tailor a custom solution for such situations. @@ -407,7 +430,6 @@ Here's an example:: // Assume that we have $ciphertext, $key and $hmac_key // from on outside source - $message = $encrypter->decrypt( $ciphertext, array( @@ -427,11 +449,10 @@ HMAC. example. When using custom parameters, encryption and HMAC keys are not derived like the default behavior of the library is. -Below is a list of the available options. - -However, unless you really need to and you know what you are doing, +Below is a list of the available options for ``encrypt()`` and ``decrypt``. +Unless you really need to do this, and you know what you are doing, we advise you to not change the encryption process as this could -impact security, so please do so with caution. +impact security. ============= =============== ============================= ====================================================== Option Default value Mandatory / Optional Description @@ -442,9 +463,9 @@ key N/A Yes Encryption key. hmac TRUE No Whether to use a HMAC. Boolean. If set to FALSE, then *hmac_digest* and *hmac_key* will be ignored. -hmac_digest sha512 No HMAC message digest algorithm (see :ref:`digests`). -hmac_key N/A Yes, unless *hmac* is FALSE HMAC key. -raw_data FALSE No Whether the cipher-text should be raw. +hmacDigest sha512 No HMAC message digest algorithm (see :ref:`digests`). +hmacKey N/A Yes, unless *hmac* is FALSE HMAC key. +rawdata FALSE No Whether the ciphertext should be raw. Boolean. If set to TRUE, then Base64 encoding and decoding will not be performed and HMAC will not be a hexadecimal string. @@ -452,7 +473,7 @@ raw_data FALSE No Whether the cipher-t .. important:: ``encrypt()`` and ``decrypt()`` will return FALSE if a mandatory parameter is not provided or if a provided - value is incorrect. This includes *hmac_key*, unless *hmac* + value is incorrect. This includes *hmacKey*, unless *hmac* is set to FALSE. .. _digests: @@ -487,23 +508,33 @@ Class Reference .. php:class:: CodeIgniter\\Encryption\\Encryption + .. php:staticmethod:: createKey($length) + + :param int $length: Output length + :returns: A pseudo-random cryptographic key with the specified length, or FALSE on failure + :rtype: string + + Creates a cryptographic key by fetching random data from + the operating system's sources (i.e. /dev/urandom). + + .. php:method:: initialize($params) :param array $params: Configuration parameters - :returns: CodeIgniter\\Encryption\\Encryption instance (method chaining) - :rtype: CodeIgniter\\Encryption\\Encryption + :returns: CodeIgniter\\Encryption\\EncrypterInterface instance (for method chaining) + :rtype: CodeIgniter\\Encryption\\EncrypterInterface Initializes (configures) the library to use a different driver, cipher, mode or key. Example:: - $encrypter->initialize( - array('mode' => 'ctr') - ); + $encrypter = $encryption->initialize(['mode' => 'ctr']); Please refer to the :ref:`configuration` section for detailed info. +.. php:interface:: CodeIgniter\\Encryption\\EncrypterInterface + .. php:method:: encrypt($data[, $params = NULL]) :param string $data: Data to encrypt @@ -536,15 +567,6 @@ Class Reference Please refer to the :ref:`custom-parameters` secrion for information on the optional parameters. - .. php:method:: createKey($length) - - :param int $length: Output length - :returns: A pseudo-random cryptographic key with the specified length, or FALSE on failure - :rtype: string - - Creates a cryptographic key by fetching random data from - the operating system's sources (i.e. /dev/urandom). - .. php:method:: hkdf($key[, $digest = 'sha512'[, $salt = NULL[, $length = NULL[, $info = '']]]]) :param string $key: Input key material @@ -568,7 +590,7 @@ Class Reference Example:: - $hmac_key = $encrypter->hkdf( + $hmacKey = $encrypter->hkdf( $key, 'sha512', NULL, @@ -576,4 +598,4 @@ Class Reference 'authentication' ); - // $hmac_key is a pseudo-random key with a length of 64 bytes \ No newline at end of file + // $hmacKey is a pseudo-random key with a length of 64 bytes \ No newline at end of file From d6db60e2bf02d8bf1d5d4a7690be2438e8e0b93d Mon Sep 17 00:00:00 2001 From: Master Yoda Date: Sun, 28 May 2017 21:50:50 -0700 Subject: [PATCH 11/18] Reshaping as service, with OpenSSL to start --- system/Config/Services.php | 12 +- system/Encryption/EncrypterInterface.php | 11 - system/Encryption/Encryption.php | 43 ++- system/Encryption/Handlers/BaseHandler.php | 116 +------ system/Encryption/Handlers/McryptHandler.php | 288 ------------------ system/Encryption/Handlers/OpenSSLHandler.php | 35 +-- tests/_support/Encryption/MockEncryption.php | 40 --- tests/system/Encryption/EncryptionTest.php | 124 ++------ .../source/libraries/encryption.rst | 97 +++--- 9 files changed, 118 insertions(+), 648 deletions(-) delete mode 100644 system/Encryption/Handlers/McryptHandler.php delete mode 100644 tests/_support/Encryption/MockEncryption.php diff --git a/system/Config/Services.php b/system/Config/Services.php index 93779df65a80..b9504c7160b0 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -162,20 +162,16 @@ public static function curlrequest(array $options = [], $response = null, \Confi * * @return \CodeIgniter\Encryption\EncrypterInterfrace Encryption handler */ - public static function encrypter(\Config\Encryption $config = null, array $params = [], $getShared = false) + public static function encrypter($config = null, $getShared = false) { if ($getShared === true) - { return self::getSharedInstance('encrypter', $config); - } - if ( ! is_object($config)) - { - $config = new \Config\Encryption(); - } + if ($config != null && is_object($config)) + $config = (array) $config; $encryption = new \CodeIgniter\Encryption\Encryption($config); - $encrypter = $encryption->initialize($params); + $encrypter = $encryption->initialize($config); return $encrypter; } diff --git a/system/Encryption/EncrypterInterface.php b/system/Encryption/EncrypterInterface.php index d5a5824dc282..dbbdf3a2d045 100644 --- a/system/Encryption/EncrypterInterface.php +++ b/system/Encryption/EncrypterInterface.php @@ -44,17 +44,6 @@ interface EncrypterInterface { - /** - * Initialize or re-initialize an encryption handler, - * possibly with different parameters - * - * @param array $params Configuration parameters - * @return CI_Encryption - * - * @throws \CodeIgniter\Encryption\EncryptionException - */ - public function initialize(array $params); - /** * Encrypt - convert plaintext into ciphertext * diff --git a/system/Encryption/Encryption.php b/system/Encryption/Encryption.php index e6f78c2453c9..9b9814bf5f86 100644 --- a/system/Encryption/Encryption.php +++ b/system/Encryption/Encryption.php @@ -91,7 +91,6 @@ class Encryption */ protected $drivers = [ 'openssl' => 'OpenSSL', - 'mcrypt' => 'Mcrypt', ]; /** @@ -100,6 +99,27 @@ class Encryption */ protected $logger; + /** + * Encryption cipher + * + * @var string + */ + protected $cipher = 'aes-128'; + + /** + * Cipher mode + * + * @var string + */ + protected $mode = 'cbc'; + + /** + * Encryption key + * + * @var string + */ + protected $key; + // -------------------------------------------------------------------- /** @@ -131,10 +151,9 @@ public function __construct($params = []) // determine what is installed $this->handlers = [ 'openssl' => extension_loaded('openssl'), - 'mcrypt' => defined('MCRYPT_DEV_URANDOM'), ]; - if ( ! $this->handlers['mcrypt'] && ! $this->handlers['openssl']) + if ( ! $this->handlers['openssl']) throw new EncryptionException('Unable to find an available encryption handler.'); $this->logger->info('Encryption class Initialized'); @@ -148,7 +167,7 @@ public function __construct($params = []) * * @throws \CodeIgniter\Encryption\EncryptionException */ - public function initialize(array $params) + public function initialize(array $params = null) { // how should this be handled? $this->driver = $params['driver'] ?? 'openssl'; @@ -157,7 +176,7 @@ public function initialize(array $params) // use config key if initialization didn't create one if ( ! isset($this->key) && self::strlen($key = $this->config->key) > 0) $this->key = $key; - + if ( ! empty($params['handler'])) { if (isset($this->handlers[$params['handler']])) @@ -177,19 +196,11 @@ public function initialize(array $params) } } - if (empty($this->handler)) - { - $this->handler = ($this->handlers['openssl'] === true) ? 'openssl' : 'mcrypt'; - - $this->logger->debug("Encryption: Auto-configured handler '" . $this->handler . "'."); - } - empty($params['cipher']) && $params['cipher'] = $this->cipher; empty($params['key']) OR $this->key = $params['key']; $handlerName = 'CodeIgniter\\Encryption\\Handlers\\' . $this->handler . 'Handler'; - $this->encrypter = new $handlerName(); - $this->encrypter->initialize($params); + $this->encrypter = new $handlerName($params); return $this->encrypter; } @@ -220,7 +231,7 @@ public static function createKey($length) // -------------------------------------------------------------------- /** - * __get() magic, providing readonly access to some of our properties + * __get() magic, providing readonly access to some of our protected properties * * @param string $key Property name * @return mixed @@ -232,7 +243,7 @@ public function __get($key) { return array_search($this->mode, $this->modes[$this->handler], true); } - elseif (in_array($key, ['cipher', 'handler', 'handlers', 'digests'], true)) + elseif (in_array($key, ['cipher', 'key', 'handler', 'handlers', 'digests'], true)) { return $this->{$key}; } diff --git a/system/Encryption/Handlers/BaseHandler.php b/system/Encryption/Handlers/BaseHandler.php index d17bbd4a368b..1e355c663b77 100644 --- a/system/Encryption/Handlers/BaseHandler.php +++ b/system/Encryption/Handlers/BaseHandler.php @@ -117,121 +117,21 @@ public function __construct($config = null) $params = (array) $this->config; - $this->handlers = [ - 'openssl' => extension_loaded('openssl'), - 'mcrypt' => defined('MCRYPT_DEV_URANDOM'), - ]; - - if ( ! $this->handlers['mcrypt'] && ! $this->handlers['openssl']) - { - throw new EncryptionException('Unable to find an available encryption handler.'); - } - - $this->initialize($params); - if ( ! isset($this->key) && self::strlen($key = $this->config->key) > 0) - { $this->key = $key; - } $this->logger->info('Encryption handler Initialized'); } - /** - * Encrypt - * - * @param string $data Input data - * @param array $params Input parameters - * @return string - */ - public function encrypt($data, array $params = null) - { - if (($params = $this->getParams($params)) === false) - { - return false; - } - - isset($params['key']) OR $params['key'] = $this->hkdf($this->key, 'sha512', null, self::strlen($this->key), 'encryption'); - - if (($data = $this->encryptIt($data, $params)) === false) - { - return false; - } - - $params['base64'] && $data = base64_encode($data); - - if (isset($params['hmac_digest'])) - { - isset($params['hmackey']) OR $params['hmackey'] = $this->hkdf($this->key, 'sha512', null, null, 'authentication'); - return hash_hmac($params['hmac_digest'], $data, $params['hmackey'], ! $params['base64']) . $data; - } - - return $data; - } - - /** - * Decrypt - * - * @param string $data Encrypted data - * @param array $params Input parameters - * @return string - */ - public function decrypt($data, array $params = null) - { - if (($params = $this->getParams($params)) === false) - { - return false; - } - - if (isset($params['hmac_digest'])) - { - // This might look illogical, but it is done during encryption as well ... - // The 'base64' value is effectively an inverted "raw data" parameter - $digest_size = ($params['base64']) ? $this->digests[$params['hmac_digest']] * 2 : $this->digests[$params['hmac_digest']]; - - if (self::strlen($data) <= $digest_size) - { - return false; - } - - $hmac_input = self::substr($data, 0, $digest_size); - $data = self::substr($data, $digest_size); - - isset($params['hmackey']) OR $params['hmackey'] = $this->hkdf($this->key, 'sha512', null, null, 'authentication'); - $hmac_check = hash_hmac($params['hmac_digest'], $data, $params['hmackey'], ! $params['base64']); - - // Time-attack-safe comparison - $diff = 0; - for ($i = 0; $i < $digest_size; $i ++ ) - { - $diff |= ord($hmac_input[$i]) ^ ord($hmac_check[$i]); - } - - if ($diff !== 0) - { - return false; - } - } - - if ($params['base64']) - { - $data = base64_decode($data); - } - - isset($params['key']) OR $params['key'] = $this->hkdf($this->key, 'sha512', null, self::strlen($this->key), 'encryption'); - - return $this->decryptIt($data, $params); - } - // -------------------------------------------------------------------- /** - * Get params + * Get params. Is there harm to exposing this? * * @param array $params Input parameters * @return array */ - protected function getParams($params) + public function getParams($params) { if (empty($params)) { @@ -303,18 +203,6 @@ protected function getParams($params) return $params; } -// -------------------------------------------------------------------- - - /** - * Get handler's handle - * - * @param string $cipher Cipher name - * @param string $mode Encryption mode - * @return string - */ - abstract protected function getHandle($cipher, $mode); - - // -------------------------------------------------------------------- /** diff --git a/system/Encryption/Handlers/McryptHandler.php b/system/Encryption/Handlers/McryptHandler.php deleted file mode 100644 index 3200b3258517..000000000000 --- a/system/Encryption/Handlers/McryptHandler.php +++ /dev/null @@ -1,288 +0,0 @@ - 'cbc', - 'ecb' => 'ecb', - 'ofb' => 'nofb', - 'ofb8' => 'ofb', - 'cfb' => 'ncfb', - 'cfb8' => 'cfb', - 'ctr' => 'ctr', - 'stream' => 'stream' - ]; - - // -------------------------------------------------------------------- - - /** - * Initialize MCrypt - * - * @param array $params Configuration parameters - * @return void - * - * @throws \CodeIgniter\Encryption\EncryptionException - */ - protected function initialize($params) - { - if ( ! empty($params['cipher'])) - { - $params['cipher'] = strtolower($params['cipher']); - $this->cipherAlias($params['cipher']); - - if ( ! in_array($params['cipher'], mcrypt_list_algorithms(), true)) - { - throw new EncryptionException('MCrypt cipher ' . strtoupper($params['cipher']) . ' is not available.'); - } - else - { - $this->cipher = $params['cipher']; - } - } - - if ( ! empty($params['mode'])) - { - $params['mode'] = strtolower($params['mode']); - if ( ! isset($this->modes[$params['mode']])) - { - throw new EncryptionException('MCrypt mode ' . strtoupper($params['cipher']) . ' is not available.'); - } - else - { - $this->mode = $this->modes[$params['mode']]; - } - } - - if (isset($this->cipher, $this->mode)) - { - if (is_resource($this->handle) && (strtolower(mcrypt_enc_get_algorithms_name($this->handle)) !== $this->cipher - OR strtolower(mcrypt_enc_getmodes_name($this->handle)) !== $this->mode) - ) - { - mcrypt_module_close($this->handle); - } - - if ($this->handle = mcrypt_module_open($this->cipher, '', $this->mode, '')) - { - $this->logger->info('Encryption: MCrypt cipher ' . strtoupper($this->cipher) . ' initialized in ' . strtoupper($this->mode) . ' mode.'); - } - else - { - throw new EncryptionException('Unable to initialize MCrypt with cipher ' . strtoupper($this->cipher) . ' in ' . strtoupper($this->mode) . ' mode.'); - } - } - } - - /** - * Encrypt - * - * @param string $data Input data - * @param array $params Input parameters - * @return string - */ - public function encryptIt($data, array $params = null) - { - - if ( ! is_resource($params['handle'])) - { - return false; - } - - // The greater-than-1 comparison is mostly a work-around for a bug, - // where 1 is returned for ARCFour instead of 0. - $iv = (($iv_size = mcrypt_enc_get_iv_size($params['handle'])) > 1) ? $this->createKey($iv_size) : null; - - if (mcrypt_generic_init($params['handle'], $params['key'], $iv) < 0) - { - if ($params['handle'] !== $this->handle) - { - mcrypt_module_close($params['handle']); - } - - return false; - } - - // Use PKCS#7 padding in order to ensure compatibility with OpenSSL - // and other implementations outside of PHP. - if (in_array(strtolower(mcrypt_enc_getmodes_name($params['handle'])), ['cbc', 'ecb'], true)) - { - $block_size = mcrypt_enc_get_block_size($params['handle']); - $pad = $block_size - (self::strlen($data) % $block_size); - $data .= str_repeat(chr($pad), $pad); - } - - // Work-around for yet another strange behavior in MCrypt. - // - // When encrypting in ECB mode, the IV is ignored. Yet - // mcrypt_enc_get_iv_size() returns a value larger than 0 - // even if ECB is used AND mcrypt_generic_init() complains - // if you don't pass an IV with length equal to the said - // return value. - // - // This probably would've been fine (even though still wasteful), - // but OpenSSL isn't that dumb and we need to make the process - // portable, so ... - $data = (mcrypt_enc_getmodes_name($params['handle']) !== 'ECB') ? $iv . mcrypt_generic($params['handle'], $data) : mcrypt_generic($params['handle'], $data); - - mcrypt_generic_deinit($params['handle']); - if ($params['handle'] !== $this->handle) - { - mcrypt_module_close($params['handle']); - } - - return $data; - } - - // -------------------------------------------------------------------- - - /** - * Decrypt - * - * @param string $data Encrypted data - * @param array $params Input parameters - * @return string - */ - public function decryptIt($data, array $params = null) - { - - if ( ! is_resource($params['handle'])) - { - return false; - } - - // The greater-than-1 comparison is mostly a work-around for a bug, - // where 1 is returned for ARCFour instead of 0. - if (($iv_size = mcrypt_enc_get_iv_size($params['handle'])) > 1) - { - if (mcrypt_enc_getmodes_name($params['handle']) !== 'ECB') - { - $iv = self::substr($data, 0, $iv_size); - $data = self::substr($data, $iv_size); - } - else - { - // MCrypt is dumb and this is ignored, only size matters - $iv = str_repeat("\x0", $iv_size); - } - } - else - { - $iv = null; - } - - if (mcrypt_generic_init($params['handle'], $params['key'], $iv) < 0) - { - if ($params['handle'] !== $this->handle) - { - mcrypt_module_close($params['handle']); - } - - return false; - } - - $data = mdecrypt_generic($params['handle'], $data); - // Remove PKCS#7 padding, if necessary - if (in_array(strtolower(mcrypt_enc_getmodes_name($params['handle'])), ['cbc', 'ecb'], true)) - { - $data = self::substr($data, 0, -ord($data[self::strlen($data) - 1])); - } - - mcrypt_generic_deinit($params['handle']); - if ($params['handle'] !== $this->handle) - { - mcrypt_module_close($params['handle']); - } - - return $data; - } - - // -------------------------------------------------------------------- - - /** - * Get MCrypt handle - * - * @param string $cipher Cipher name - * @param string $mode Encryption mode - * @return resource - */ - protected function getHandle($cipher, $mode) - { - return mcrypt_module_open($cipher, '', $mode, ''); - } - - // -------------------------------------------------------------------- - - /** - * Cipher alias - * - * Tries to translate cipher names as appropriate for this handler - * - * @param string $cipher Cipher name - * @return void - */ - protected function cipherAlias(&$cipher) - { - static $dictionary; - - if (empty($dictionary)) - { - $dictionary = [ - 'aes-128' => 'rijndael-128', - 'aes-192' => 'rijndael-128', - 'aes-256' => 'rijndael-128', - 'des3-ede3' => 'tripledes', - 'bf' => 'blowfish', - 'cast5' => 'cast-128', - 'rc4' => 'arcfour', - 'rc4-40' => 'arcfour' - ]; - } - - if (isset($dictionary[$cipher])) - { - $cipher = $dictionary[$cipher]; - } - } - -} diff --git a/system/Encryption/Handlers/OpenSSLHandler.php b/system/Encryption/Handlers/OpenSSLHandler.php index b335f031f8a5..e04ece5710b6 100644 --- a/system/Encryption/Handlers/OpenSSLHandler.php +++ b/system/Encryption/Handlers/OpenSSLHandler.php @@ -62,8 +62,10 @@ class OpenSSLHandler extends BaseHandler * @param array $params Configuration parameters * @return void */ - protected function initialize($params) + public function __construct($params = null) { + parent::__construct(); + if ( ! empty($params['cipher'])) { $params['cipher'] = strtolower($params['cipher']); @@ -109,17 +111,17 @@ protected function initialize($params) * @param array $params Input parameters * @return string */ - public function encryptIt($data, array $params = null) + public function encrypt($data, array $params = null) { - if (empty($params['handle'])) + if (empty($params['cipher'])) { return false; } - $iv = ($iv_size = opensslcipher_iv_length($params['handle'])) ? $this->createKey($iv_size) : null; + $iv = ($iv_size = opensslcipher_iv_length($params['cipher'])) ? $this->createKey($iv_size) : null; $data = openssl_encrypt( - $data, $params['handle'], $params['key'], OPENSSL_RAW_DATA, $iv + $data, $params['cipher'], $params['key'], OPENSSL_RAW_DATA, $iv ); if ($data === false) @@ -139,10 +141,10 @@ public function encryptIt($data, array $params = null) * @param array $params Input parameters * @return string */ - public function decryptIt($data, array $params = null) + public function decrypt($data, array $params = null) { - if ($iv_size = opensslcipher_iv_length($params['handle'])) + if ($iv_size = openssl_cipher_iv_length($params['cipher'])) { $iv = self::substr($data, 0, $iv_size); $data = self::substr($data, $iv_size); @@ -152,28 +154,13 @@ public function decryptIt($data, array $params = null) $iv = null; } - return empty($params['handle']) ? false : openssl_decrypt( - $data, $params['handle'], $params['key'], OPENSSL_RAW_DATA, $iv + return empty($params['cipher']) ? false : openssl_decrypt( + $data, $params['cipher'], $params['key'], OPENSSL_RAW_DATA, $iv ); } // -------------------------------------------------------------------- - /** - * Get OpenSSL handle - * - * @param string $cipher Cipher name - * @param string $mode Encryption mode - * @return string - */ - protected function getHandle($cipher, $mode) - { - // OpenSSL methods aren't suffixed with '-stream' for this mode - return ($mode === 'stream') ? $cipher : $cipher . '-' . $mode; - } - - // -------------------------------------------------------------------- - /** * Cipher alias * diff --git a/tests/_support/Encryption/MockEncryption.php b/tests/_support/Encryption/MockEncryption.php deleted file mode 100644 index b94417abb8d6..000000000000 --- a/tests/_support/Encryption/MockEncryption.php +++ /dev/null @@ -1,40 +0,0 @@ -getParams($params); - } - - // -------------------------------------------------------------------- - - /** - * get_key() - * - * Allows checking for key changes. - */ - public function getKey() - { - return $this->_key; - } - - // -------------------------------------------------------------------- - - /** - * __driver_get_handle() - * - * Allows checking for _openssl_get_handle() - */ - public function handlerGetHandle($driver, $cipher, $mode) - { - return $this->{$driver . 'GetHandle'}($cipher, $mode); - } - -} diff --git a/tests/system/Encryption/EncryptionTest.php b/tests/system/Encryption/EncryptionTest.php index 277a69118850..faa493743ba7 100644 --- a/tests/system/Encryption/EncryptionTest.php +++ b/tests/system/Encryption/EncryptionTest.php @@ -8,7 +8,7 @@ class EncryptionTest extends CIUnitTestCase public function setUp() { - $this->encryption = new MockEncryption(); + $this->encryption = new \CodeIgniter\Encryption\Encryption(); } // -------------------------------------------------------------------- @@ -21,16 +21,16 @@ public function setUp() public function testConstructor() { // Assume no configuration from set_up() - $this->assertNull($this->encryption->getKey()); + $this->assertNull($this->encryption->key); // Try with an empty value $config = new \Config\Encryption(); - $this->encrypt = new MockEncryption($config); - $this->assertNull($this->encrypt->getKey()); + $this->encrypt = new \CodeIgniter\Encryption\Encryption($config); + $this->assertNull($this->encrypt->key); $config->key = str_repeat("\x0", 16); - $this->encrypt = new MockEncryption($config); - $this->assertEquals(str_repeat("\x0", 16), $this->encrypt->getKey()); + $this->encrypt = new \CodeIgniter\Encryption\Encryption($config); + $this->assertEquals(str_repeat("\x0", 16), $this->encrypt->key); } // -------------------------------------------------------------------- @@ -133,9 +133,11 @@ public function testGetParams() ['cipher' => 'aes-128', 'mode' => 'foo', 'key' => $key, 'hmac_digest' => 'sha256', 'hmac_key' => $key] ]; + $this->encrypter = \Config\Services::encrypter($params); + for ($i = 0, $c = count($params); $i < $c; $i ++ ) { - $this->assertFalse($this->encryption->__getParams($params[$i])); + $this->assertFalse($this->encrypter->getParams($params[$i])); } // Valid parameters @@ -146,7 +148,7 @@ public function testGetParams() 'hmac_key' => str_repeat("\x0", 16) ]; - $this->assertTrue(is_array($this->encryption->__getParams($params))); + $this->assertTrue(is_array($this->encrypter->getParams($params))); $params['base64'] = TRUE; $params['hmac_digest'] = 'sha512'; @@ -161,7 +163,7 @@ public function testGetParams() 'hmac_digest' => 'sha256' ]; - $output = $this->encryption->__getParams($params); + $output = $this->encrypter->getParams($params); unset($output['handle'], $output['cipher'], $params['raw_data'], $params['cipher']); $params['base64'] = FALSE; $this->assertEquals($params, $output); @@ -170,7 +172,7 @@ public function testGetParams() unset($params['hmac_key'], $params['hmac_digest']); $params['hmac'] = $params['raw_data'] = FALSE; $params['cipher'] = 'aes-128'; - $output = $this->encryption->__getParams($params); + $output = $this->encrypter->getParams($params); unset($output['handle'], $output['cipher'], $params['hmac'], $params['raw_data'], $params['cipher']); $params['base64'] = TRUE; $params['hmac_digest'] = $params['hmac_key'] = null; @@ -195,16 +197,16 @@ public function testInitializeEncryptDecrypt() $key = "\xd0\xc9\x08\xc4\xde\x52\x12\x6e\xf8\xcc\xdb\x03\xea\xa0\x3a\x5c"; // Default state (AES-128/Rijndael-128 in CBC mode) - $this->encryption->initialize(array('key' => $key)); + $encrypter = $this->encryption->initialize(array('key' => $key)); // Was the key properly set? - $this->assertEquals($key, $this->encryption->getKey()); + $this->assertEquals($key, $encrypter->key); - $this->assertEquals($message, $this->encryption->decrypt($this->encryption->encrypt($message))); + $this->assertEquals($message, $encrypter->decrypt($encrypter->encrypt($message))); // Try DES in ECB mode, just for the sake of changing stuff - $this->encryption->initialize(array('cipher' => 'des', 'mode' => 'ecb', 'key' => substr($key, 0, 8))); - $this->assertEquals($message, $this->encryption->decrypt($this->encryption->encrypt($message))); + $encrypter = $this->encryption->initialize(array('cipher' => 'des', 'mode' => 'ecb', 'key' => substr($key, 0, 8))); + $this->assertEquals($message, $encrypter->decrypt($encrypter->encrypt($message))); } // -------------------------------------------------------------------- @@ -218,9 +220,11 @@ public function testEncryptDecryptCustom() { $message = 'Another plain-text message.'; + $encrypter = $this->encryption->initialize(); + // A random invalid parameter - $this->assertFalse($this->encryption->encrypt($message, array('foo'))); - $this->assertFalse($this->encryption->decrypt($message, array('foo'))); + $this->assertFalse($encrypter->encrypt($message, array('foo'))); + $this->assertFalse($encrypter->decrypt($message, array('foo'))); // No HMAC, binary output $params = [ @@ -231,65 +235,18 @@ public function testEncryptDecryptCustom() 'hmac' => FALSE ]; - $ciphertext = $this->encryption->encrypt($message, $params); - - $this->assertEquals($message, $this->encryption->decrypt($ciphertext, $params)); - } - - // -------------------------------------------------------------------- - - /** - * _mcrypt_get_handle() test - */ - public function testMcryptGetHandle() - { - if ($this->encryption->handlers['mcrypt'] === FALSE) - { - return $this->markTestSkipped('Cannot test MCrypt because it is not available.'); - } - elseif (version_compare(PHP_VERSION, '7.1.0-alpha', '>=')) - { - return $this->markTestSkipped('ext/mcrypt is deprecated since PHP 7.1 and will generate notices here.'); - } - - $this->assertTrue(is_resource($this->encryption->handlerGetHandle('mcrypt', 'rijndael-128', 'cbc'))); - } - - // -------------------------------------------------------------------- - - /** - * _openssl_get_handle() test - */ - public function testOpensslMcryptGetHandle() - { - if ($this->encryption->handlers['openssl'] === FALSE) - { - return $this->markTestSkipped('Cannot test OpenSSL because it is not available.'); - } + $ciphertext = $encrypter->encrypt($message, $params); - $this->assertEquals('aes-128-cbc', $this->encryption->handlerGetHandle('openssl', 'aes-128', 'cbc')); - $this->assertEquals('rc4-40', $this->encryption->handlerGetHandle('openssl', 'rc4-40', 'stream')); + $this->assertEquals($message, $encrypter->decrypt($ciphertext, $params)); } // -------------------------------------------------------------------- /** - * OpenSSL/MCrypt portability test - * - * Amongst the obvious stuff, _cipher_alias() is also tested here. + * Make sure all our supported drivers handle all the portable cipher/mode combinations */ public function testPortability() { - if ( ! $this->encryption->handlers['mcrypt'] OR ! $this->encryption->handlers['openssl']) - { - $this->markTestSkipped('Both MCrypt and OpenSSL support are required for portability tests.'); - return; - } - elseif (version_compare(PHP_VERSION, '7.1.0-alpha', '>=')) - { - return $this->markTestSkipped('ext/mcrypt is deprecated since PHP 7.1 and will generate notices here.'); - } - $message = 'This is a message encrypted via MCrypt and decrypted via OpenSSL, or vice-versa.'; // Format is: , , @@ -353,43 +310,28 @@ public function testPortability() ['rc4', 'stream', 128], ['rc4', 'stream', 256] ]; - $handler_index = ['mcrypt', 'openssl']; + $handler_index = ['openssl']; foreach ($portable as &$test) { // Add some randomness to the selected handler - $handler = mt_rand(0, 1); + $handler = mt_rand(0, sizeof($handler_index) - 1); $params = [ - 'handler' => $handler_index[$handler], + 'driver' => $handler_index[$handler], 'cipher' => $test[0], 'mode' => $test[1], 'key' => openssl_random_pseudo_bytes($test[2]) ]; - $this->encryption->initialize($params); - $ciphertext = $this->encryption->encrypt($message); + $encrypter = $this->encryption->initialize($params); + $ciphertext = $encrypter->encrypt($message); - $handler = (int) ! $handler; - $params['handler'] = $handler_index[$handler]; + $handler = mt_rand(0, sizeof($handler_index) - 1); + $params['driver'] = $handler_index[$handler]; - $this->encryption->initialize($params); - $this->assertEquals($message, $this->encryption->decrypt($ciphertext)); + $encrypter = $this->encryption->initialize($params); + $this->assertEquals($message, $encrypter->decrypt($ciphertext)); } } - // -------------------------------------------------------------------- - - /** - * __get() test - */ - public function testMagicGet() - { - $this->assertNull($this->encryption->foo); - $this->assertEquals(['openssl', 'mcrypt'], array_keys($this->encryption->handlers)); - - // 'stream' mode is translated into an empty string for OpenSSL - $this->encryption->initialize(['cipher' => 'rc4', 'mode' => 'stream']); - $this->assertEquals('stream', $this->encryption->mode); - } - } diff --git a/user_guide_src/source/libraries/encryption.rst b/user_guide_src/source/libraries/encryption.rst index 34db3ff36b7c..ebad27bfef74 100644 --- a/user_guide_src/source/libraries/encryption.rst +++ b/user_guide_src/source/libraries/encryption.rst @@ -1,5 +1,5 @@ ################## -Encryption Library +Encryption Service ################## .. important:: DO NOT use this or any other *encryption* library for @@ -7,21 +7,24 @@ Encryption Library should do that via PHP's own `Password Hashing extension `_. -The Encryption Library provides two-way data encryption. To do so in -a cryptographically secure way, it utilizes PHP extensions that are -unfortunately not always available on all systems. -You must meet one of the following dependencies in order to use this -library: +The Encryption Service provides two-way data encryption. +The encryption manager will instantiate and/or initialize an +encryption handler to suit your parameters, explained below. -- `OpenSSL `_ -- `MCrypt `_ (and `MCRYPT_DEV_URANDOM` availability) +The handlers adapt our simple ``EncrypterInterface`` to use an +appropriate PHP cryptographic extension or third party library. +Such extensions may need to be explicitly enabled in your instance of PHP. + +The following extensions are currentlty supported: -If neither of the above dependencies is met, we simply cannot offer -you a good enough implementation to meet the high standards required -for proper cryptography. +- `OpenSSL `_ -.. important:: Mcrypt is being deprecated, as of PHP7.2, and we don't recommend it. +We plan to add a couple more: +`GnuPG `_ and +`libsodium `_. +.. note:: Support for the ``MCrypt`` extension has been dropped, as that has + been deprecated as of PHP 7.2. .. contents:: :local: @@ -43,25 +46,27 @@ Default behavior By default, the Encryption Library will use the OpenSSL handler, with the AES-128 cipher in CBC mode, -using your configured *encryption key* and SHA512 HMAC authentication. +using your configured *key* and SHA512 HMAC authentication. AES-128 is chosen both because it is proven to be strong and because of its wide availability across different cryptographic software and programming languages' APIs. -However, the *encryption key* is not used as is. -Keyed-hash message authentication (HMAC) requires a secret key, -and using the same key for both -encryption and authentication is a bad practice. - -Because of that, two separate keys are derived from your already configured -*encryption key*: one for encryption and one for authentication. This is +The *key* you provide is used for +"keyed-hash message authentication" (HMAC), which derives +two separate keys from your configured one: +one for encryption and one for authentication. This is done via a technique called `HMAC-based Key Derivation Function `_ (HKDF). Setting your encryption key =========================== + +`symmetric encryption `_ +`asymmetric encryption `_ + + An *encryption key* is a piece of information that controls the cryptographic process and permits a plain-text string to be encrypted, and afterwards - decrypted. It is the secret "ingredient" in the whole @@ -128,15 +133,15 @@ Portable ciphers ---------------- Different encryption drivers support different sets of encryption algorithms and often implement -them in different ways. Our Encryption library is designed to use them in -a portable fashion - interchangeably, for the ciphers supported by both drivers. +them in different ways. Our Encryption service is designed to use them in +a portable fashion - interchangeably, for the ciphers supported by all drivers. It is also implemented in a way that aims to match the standard implementations in other programming languages and libraries. Here's a list of the so called "portable" ciphers, where "CodeIgniter name" is the string value that you'd have to pass to the -Encryption library to use that cipher: +Encryption manager to use that cipher: ======================== ================== ============================ =============================== Cipher name CodeIgniter name Key lengths (bits / bytes) Supported modes @@ -151,10 +156,6 @@ RC4 / ARCFour rc4 40-2048 / 5-256 Stream TripleDES tripledes 56 / 7, 112 / 14, 168 / 21 CBC, CFB, CFB8, OFB ======================== ================== ============================ =============================== -.. important:: Because of how MCrypt works, if you fail to provide a key - with the appropriate length, you might end up using a different - algorithm than the one configured, so be really careful with that! - .. note:: In case it isn't clear from the above table, Blowfish, CAST5 and RC4 support variable length keys. That is, any number in the shown ranges is valid, although in bit terms that only happens @@ -190,18 +191,6 @@ Camellia-192 OpenSSL 192 / 24 CBC, CFB, CFB8, OFB, ECB Camellia-256 OpenSSL 256 / 32 CBC, CFB, CFB8, OFB, ECB RC2 OpenSSL 8-1024 / 1-128 CBC, CFB, OFB, ECB Seed OpenSSL 128 / 16 CBC, CFB, OFB, ECB -CAST-128 MCrypt 40-128 / 5-16 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB -CAST-256 MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB -GOST MCrypt 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB -Loki97 MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB -RC2 MCrypt 8-1024 / 1-128 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB -Rijndael-128 MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB -Rijndael-192 MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB -Rijndael-256 MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB -SaferPlus MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB -Serpent MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB -Twofish MCrypt 128 / 16, 192 / 24, 256 / 32 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB -XTEA MCrypt 128 / 16 CBC, CTR, CFB, CFB8, OFB, OFB8, ECB ============== ========= ============================== ========================================= .. note:: If you wish to use one of those ciphers, you'd have to pass @@ -218,11 +207,8 @@ XTEA MCrypt 128 / 16 CBC, CTR, CFB, CFB8, OFB implementation doesn't appear to be working correctly with key sizes of 80 bits and lower. - RC2 is listed as supported by both MCrypt and OpenSSL. - However, both drivers implement them differently and they - are not portable. It is probably worth noting that we only - found one obscure source confirming that it is MCrypt that - is not properly implementing it. + RC2 is supported by multiple drivers, but the implementation may differ, + so it is not portable. .. _encryption-modes: @@ -241,15 +227,14 @@ general purposes. =========== ================== ================= =================================================================================================================================================== Mode name CodeIgniter name Driver support Additional info =========== ================== ================= =================================================================================================================================================== -CBC cbc OpenSSL, MCrypt A safe default choice -CFB cfb OpenSSL, MCrypt N/A -CFB8 cfb8 OpenSSL, MCrypt Same as CFB, but operates in 8-bit mode (not recommended). -CTR ctr OpenSSL, MCrypt Considered as theoretically better than CBC, but not as widely available -ECB ecb OpenSSL, MCrypt Ignores IV (not recommended). -OFB ofb OpenSSL, MCrypt N/A -OFB8 ofb8 MCrypt Same as OFB, but operates in 8-bit mode (not recommended). +CBC cbc OpenSSL A safe default choice +CFB cfb OpenSSL N/A +CFB8 cfb8 OpenSSL Same as CFB, but operates in 8-bit mode (not recommended). +CTR ctr OpenSSL Considered as theoretically better than CBC, but not as widely available +ECB ecb OpenSSL Ignores IV (not recommended). +OFB ofb OpenSSL N/A XTS xts OpenSSL Usually used for encrypting random access data such as RAM or hard-disk storage. -Stream stream OpenSSL, MCrypt This is not actually a mode, it just says that a stream cipher is being used. Required because of the general cipher+mode initialization process. +Stream stream OpenSSL This is not actually a mode, it just says that a stream cipher is being used. Required because of the general cipher+mode initialization process. =========== ================== ================= =================================================================================================================================================== Message Length @@ -284,7 +269,7 @@ application/config/Encryption.php. ======== =============================================== Option Possible values ======== =============================================== -driver Preferred handler: 'openssl', 'mcrypt' +driver Preferred handler: 'openssl' cipher Cipher name (see :ref:`ciphers-and-modes`) mode Encryption mode (see :ref:`encryption-modes`) key Encryption key @@ -295,14 +280,14 @@ or an associative array of parameters, to the Services:: $encrypter = \Config\Services::encrypter($params); -These will replace any same-named settings in ``Config/Encryption``. +These will replace any same-named settings in ``Config\Encryption``. Using the Encryption manager directly ===================================== Instead of, or in addition to, using the `Services` described at the beginning of this page, you can use the encryption manager -directly, to create an `Encrypter`` or to change the settings +directly, to create an ``Encrypter`` or to change the settings of the current one. $encryption = new \Encryption\Encryption(); @@ -323,7 +308,7 @@ but we also included a key in the example. As previously noted, it is important that you choose a key with a proper size for the used algorithm. If you want to change the driver, for instance switching between -MCrypt and OpenSSL, you could go through the Services:: +MCrypt and OpenSSL (if MCrypt were supported), you could go through the Services:: // Switch to the MCrypt driver $encrypter= \Config\Services::encrypter(['driver' => 'mcrypt']);; From 25637c8bd5663fa2b89f7d95dfd02c23f1a14fb0 Mon Sep 17 00:00:00 2001 From: Master Yoda Date: Fri, 7 Jul 2017 06:29:09 -0700 Subject: [PATCH 12/18] Use Sodium namespace --- system/Encryption/Handlers/SodiumHandler.php | 13 ++++++++----- user_guide_src/source/libraries/encryption.rst | 5 +++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/system/Encryption/Handlers/SodiumHandler.php b/system/Encryption/Handlers/SodiumHandler.php index 83013f2f8286..c0e203c13406 100644 --- a/system/Encryption/Handlers/SodiumHandler.php +++ b/system/Encryption/Handlers/SodiumHandler.php @@ -35,6 +35,9 @@ * @since Version 3.0.0 * @filesource */ + +use \Sodium; + class SodiumHandler extends BaseHandler { @@ -97,9 +100,9 @@ public function encrypt($data, array $params = null) } $key = $params['key']; - $nonce = \Sodium\randombytes_buf(\Sodium\CRYPTO_SECRETBOX_NONCEBYTES); + $nonce = randombytes_buf(CRYPTO_SECRETBOX_NONCEBYTES); - $ciphertext = \Sodium\crypto_secretbox($data, $nonce, $key); + $ciphertext = crypto_secretbox($data, $nonce, $key); if ($ciphertext === false) { @@ -122,10 +125,10 @@ public function decrypt($data, array $params = null) { $key = $params['key']; - $nonce = self::substr($data, 0, \Sodium\CRYPTO_SECRETBOX_NONCEBYTES); - $data = self::substr($data, \Sodium\CRYPTO_SECRETBOX_NONCEBYTES); + $nonce = self::substr($data, 0, CRYPTO_SECRETBOX_NONCEBYTES); + $data = self::substr($data, CRYPTO_SECRETBOX_NONCEBYTES); - $plaintext = \Sodium\crypto_secretbox_open($data, $nonce, $key); + $plaintext = crypto_secretbox_open($data, $nonce, $key); if ($plaintext === false) { throw new EncryptionException("Bad ciphertext"); diff --git a/user_guide_src/source/libraries/encryption.rst b/user_guide_src/source/libraries/encryption.rst index 440341aa9681..1a79b74580a6 100644 --- a/user_guide_src/source/libraries/encryption.rst +++ b/user_guide_src/source/libraries/encryption.rst @@ -225,6 +225,11 @@ XTS xts OpenSSL Usually used for encrypting ran Stream stream OpenSSL This is not actually a mode, it just says that a stream cipher is being used. Required because of the general cipher+mode initialization process. =========== ================== ================= =================================================================================================================================================== +Sodium Notes +------------ + + + Message Length ============== From 31177b8c00e946034280feb198ae1b9fa9ace9f5 Mon Sep 17 00:00:00 2001 From: Master Yoda Date: Sun, 9 Jul 2017 08:07:03 -0700 Subject: [PATCH 13/18] Revise writeup, testing & Sodium handler --- system/Encryption/Encryption.php | 3 +- system/Encryption/Handlers/SodiumHandler.php | 51 ++--- tests/system/Encryption/EncryptionTest.php | 187 ++++++++-------- .../source/libraries/encryption.rst | 211 +++++++----------- 4 files changed, 187 insertions(+), 265 deletions(-) diff --git a/system/Encryption/Encryption.php b/system/Encryption/Encryption.php index 2acf319b2eb8..6de96a0cf13b 100644 --- a/system/Encryption/Encryption.php +++ b/system/Encryption/Encryption.php @@ -105,7 +105,7 @@ class Encryption * * @var string */ - protected $cipher = 'aes-128'; + protected $cipher = 'aes-256'; /** * Cipher mode @@ -158,6 +158,7 @@ public function __construct($params = []) if ( ! $this->handlers['openssl']) throw new EncryptionException('Unable to find an available encryption handler.'); + $this->initialize($params); $this->logger->info('Encryption class Initialized'); } diff --git a/system/Encryption/Handlers/SodiumHandler.php b/system/Encryption/Handlers/SodiumHandler.php index c0e203c13406..d273d40c5711 100644 --- a/system/Encryption/Handlers/SodiumHandler.php +++ b/system/Encryption/Handlers/SodiumHandler.php @@ -35,21 +35,10 @@ * @since Version 3.0.0 * @filesource */ - use \Sodium; class SodiumHandler extends BaseHandler { - - /** - * List of available modes - * - * @var array - */ - protected $modes = [ - 'gcm' => 'gcm' - ]; - // -------------------------------------------------------------------- /** @@ -64,24 +53,16 @@ public function __construct($params = null) { parent::__construct(); - if ( ! empty($params['cipher'])) + $this->cipher = 'N/A'; + $this->mode = 'N/A'; + + if (sodium_init()) { - $params['cipher'] = strtolower($params['cipher']); - $this->cipherAlias($params['cipher']); - $this->cipher = $params['cipher']; + $this->logger->info('Encryption: Sodium initialized.'); } - - if ( ! empty($params['mode'])) + else { - $params['mode'] = strtolower($params['mode']); - if ( ! isset($this->modes[$params['mode']])) - { - $this->logger->error('Encryption: Sodium mode ' . strtoupper($params['mode']) . ' is not available.'); - } - else - { - $this->mode = $this->modes[$params['mode']]; - } + $this->logger->error('Encryption: Unable to initialize Sodium.'); } } @@ -94,12 +75,9 @@ public function __construct($params = null) */ public function encrypt($data, array $params = null) { - if (empty($params['cipher'])) - { - return false; - } + // allow key to be over-ridden + $key = empty($params['key']) ? $this->key : $params['key']; - $key = $params['key']; $nonce = randombytes_buf(CRYPTO_SECRETBOX_NONCEBYTES); $ciphertext = crypto_secretbox($data, $nonce, $key); @@ -117,22 +95,27 @@ public function encrypt($data, array $params = null) /** * Decrypt * - * @param string $data Encrypted data + * @param string $data Encrypted data, with nonce pre-fixed * @param array $params Input parameters * @return string */ public function decrypt($data, array $params = null) { + // allow key to be over-ridden + $key = empty($params['key']) ? $this->key : $params['key']; - $key = $params['key']; + // split the data into nonce & ciphertext $nonce = self::substr($data, 0, CRYPTO_SECRETBOX_NONCEBYTES); $data = self::substr($data, CRYPTO_SECRETBOX_NONCEBYTES); $plaintext = crypto_secretbox_open($data, $nonce, $key); + if ($plaintext === false) { throw new EncryptionException("Bad ciphertext"); } + + return $plaintext; } // -------------------------------------------------------------------- @@ -147,7 +130,7 @@ public function decrypt($data, array $params = null) */ protected function cipherAlias(&$cipher) { - $cipher = 'AES-256'; + $cipher = 'N/A'; } } diff --git a/tests/system/Encryption/EncryptionTest.php b/tests/system/Encryption/EncryptionTest.php index 2a5aaeddeca3..c0b619bb59d2 100644 --- a/tests/system/Encryption/EncryptionTest.php +++ b/tests/system/Encryption/EncryptionTest.php @@ -16,7 +16,7 @@ public function setUp() /** * __construct test * - * Covers behavior with config encryptionKey set or not + * Covers behavior with config encryption key set or not */ public function testConstructor() { @@ -185,8 +185,7 @@ public function testGetParams() * initialize(), encrypt(), decrypt() test * * Testing the three methods separately is not realistic as they are - * designed to work together. A more thorough test for initialize() - * though is the OpenSSL/MCrypt compatibility test. + * designed to work together. * * @depends test_hkdf * @depends test__get_params @@ -243,96 +242,96 @@ public function testEncryptDecryptCustom() // -------------------------------------------------------------------- - /** - * Make sure all our supported drivers handle all the portable cipher/mode combinations - */ - public function testPortability() - { - $message = 'This is a message encrypted via MCrypt and decrypted via OpenSSL, or vice-versa.'; - - // Format is: , , - $portable = [ - ['aes-128', 'cbc', 16], - ['aes-128', 'cfb', 16], - ['aes-128', 'cfb8', 16], - ['aes-128', 'ofb', 16], - ['aes-128', 'ecb', 16], - ['aes-128', 'ctr', 16], - ['aes-192', 'cbc', 24], - ['aes-192', 'cfb', 24], - ['aes-192', 'cfb8', 24], - ['aes-192', 'ofb', 24], - ['aes-192', 'ecb', 24], - ['aes-192', 'ctr', 24], - ['aes-256', 'cbc', 32], - ['aes-256', 'cfb', 32], - ['aes-256', 'cfb8', 32], - ['aes-256', 'ofb', 32], - ['aes-256', 'ecb', 32], - ['aes-256', 'ctr', 32], - ['des', 'cbc', 7], - ['des', 'cfb', 7], - ['des', 'cfb8', 7], - ['des', 'ofb', 7], - ['des', 'ecb', 7], - ['tripledes', 'cbc', 7], - ['tripledes', 'cfb', 7], - ['tripledes', 'cfb8', 7], - ['tripledes', 'ofb', 7], - ['tripledes', 'cbc', 14], - ['tripledes', 'cfb', 14], - ['tripledes', 'cfb8', 14], - ['tripledes', 'ofb', 14], - ['tripledes', 'cbc', 21], - ['tripledes', 'cfb', 21], - ['tripledes', 'cfb8', 21], - ['tripledes', 'ofb', 21], - ['blowfish', 'cbc', 16], - ['blowfish', 'cfb', 16], - ['blowfish', 'ofb', 16], - ['blowfish', 'ecb', 16], - ['blowfish', 'cbc', 56], - ['blowfish', 'cfb', 56], - ['blowfish', 'ofb', 56], - ['blowfish', 'ecb', 56], - ['cast5', 'cbc', 11], - ['cast5', 'cfb', 11], - ['cast5', 'ofb', 11], - ['cast5', 'ecb', 11], - ['cast5', 'cbc', 16], - ['cast5', 'cfb', 16], - ['cast5', 'ofb', 16], - ['cast5', 'ecb', 16], - ['rc4', 'stream', 5], - ['rc4', 'stream', 8], - ['rc4', 'stream', 16], - ['rc4', 'stream', 32], - ['rc4', 'stream', 64], - ['rc4', 'stream', 128], - ['rc4', 'stream', 256] - ]; - $handler_index = ['openssl', 'libsodium']; - - foreach ($portable as &$test) - { - // Add some randomness to the selected handler - $handler = mt_rand(0, sizeof($handler_index) - 1); - $params = [ - 'driver' => $handler_index[$handler], - 'cipher' => $test[0], - 'mode' => $test[1], - 'key' => openssl_random_pseudo_bytes($test[2]) - ]; - - $encrypter = $this->encryption->initialize($params); - $ciphertext = $encrypter->encrypt($message); - - $handler = mt_rand(0, sizeof($handler_index) - 1); - $params['driver'] = $handler_index[$handler]; - - $encrypter = $this->encryption->initialize($params); - $this->assertEquals($message, $encrypter->decrypt($ciphertext)); - } - } +// /** +// * Make sure all our supported drivers handle all the portable cipher/mode combinations +// */ +// public function testPortability() +// { +// $message = 'This is a message encrypted via MCrypt and decrypted via OpenSSL, or vice-versa.'; +// +// // Format is: , , +// $portable = [ +// ['aes-128', 'cbc', 16], +// ['aes-128', 'cfb', 16], +// ['aes-128', 'cfb8', 16], +// ['aes-128', 'ofb', 16], +// ['aes-128', 'ecb', 16], +// ['aes-128', 'ctr', 16], +// ['aes-192', 'cbc', 24], +// ['aes-192', 'cfb', 24], +// ['aes-192', 'cfb8', 24], +// ['aes-192', 'ofb', 24], +// ['aes-192', 'ecb', 24], +// ['aes-192', 'ctr', 24], +// ['aes-256', 'cbc', 32], +// ['aes-256', 'cfb', 32], +// ['aes-256', 'cfb8', 32], +// ['aes-256', 'ofb', 32], +// ['aes-256', 'ecb', 32], +// ['aes-256', 'ctr', 32], +// ['des', 'cbc', 7], +// ['des', 'cfb', 7], +// ['des', 'cfb8', 7], +// ['des', 'ofb', 7], +// ['des', 'ecb', 7], +// ['tripledes', 'cbc', 7], +// ['tripledes', 'cfb', 7], +// ['tripledes', 'cfb8', 7], +// ['tripledes', 'ofb', 7], +// ['tripledes', 'cbc', 14], +// ['tripledes', 'cfb', 14], +// ['tripledes', 'cfb8', 14], +// ['tripledes', 'ofb', 14], +// ['tripledes', 'cbc', 21], +// ['tripledes', 'cfb', 21], +// ['tripledes', 'cfb8', 21], +// ['tripledes', 'ofb', 21], +// ['blowfish', 'cbc', 16], +// ['blowfish', 'cfb', 16], +// ['blowfish', 'ofb', 16], +// ['blowfish', 'ecb', 16], +// ['blowfish', 'cbc', 56], +// ['blowfish', 'cfb', 56], +// ['blowfish', 'ofb', 56], +// ['blowfish', 'ecb', 56], +// ['cast5', 'cbc', 11], +// ['cast5', 'cfb', 11], +// ['cast5', 'ofb', 11], +// ['cast5', 'ecb', 11], +// ['cast5', 'cbc', 16], +// ['cast5', 'cfb', 16], +// ['cast5', 'ofb', 16], +// ['cast5', 'ecb', 16], +// ['rc4', 'stream', 5], +// ['rc4', 'stream', 8], +// ['rc4', 'stream', 16], +// ['rc4', 'stream', 32], +// ['rc4', 'stream', 64], +// ['rc4', 'stream', 128], +// ['rc4', 'stream', 256] +// ]; +// $handler_index = ['openssl', 'libsodium']; +// +// foreach ($portable as &$test) +// { +// // Add some randomness to the selected handler +// $handler = mt_rand(0, sizeof($handler_index) - 1); +// $params = [ +// 'driver' => $handler_index[$handler], +// 'cipher' => $test[0], +// 'mode' => $test[1], +// 'key' => openssl_random_pseudo_bytes($test[2]) +// ]; +// +// $encrypter = $this->encryption->initialize($params); +// $ciphertext = $encrypter->encrypt($message); +// +// $handler = mt_rand(0, sizeof($handler_index) - 1); +// $params['driver'] = $handler_index[$handler]; +// +// $encrypter = $this->encryption->initialize($params); +// $this->assertEquals($message, $encrypter->decrypt($ciphertext)); +// } +// } } diff --git a/user_guide_src/source/libraries/encryption.rst b/user_guide_src/source/libraries/encryption.rst index 1a79b74580a6..f0552e4de36d 100644 --- a/user_guide_src/source/libraries/encryption.rst +++ b/user_guide_src/source/libraries/encryption.rst @@ -45,7 +45,7 @@ By default, the Encryption Library will use the OpenSSL handler, with the AES-256 cipher in CBC mode, using your configured *key* and SHA512 HMAC authentication. -AES-128 is chosen both because it is proven to be strong and +AES-256 is chosen both because it is proven to be strong and because of its wide availability across different cryptographic software and programming languages' APIs. @@ -116,127 +116,108 @@ a more friendly manner. For example:: .. _ciphers-and-modes: -Supported encryption ciphers and modes -====================================== +Encryption ciphers and modes +============================ .. note:: The terms 'cipher' and 'encryption algorithm' are interchangeable. -Portable ciphers ----------------- - Different encryption drivers support different sets of encryption algorithms and often implement -them in different ways. Our Encryption service is designed to use them in -a portable fashion - interchangeably, for the ciphers supported by all drivers. - -It is also implemented in a way that aims to match the standard -implementations in other programming languages and libraries. - -Here's a list of the so called "portable" ciphers, where -"CodeIgniter name" is the string value that you'd have to pass to the -Encryption manager to use that cipher: - -======================== ================== ============================ =============================== -Cipher name CodeIgniter name Key lengths (bits / bytes) Supported modes -======================== ================== ============================ =============================== -AES-128 / Rijndael-128 aes-128 128 / 16 CBC, CTR, CFB, CFB8, OFB, ECB -AES-192 aes-192 192 / 24 CBC, CTR, CFB, CFB8, OFB, ECB -AES-256 aes-256 256 / 32 CBC, CTR, CFB, CFB8, OFB, ECB -Blowfish blowfish 128-448 / 16-56 CBC, CFB, OFB, ECB -CAST5 / CAST-128 cast5 88-128 / 11-16 CBC, CFB, OFB, ECB -DES des 56 / 7 CBC, CFB, CFB8, OFB, ECB -RC4 / ARCFour rc4 40-2048 / 5-256 Stream -TripleDES tripledes 56 / 7, 112 / 14, 168 / 21 CBC, CFB, CFB8, OFB +them in different ways. Some algorithms expect specific key lengths, while others support +variable length keys. Each algorithm usually supports several different encryption modes. + +Here's a list of common ciphers: + +======================== ============================ =============================== +Cipher name Key lengths (bits / bytes) Supported modes +======================== ============================ =============================== +AES-128 / Rijndael-128 128 / 16 CBC, CTR, CFB, CFB8, OFB, ECB +AES-192 192 / 24 CBC, CTR, CFB, CFB8, OFB, ECB +AES-256 256 / 32 CBC, CTR, CFB, CFB8, OFB, ECB +Blowfish 128-448 / 16-56 CBC, CFB, OFB, ECB +CAST5 / CAST-128 88-128 / 11-16 CBC, CFB, OFB, ECB +DES 56 / 7 CBC, CFB, CFB8, OFB, ECB +RC4 / ARCFour 40-2048 / 5-256 Stream +TripleDES 56 / 7, 112 / 14, 168 / 21 CBC, CFB, CFB8, OFB ======================== ================== ============================ =============================== -.. note:: In case it isn't clear from the above table, Blowfish, CAST5 - and RC4 support variable length keys. That is, any number in the - shown ranges is valid, although in bit terms that only happens - in 8-bit increments. +.. note:: Blowfish, CAST5 and RC4 support variable length keys, + although in bit terms that only happens in 8-bit increments. Even though CAST5 supports key lengths lower than 128 bits (16 bytes), in fact they will just be zero-padded to the maximum length, as specified in `RFC 2144 `_. - -OpenSSL Notes -------------- - -As noted above, the encryption drivers support different sets of encryption -ciphers. We do recommend that use driver-specific settings. - -For reference, here's a list of most of them: - - -============== ========= ============================== ========================================= -Cipher name Driver Key lengths (bits / bytes) Supported modes -============== ========= ============================== ========================================= -AES-128 OpenSSL 128 / 16 CBC, CTR, CFB, CFB8, OFB, ECB, XTS -AES-192 OpenSSL 192 / 24 CBC, CTR, CFB, CFB8, OFB, ECB, XTS -AES-256 OpenSSL 256 / 32 CBC, CTR, CFB, CFB8, OFB, ECB, XTS -Camellia-128 OpenSSL 128 / 16 CBC, CFB, CFB8, OFB, ECB -Camellia-192 OpenSSL 192 / 24 CBC, CFB, CFB8, OFB, ECB -Camellia-256 OpenSSL 256 / 32 CBC, CFB, CFB8, OFB, ECB -RC2 OpenSSL 8-1024 / 1-128 CBC, CFB, OFB, ECB -Seed OpenSSL 128 / 16 CBC, CFB, OFB, ECB -============== ========= ============================== ========================================= - -.. note:: If you wish to use one of those ciphers, you'd have to pass - its name in lower-case to the Encryption library. - - You've probably noticed that all AES cipers (and Rijndael-128) - are also listed in the portable ciphers list. This is because - drivers support different modes for these ciphers. Also, it is - important to note that AES-128 and Rijndael-128 are actually - the same cipher, but **only** when used with a 128-bit key. - - CAST-128 / CAST-5 is also listed in both the portable and - driver-specific ciphers list. This is because OpenSSL's - implementation doesn't appear to be working correctly with - key sizes of 80 bits and lower. - - RC2 is supported by multiple drivers, but the implementation may differ, - so it is not portable. - .. _encryption-modes: Encryption modes ---------------- Different modes of encryption have different characteristics and serve -for different purposes. Some are stronger than others, some are faster +different purposes. Some are stronger than others, some are faster and some offer extra features. -We are not going in depth into that here, we'll leave that to the -cryptography experts. The table below is to provide brief informational -reference to our more experienced users. If you are a beginner, just -stick to the CBC mode - it is widely accepted as strong and secure for -general purposes. - -=========== ================== ================= =================================================================================================================================================== -Mode name CodeIgniter name Driver support Additional info -=========== ================== ================= =================================================================================================================================================== -CBC cbc OpenSSL A safe default choice -CFB cfb OpenSSL N/A -CFB8 cfb8 OpenSSL Same as CFB, but operates in 8-bit mode (not recommended). -CTR ctr OpenSSL Considered as theoretically better than CBC, but not as widely available -ECB ecb OpenSSL Ignores IV (not recommended). -OFB ofb OpenSSL N/A -XTS xts OpenSSL Usually used for encrypting random access data such as RAM or hard-disk storage. -Stream stream OpenSSL This is not actually a mode, it just says that a stream cipher is being used. Required because of the general cipher+mode initialization process. -=========== ================== ================= =================================================================================================================================================== +If you are unsure which to use, stick to the CBC mode - it is widely accepted +as strong and secure for general purposes. + +=========== =================================================================================================================================================== +Mode name Additional info +=========== =================================================================================================================================================== +CBC A safe default choice +CFB N/A +CFB8 Same as CFB, but operates in 8-bit mode (not recommended). +CTR Considered as theoretically better than CBC, but not as widely available +ECB Ignores IV (not recommended). +OFB N/A +XTS Usually used for encrypting random access data such as RAM or hard-disk storage. +Stream This is not actually a mode, it just says that a stream cipher is being used. Required because of the general cipher+mode initialization process. +=========== =================================================================================================================================================== + +OpenSSL Notes +------------- + +As noted above, the encryption drivers support different sets of encryption +ciphers. We do recommend that you use driver-specific settings. + +The following are supported by OpenSSL: + +============== ============================== ========================================= +Cipher name Key lengths (bits / bytes) Supported modes +============== ============================== ========================================= +AES-128 128 / 16 CBC, CTR, CFB, CFB8, OFB, ECB, XTS +AES-192 192 / 24 CBC, CTR, CFB, CFB8, OFB, ECB, XTS +AES-256 256 / 32 CBC, CTR, CFB, CFB8, OFB, ECB, XTS +Blowfish 128-448 / 16-56 CBC, CFB, OFB, ECB +Camellia-128 128 / 16 CBC, CFB, CFB8, OFB, ECB +Camellia-192 192 / 24 CBC, CFB, CFB8, OFB, ECB +Camellia-256 256 / 32 CBC, CFB, CFB8, OFB, ECB +CAST5 88-128 / 11-16 CBC, CFB, OFB, ECB +DES 56 / 7 CBC, CFB, CFB8, OFB, ECB +RC2 8-1024 / 1-128 CBC, CFB, OFB, ECB +RC4 40-2048 / 5-256 Stream +TripleDES 56 / 7, 112 / 14, 168 / 21 CBC, CFB, CFB8, OFB +Seed 128 / 16 CBC, CFB, OFB, ECB +============== ============================== ========================================= + Sodium Notes ------------ +Sodium is a modern, easy-to-use software library for encryption, decryption, signatures, password hashing and more. +Sodium automatically uses AES-256 if it detects hardware acceleration/ +Otherwise, it will use the ChaCha20 cipher. + +You will need *libsodium* installed, as well as the PECL *Libsodium extenstion*, +in order to use this handler. Message Length ============== -It's probably important for you to know that an encrypted string is usually +An encrypted string is usually longer than the original, plain-text string (depending on the cipher). -This is influenced by the cipher algorithm itself, the IV prepended to the +This is influenced by the cipher algorithm itself, the initialization vector (IV) +prepended to the cipher-text and the HMAC authentication message that is also prepended. Furthermore, the encrypted message is also Base64-encoded so that it is safe for storage and transmission, regardless of a possible character set in use. @@ -253,7 +234,7 @@ The Encryption library is designed to use repeatedly the same driver, encryption cipher, mode and key. As noted in the "Default behavior" section above, this means using an -auto-detected driver (OpenSSL has a higher priority), the AES-128 ciper +auto-detected driver (OpenSSL has a higher priority), the AES-256 ciper in CBC mode, and your ``$key`` value. Encryption configuration settings are normally set in @@ -262,7 +243,7 @@ application/config/Encryption.php. ======== =============================================== Option Possible values ======== =============================================== -driver Preferred handler: 'openssl' +driver Preferred handler: 'OpenSSL' cipher Cipher name (see :ref:`ciphers-and-modes`) mode Encryption mode (see :ref:`encryption-modes`) key Encryption key @@ -345,48 +326,6 @@ You don't need to worry about it. configuration, you should always check the return value of ``decrypt()`` in production code. -How it works ------------- - -Here's what happens under the hood: - -- ``$encrypter->encrypt($plaintext)`` - - #. Derive an encryption key and a HMAC key from your configured - *encryption_key* via HKDF, using the SHA-512 digest algorithm. - - #. Generate a random initialization vector (IV). - - #. Encrypt the data via AES-128 in CBC mode (or another previously - configured cipher and mode), using the above-mentioned derived - encryption key and IV. - - #. Prepend said IV to the resulting cipher-text. - - #. Base64-encode the resulting string, so that it can be safely - stored or transferred without worrying about character sets. - - #. Create a SHA-512 HMAC authentication message using the derived - HMAC key to ensure data integrity and prepend it to the Base64 - string. - -- ``$encrypter->decrypt($ciphertext)`` - - #. Derive an encryption key and a HMAC key from your configured - *encryption_key* via HKDF, using the SHA-512 digest algorithm. - Because your configured *encryption_key* is the same, this - will produce the same result as in the ``encrypt()`` method - above - otherwise you won't be able to decrypt it. - - #. Check if the string is long enough, separate the HMAC out of - it and validate if it is correct (this is done in a way that - prevents timing attacks against it). Return FALSE if either of - the checks fails. - - #. Base64-decode the string. - - #. Separate the IV out of the cipher-text and decrypt the said - cipher-text using that IV and the derived encryption key. .. _custom-parameters: From dfa73b63eeaafc6ec6659f437a1fd4df3564f3f5 Mon Sep 17 00:00:00 2001 From: Master Yoda Date: Mon, 10 Jul 2017 00:31:21 -0700 Subject: [PATCH 14/18] Getting ready to use vfsStream --- system/Autoloader/Autoloader.php | 25 ++-- system/Autoloader/FileLocator.php | 72 +++++----- tests/_support/Autoloader/MockAutoloader.php | 1 - tests/_support/Autoloader/MockFileLocator.php | 1 + tests/system/Autoloader/AutoloaderTest.php | 77 +++++----- tests/system/Autoloader/FileLocatorTest.php | 135 +++++++++--------- 6 files changed, 159 insertions(+), 152 deletions(-) diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index cb6a19bd8e8c..c2b849152eb8 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -143,17 +143,15 @@ public function register() // Now prepend another loader for the files in our class map. $config = is_array($this->classmap) ? $this->classmap : []; - spl_autoload_register(function ($class) use ($config) - { + spl_autoload_register(function ($class) use ($config) { if ( ! array_key_exists($class, $config)) { return false; } include_once $config[$class]; - }, - true, // Throw exception - true // Prepend + }, true, // Throw exception + true // Prepend ); } @@ -257,11 +255,13 @@ protected function loadInNamespace($class) { $directory = rtrim($directory, '/'); - if (strpos($class, $namespace) === 0) { + if (strpos($class, $namespace) === 0) + { $filePath = $directory . str_replace('\\', '/', substr($class, strlen($namespace))) . '.php'; $filename = $this->requireFile($filePath); - if ($filename) { + if ($filename) + { return $filename; } } @@ -293,16 +293,16 @@ protected function loadLegacy($class) } $paths = [ - APPPATH.'Controllers/', - APPPATH.'Libraries/', - APPPATH.'Models/', + APPPATH . 'Controllers/', + APPPATH . 'Libraries/', + APPPATH . 'Models/', ]; - $class = str_replace('\\', '/', $class).'.php'; + $class = str_replace('\\', '/', $class) . '.php'; foreach ($paths as $path) { - if ($file = $this->requireFile($path.$class)) + if ($file = $this->requireFile($path . $class)) { return $file; } @@ -368,5 +368,4 @@ public function sanitizeFilename(string $filename): string } //-------------------------------------------------------------------- - } diff --git a/system/Autoloader/FileLocator.php b/system/Autoloader/FileLocator.php index d6275fd59324..e4aeddbba6d4 100644 --- a/system/Autoloader/FileLocator.php +++ b/system/Autoloader/FileLocator.php @@ -35,7 +35,6 @@ * @since Version 3.0.0 * @filesource */ - use Config\Autoload; /** @@ -87,17 +86,15 @@ public function __construct(Autoload $autoload) * * @return string The path to the file if found, or an empty string. */ - public function locateFile(string $file, string $folder=null, string $ext = 'php'): string + public function locateFile(string $file, string $folder = null, string $ext = 'php'): string { // Ensure the extension is on the filename - $file = strpos($file, '.'.$ext) !== false - ? $file - : $file.'.'.$ext; + $file = strpos($file, '.' . $ext) !== false ? $file : $file . '.' . $ext; // Clean the folder name from the filename - if (! empty($folder)) + if ( ! empty($folder)) { - $file = str_replace($folder.'/', '', $file); + $file = str_replace($folder . '/', '', $file); } // No namespaceing? Try the application folder. @@ -112,24 +109,23 @@ public function locateFile(string $file, string $folder=null, string $ext = 'php $segments = explode('\\', $file); // The first segment will be empty if a slash started the filename. - if (empty($segments[0])) unset($segments[0]); + if (empty($segments[0])) + unset($segments[0]); - $path = ''; - $prefix = ''; + $path = ''; + $prefix = ''; $filename = ''; - while (! empty($segments)) + while ( ! empty($segments)) { - $prefix .= empty($prefix) - ? ucfirst(array_shift($segments)) - : '\\'. ucfirst(array_shift($segments)); + $prefix .= empty($prefix) ? ucfirst(array_shift($segments)) : '\\' . ucfirst(array_shift($segments)); - if (! array_key_exists($prefix, $this->namespaces)) + if ( ! array_key_exists($prefix, $this->namespaces)) { continue; } - $path = $this->namespaces[$prefix].'/'; + $path = $this->namespaces[$prefix] . '/'; $filename = implode('/', $segments); break; } @@ -138,14 +134,14 @@ public function locateFile(string $file, string $folder=null, string $ext = 'php // expects this file to be within that folder, like 'Views', // or 'libraries'. // @todo Allow it to check with and without the nested folder. - if (! empty($folder) && strpos($filename, $folder) === false) + if ( ! empty($folder) && strpos($filename, $folder) === false) { - $filename = $folder.'/'.$filename; + $filename = $folder . '/' . $filename; } $path .= $filename; - if (! $this->requireFile($path)) + if ( ! $this->requireFile($path)) { $path = ''; } @@ -178,17 +174,15 @@ public function search(string $path, string $ext = 'php'): array $foundPaths = []; // Ensure the extension is on the filename - $path = strpos($path, '.'.$ext) !== false - ? $path - : $path.'.'.$ext; + $path = strpos($path, '.' . $ext) !== false ? $path : $path . '.' . $ext; foreach ($this->namespaces as $name => $folder) { - $folder = rtrim($folder, '/').'/'; + $folder = rtrim($folder, '/') . '/'; - if (file_exists($folder.$path)) + if (file_exists($folder . $path)) { - $foundPaths[] = $folder.$path; + $foundPaths[] = $folder . $path; } } @@ -213,7 +207,7 @@ public function findQualifiedNameFromPath(string $path) { $path = realpath($path); - if (! $path) + if ( ! $path) { return; } @@ -221,12 +215,13 @@ public function findQualifiedNameFromPath(string $path) foreach ($this->namespaces as $namespace => $nsPath) { $nsPath = realpath($nsPath); - if (is_numeric($namespace)||empty($nsPath)) continue; + if (is_numeric($namespace) || empty($nsPath)) + continue; - if (mb_strpos($path,$nsPath) === 0) + if (mb_strpos($path, $nsPath) === 0) { - $className = '\\'.$namespace.'\\'. - ltrim(str_replace('/', '\\', mb_substr($path, mb_strlen($nsPath))), '\\'); + $className = '\\' . $namespace . '\\' . + ltrim(str_replace('/', '\\', mb_substr($path, mb_strlen($nsPath))), '\\'); // Remove the file extension (.php) $className = mb_substr($className, 0, -4); @@ -247,21 +242,23 @@ public function findQualifiedNameFromPath(string $path) */ public function listFiles(string $path): array { - if (empty($path)) return []; + if (empty($path)) + return []; $files = []; helper('filesystem'); foreach ($this->namespaces as $namespace => $nsPath) { - $fullPath = realpath(rtrim($nsPath, '/') .'/'. $path); + $fullPath = realpath(rtrim($nsPath, '/') . '/' . $path); - if (! is_dir($fullPath)) continue; + if ( ! is_dir($fullPath)) + continue; $tempFiles = get_filenames($fullPath, true); //CLI::newLine($tempFiles); - if (! count($tempFiles)) + if ( ! count($tempFiles)) { continue; } @@ -283,15 +280,13 @@ public function listFiles(string $path): array * @internal param string $ext * */ - protected function legacyLocate(string $file, string $folder=null): string + protected function legacyLocate(string $file, string $folder = null): string { $paths = [APPPATH, BASEPATH]; foreach ($paths as $path) { - $path .= empty($folder) - ? $file - : $folder.'/'.$file; + $path .= empty($folder) ? $file : $folder . '/' . $file; if ($this->requireFile($path) === true) { @@ -319,5 +314,4 @@ protected function requireFile(string $path): bool } //-------------------------------------------------------------------- - } diff --git a/tests/_support/Autoloader/MockAutoloader.php b/tests/_support/Autoloader/MockAutoloader.php index 6b0987536e91..a9b1fe097829 100644 --- a/tests/_support/Autoloader/MockAutoloader.php +++ b/tests/_support/Autoloader/MockAutoloader.php @@ -20,5 +20,4 @@ protected function requireFile($file) } //-------------------------------------------------------------------- - } diff --git a/tests/_support/Autoloader/MockFileLocator.php b/tests/_support/Autoloader/MockFileLocator.php index dd53bebc8623..00d2dbda6070 100644 --- a/tests/_support/Autoloader/MockFileLocator.php +++ b/tests/_support/Autoloader/MockFileLocator.php @@ -2,6 +2,7 @@ class MockFileLocator extends FileLocator { + protected $files = []; //-------------------------------------------------------------------- diff --git a/tests/system/Autoloader/AutoloaderTest.php b/tests/system/Autoloader/AutoloaderTest.php index 3d066e647a20..c995a8601427 100644 --- a/tests/system/Autoloader/AutoloaderTest.php +++ b/tests/system/Autoloader/AutoloaderTest.php @@ -1,6 +1,7 @@ classmap = [ - 'FirstClass' => '/app/dir/First.php', - 'Name\Spaced\Class' => '/app/namespace/Class.php', + 'FirstClass' => '/app/dir/First.php', + 'Name\Spaced\Class' => '/app/namespace/Class.php', ]; $config->psr4 = [ - 'App\Controllers' => '/application/Controllers', - 'App\Libraries' => '/application/somewhere', + 'App\Controllers' => '/application/Controllers', + 'App\Libraries' => '/application/somewhere', ]; $this->loader = new MockAutoloader(); @@ -32,37 +33,40 @@ protected function setUp() '/application/somewhere/Classname.php', '/app/dir/First.php', '/app/namespace/Class.php', - '/my/app/Class.php', - APPPATH.'Libraries/someLibrary.php', - APPPATH.'Models/someModel.php', + '/my/app/Class.php', + APPPATH . 'Libraries/someLibrary.php', + APPPATH . 'Models/someModel.php', ]); } //-------------------------------------------------------------------- - //-------------------------------------------------------------------- // PSR4 Namespacing //-------------------------------------------------------------------- - public function testServiceAutoLoaderFromShareInstances() { + public function testServiceAutoLoaderFromShareInstances() + { $auto_loader = \CodeIgniter\Config\Services::autoloader(); // $auto_loader->register(); - $actual = $auto_loader->loadClass('App\Controllers\Checks'); - $expected = APPPATH.'Controllers/Checks.php'; + // look for Home controller, as that should be in base repo + $actual = $auto_loader->loadClass('App\Controllers\Home'); + $expected = APPPATH . 'Controllers/Home.php'; $this->assertSame($expected, $actual); } //-------------------------------------------------------------------- - public function testServiceAutoLoader() { + public function testServiceAutoLoader() + { $getShared = false; $auto_loader = \CodeIgniter\Config\Services::autoloader($getShared); $auto_loader->initialize(new Autoload()); $auto_loader->register(); - $actual = $auto_loader->loadClass('App\Controllers\Checks'); - $expected = APPPATH.'Controllers/Checks.php'; + // look for Home controller, as that should be in base repo + $actual = $auto_loader->loadClass('App\Controllers\Home'); + $expected = APPPATH . 'Controllers/Home.php'; $this->assertSame($expected, $actual); } @@ -70,11 +74,11 @@ public function testServiceAutoLoader() { public function testExistingFile() { - $actual = $this->loader->loadClass('App\Controllers\Classname'); + $actual = $this->loader->loadClass('App\Controllers\Classname'); $expected = '/application/Controllers/Classname.php'; $this->assertSame($expected, $actual); - $actual = $this->loader->loadClass('App\Libraries\Classname'); + $actual = $this->loader->loadClass('App\Libraries\Classname'); $expected = '/application/somewhere/Classname.php'; $this->assertSame($expected, $actual); } @@ -83,7 +87,7 @@ public function testExistingFile() public function testMatchesWithPreceedingSlash() { - $actual = $this->loader->loadClass('\App\Controllers\Classname'); + $actual = $this->loader->loadClass('\App\Controllers\Classname'); $expected = '/application/Controllers/Classname.php'; $this->assertSame($expected, $actual); } @@ -92,7 +96,7 @@ public function testMatchesWithPreceedingSlash() public function testMatchesWithFileExtension() { - $actual = $this->loader->loadClass('\App\Controllers\Classname.php'); + $actual = $this->loader->loadClass('\App\Controllers\Classname.php'); $expected = '/application/Controllers/Classname.php'; $this->assertSame($expected, $actual); } @@ -105,7 +109,6 @@ public function testMissingFile() } //-------------------------------------------------------------------- - //-------------------------------------------------------------------- /** @@ -159,24 +162,32 @@ public function testAddNamespaceStingToArray() $this->assertSame('/application/Controllers/Classname.php', $this->loader->loadClass('App\Controllers\Classname')); } - //-------------------------------------------------------------------- - - public function testRemoveNamespace() - { - $this->loader->addNamespace('My\App', '/my/app'); - $this->assertSame('/my/app/Class.php',$this->loader->loadClass('My\App\Class')); - - $this->loader->removeNamespace('My\App'); - $this->assertFalse((bool)$this->loader->loadClass('My\App\Class')); - } + //-------------------------------------------------------------------- + + public function testRemoveNamespace() + { + $this->loader->addNamespace('My\App', '/my/app'); + $this->assertSame('/my/app/Class.php', $this->loader->loadClass('My\App\Class')); + + $this->loader->removeNamespace('My\App'); + $this->assertFalse((bool) $this->loader->loadClass('My\App\Class')); + } //-------------------------------------------------------------------- public function testLoadLegacy() { - $this->assertFalse((bool)$this->loader->loadClass('someLibraries')); - $this->assertTrue((bool)$this->loader->loadClass('someLibrary')); - $this->assertTrue((bool)$this->loader->loadClass('someModel')); + // should not be able to find a folder + $this->assertFalse((bool) $this->loader->loadClass('someLibraries')); + // should be able to find these because we said so in the MockAutoloader + $this->assertTrue((bool) $this->loader->loadClass('someLibrary')); + $this->assertTrue((bool) $this->loader->loadClass('someModel')); + // should not be able to find these - don't exist + $this->assertFalse((bool) $this->loader->loadClass('anotherLibrary')); + $this->assertFalse((bool) $this->loader->loadClass('\nester\anotherLibrary')); + $this->assertFalse((bool) $this->loader->loadClass('\Shouldnt\Find\This')); + // should not be able to find these legacy classes - namespaced + $this->assertFalse((bool) $this->loader->loadClass('anotherLibrary')); } //-------------------------------------------------------------------- @@ -190,7 +201,7 @@ public function testSanitizationSimply() } //-------------------------------------------------------------------- - + public function testSanitizationAllowsWindowsFilepaths() { $test = 'C:\path\to\some/file.php'; diff --git a/tests/system/Autoloader/FileLocatorTest.php b/tests/system/Autoloader/FileLocatorTest.php index 68956f9ce6ea..720ae94f87da 100644 --- a/tests/system/Autoloader/FileLocatorTest.php +++ b/tests/system/Autoloader/FileLocatorTest.php @@ -2,9 +2,11 @@ use Config\MockAutoload; use PHPUnit\Framework\TestCase; +use org\bovigo\vfs\vfsStream; -class FileLocatorTest extends TestCase +class FileLocatorTest extends \CIUnitTestCase { + /** * @var MockFileLocator */ @@ -16,18 +18,18 @@ public function setUp() { $config = new MockAutoload(); $config->psr4 = [ - 'App\Libraries' => '/application/somewhere', - 'App' => '/application', - 'Sys' => BASEPATH, - 'Blog' => '/modules/blog' + 'App\Libraries' => '/application/somewhere', + 'App' => '/application', + 'Sys' => BASEPATH, + 'Blog' => '/modules/blog' ]; $this->loader = new MockFileLocator($config); $this->loader->setFiles([ - APPPATH.'index.php', - APPPATH.'Views/index.php', - APPPATH.'Views/admin/users/create.php', + APPPATH . 'index.php', + APPPATH . 'Views/index.php', + APPPATH . 'Views/admin/users/create.php', '/modules/blog/Views/index.php', '/modules/blog/Views/admin/posts.php' ]); @@ -39,7 +41,7 @@ public function testLocateFileWorksInApplicationDirectory() { $file = 'index'; - $expected = APPPATH.'Views/index.php'; + $expected = APPPATH . 'Views/index.php'; $this->assertEquals($expected, $this->loader->locateFile($file, 'Views')); } @@ -50,7 +52,7 @@ public function testLocateFileWorksInApplicationDirectoryWithoutFolder() { $file = 'index'; - $expected = APPPATH.'index.php'; + $expected = APPPATH . 'index.php'; $this->assertEquals($expected, $this->loader->locateFile($file)); } @@ -61,7 +63,7 @@ public function testLocateFileWorksInNestedApplicationDirectory() { $file = 'admin/users/create'; - $expected = APPPATH.'Views/admin/users/create.php'; + $expected = APPPATH . 'Views/admin/users/create.php'; $this->assertEquals($expected, $this->loader->locateFile($file, 'Views')); } @@ -83,7 +85,7 @@ public function testLocateFileReplacesFolderNameLegacy() { $file = 'Views/index.php'; - $expected = APPPATH.'Views/index.php'; + $expected = APPPATH . 'Views/index.php'; $this->assertEquals($expected, $this->loader->locateFile($file, 'Views')); } @@ -120,42 +122,42 @@ public function testLocateFileReturnsEmptyWithBadNamespace() } //-------------------------------------------------------------------- - + public function testSearchSimple() { - $expected = rtrim(APPPATH,'/').'/Config/App.php'; - + $expected = rtrim(APPPATH, '/') . '/Config/App.php'; + $foundFiles = $this->loader->search('Config/App.php'); - $this->assertEquals($expected,$foundFiles[0]); + $this->assertEquals($expected, $foundFiles[0]); } - - //-------------------------------------------------------------------- - + + //-------------------------------------------------------------------- + public function testSearchWithFileExtension() { - $expected = rtrim(APPPATH,'/').'/Config/App.php'; + $expected = rtrim(APPPATH, '/') . '/Config/App.php'; - $foundFiles = $this->loader->search('Config/App','php'); + $foundFiles = $this->loader->search('Config/App', 'php'); - $this->assertEquals($expected,$foundFiles[0]); + $this->assertEquals($expected, $foundFiles[0]); } - - //-------------------------------------------------------------------- - + + //-------------------------------------------------------------------- + public function testSearchWithMultipleFilesFound() - { - $foundFiles = $this->loader->search('index','html'); + { + $foundFiles = $this->loader->search('index', 'html'); - $expected = rtrim(APPPATH,'/').'/index.html'; - $this->assertTrue(in_array($expected,$foundFiles)); + $expected = rtrim(APPPATH, '/') . '/index.html'; + $this->assertTrue(in_array($expected, $foundFiles)); - $expected = rtrim(BASEPATH,'/').'/index.html'; - $this->assertTrue(in_array($expected,$foundFiles)); + $expected = rtrim(BASEPATH, '/') . '/index.html'; + $this->assertTrue(in_array($expected, $foundFiles)); } //-------------------------------------------------------------------- - + public function testSearchForFileNotExist() { $foundFiles = $this->loader->search('Views/Fake.html'); @@ -164,51 +166,51 @@ public function testSearchForFileNotExist() } //-------------------------------------------------------------------- - + public function testListFilesSimple() - { + { $files = $this->loader->listFiles('Config/'); - $expectedWin = APPPATH.'Config\App.php'; - $expectedLin = APPPATH.'Config/App.php'; - $this->assertTrue(in_array($expectedWin,$files)||in_array($expectedLin,$files)); + $expectedWin = APPPATH . 'Config\App.php'; + $expectedLin = APPPATH . 'Config/App.php'; + $this->assertTrue(in_array($expectedWin, $files) || in_array($expectedLin, $files)); } - - //-------------------------------------------------------------------- - + + //-------------------------------------------------------------------- + public function testListFilesWithFileAsInput() { $files = $this->loader->listFiles('Config/App.php'); $this->assertTrue(empty($files)); - } - - //-------------------------------------------------------------------- - + } + + //-------------------------------------------------------------------- + public function testListFilesFromMultipleDir() { - $files = $this->loader->listFiles('Filters/'); - - $expectedWin = APPPATH.'Filters\DebugToolbar.php'; - $expectedLin = APPPATH.'Filters/DebugToolbar.php'; - $this->assertTrue(in_array($expectedWin,$files)||in_array($expectedLin,$files)); + $files = $this->loader->listFiles('Filters/'); - $expectedWin = BASEPATH.'Filters\Filters.php'; - $expectedLin = BASEPATH.'Filters/Filters.php'; - $this->assertTrue(in_array($expectedWin,$files)||in_array($expectedLin,$files)); + $expectedWin = APPPATH . 'Filters\DebugToolbar.php'; + $expectedLin = APPPATH . 'Filters/DebugToolbar.php'; + $this->assertTrue(in_array($expectedWin, $files) || in_array($expectedLin, $files)); + + $expectedWin = BASEPATH . 'Filters\Filters.php'; + $expectedLin = BASEPATH . 'Filters/Filters.php'; + $this->assertTrue(in_array($expectedWin, $files) || in_array($expectedLin, $files)); } - - //-------------------------------------------------------------------- - + + //-------------------------------------------------------------------- + public function testListFilesWithPathNotExist() { $files = $this->loader->listFiles('Fake/'); - $this->assertTrue(empty($files)); + $this->assertTrue(empty($files)); } - - //-------------------------------------------------------------------- - + + //-------------------------------------------------------------------- + public function testListFilesWithoutPath() { $files = $this->loader->listFiles(''); @@ -221,27 +223,28 @@ public function testFindQNameFromPathSimple() $ClassName = $this->loader->findQualifiedNameFromPath('system/HTTP/Header.php'); $expected = '\Sys\HTTP\Header'; - $this->assertEquals($expected,$ClassName); + $this->assertEquals($expected, $ClassName); } public function testFindQNameFromPathWithNumericNamespace() { $ClassName = $this->loader->findQualifiedNameFromPath('application/Config/App.php'); - - $this->assertEquals(null,$ClassName); + + $this->assertEquals(null, $ClassName); } public function testFindQNameFromPathWithFileNotExist() { $ClassName = $this->loader->findQualifiedNameFromPath('modules/blog/Views/index.php'); - - $this->assertEquals(null,$ClassName); + + $this->assertEquals(null, $ClassName); } public function testFindQNameFromPathWithoutCorrespondingNamespace() { $ClassName = $this->loader->findQualifiedNameFromPath('tests/system/CodeIgniterTest.php'); - - $this->assertEquals(null,$ClassName); + + $this->assertEquals(null, $ClassName); } + } From 669dd3c19f15308bc2d99d2f4ea9c134ab07d32a Mon Sep 17 00:00:00 2001 From: Master Yoda Date: Tue, 11 Jul 2017 13:37:28 -0700 Subject: [PATCH 15/18] Encryption testing & debugging --- composer.json | 3 +- system/Encryption/Encryption.php | 19 +- system/Encryption/Handlers/BaseHandler.php | 65 ++-- system/Encryption/Handlers/OpenSSLHandler.php | 7 +- system/Encryption/Handlers/SodiumHandler.php | 10 +- tests/system/Encryption/EncryptionTest.php | 325 +++--------------- .../system/Encryption/OpenSSLHandlerTest.php | 234 +++++++++++++ tests/system/Encryption/SodiumHandlerTest.php | 229 ++++++++++++ 8 files changed, 570 insertions(+), 322 deletions(-) create mode 100644 tests/system/Encryption/OpenSSLHandlerTest.php create mode 100644 tests/system/Encryption/SodiumHandlerTest.php diff --git a/composer.json b/composer.json index 6a816612db28..6592f8518115 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ }, "require": { "php": ">=7.0", - "zendframework/zend-escaper": "^2.5" + "zendframework/zend-escaper": "^2.5", + "paragonie/sodium_compat": "^1.1" }, "require-dev": { "phpunit/phpunit": "^6.0", diff --git a/system/Encryption/Encryption.php b/system/Encryption/Encryption.php index 6de96a0cf13b..bebba7566d6f 100644 --- a/system/Encryption/Encryption.php +++ b/system/Encryption/Encryption.php @@ -90,8 +90,8 @@ class Encryption * @var array */ protected $drivers = [ - 'openssl' => 'OpenSSL', - 'libsodium' => 'Sodium' + 'openssl' => 'OpenSSL', + 'libsodium' => 'Sodium' ]; /** @@ -151,14 +151,14 @@ public function __construct($params = []) // determine what is installed $this->handlers = [ - 'openssl' => extension_loaded('openssl'), - 'sodium' => extension_loaded('libsodium') + 'OpenSSL' => extension_loaded('openssl'), + 'Sodium' => extension_loaded('libsodium') ]; - if ( ! $this->handlers['openssl']) + if ( ! $this->handlers['OpenSSL']) throw new EncryptionException('Unable to find an available encryption handler.'); - $this->initialize($params); + $this->initialize($params); $this->logger->info('Encryption class Initialized'); } @@ -173,8 +173,11 @@ public function __construct($params = []) public function initialize(array $params = null) { // how should this be handled? - $this->driver = $params['driver'] ?? 'openssl'; - $this->handler = $this->drivers[$this->driver]; + $this->driver = $params['driver'] ?? 'OpenSSL'; + // translate if needed + if (isset($this->drivers[$this->driver])) + $this->driver = $this->drivers[$this->driver]; + $this->handler = $this->driver; // use config key if initialization didn't create one if ( ! isset($this->key) && self::strlen($key = $this->config->key) > 0) diff --git a/system/Encryption/Handlers/BaseHandler.php b/system/Encryption/Handlers/BaseHandler.php index 4ec2d0f9ecd1..603676389fd9 100644 --- a/system/Encryption/Handlers/BaseHandler.php +++ b/system/Encryption/Handlers/BaseHandler.php @@ -68,14 +68,21 @@ abstract class BaseHandler implements \CodeIgniter\Encryption\EncrypterInterface protected $handle; /** - * Encryption key + * Cipher handler + * + * @var mixed + */ + protected $handler; + + /** + * Encryption key (starting point, anyway) * * @var string */ protected $key; /** - * List of available modes + * List of available modes. Override by driver * * @var array */ @@ -126,30 +133,29 @@ public function __construct($config = null) // -------------------------------------------------------------------- /** - * Get params. Is there harm to exposing this? + * Get params, for testing * * @param array $params Input parameters - * @return array + * @return mixed associative array of parameters if ok, else false */ public function getParams($params) { + // if no parameters were provided, but we have viable settings already, + // tell them what we have if (empty($params)) { - return isset($this->cipher, $this->mode, $this->key, $this->handle) ? [ - 'handle' => $this->handle, - 'cipher' => $this->cipher, - 'mode' => $this->mode, - 'key' => null, - 'base64' => true, - 'hmac_digest' => 'sha512', - 'hmackey' => null + return isset($this->cipher, $this->mode, $this->key) ? [ + 'handle' => $this->handle, + 'cipher' => $this->cipher, + 'mode' => $this->mode, + 'key' => null, + 'base64' => true, + 'hmac_digest' => 'sha512', + 'hmackey' => null ] : false; } - elseif ( ! isset($params['cipher'], $params['mode'], $params['key'])) - { - return false; - } + // if a cipher mode was given, make sure it is valid if (isset($params['mode'])) { $params['mode'] = strtolower($params['mode']); @@ -163,16 +169,19 @@ public function getParams($params) } } + // if the HMAC parameter is false, zap the HMAC digest & key if (isset($params['hmac']) && $params['hmac'] === false) { $params['hmac_digest'] = $params['hmackey'] = null; } else { + // make sure we have an HMAC key. WHY? if ( ! isset($params['hmackey'])) { return false; } + // make sure that the digest is supported elseif (isset($params['hmac_digest'])) { $params['hmac_digest'] = strtolower($params['hmac_digest']); @@ -183,20 +192,23 @@ public function getParams($params) } else { + // or else set the default digest $params['hmac_digest'] = 'sha512'; } } + // build the complete set of parameters we ended up with $params = [ - 'handle' => null, - 'cipher' => $params['cipher'], - 'mode' => $params['mode'], - 'key' => $params['key'], - 'base64' => isset($params['raw_data']) ? ! $params['raw_data'] : false, - 'hmac_digest' => $params['hmac_digest'], - 'hmackey' => $params['hmackey'] + 'handle' => null, + 'cipher' => $params['cipher'], + 'mode' => $params['mode'], + 'key' => $params['key'], + 'base64' => isset($params['raw_data']) ? ! $params['raw_data'] : false, + 'hmac_digest' => $params['hmac_digest'], + 'hmackey' => $params['hmackey'] ]; + // and adjust the handle appropriately $this->cipherAlias($params['cipher']); $params['handle'] = ($params['cipher'] !== $this->cipher OR $params['mode'] !== $this->mode) ? $this->getHandle($params['cipher'], $params['mode']) : $this->handle; @@ -236,7 +248,7 @@ public function hkdf($key, $digest = 'sha512', $salt = null, $length = null, $in $prk = hash_hmac($digest, $key, $salt, true); $key = ''; - for ($key_block = '', $block_index = 1; self::strlen($key) < $length; $block_index ++ ) + for ($key_block = '', $block_index = 1; self::strlen($key) < $length; $block_index ++) { $key_block = hash_hmac($digest, $key_block . $info . chr($block_index), $prk, true); $key .= $key_block; @@ -292,11 +304,12 @@ protected static function substr($str, $start, $length = null) public function __get($key) { // Because aliases - if ($key === 'mode') + if (($key === 'mode') && isset($this->modes[$this->handler])) { return array_search($this->mode, $this->modes[$this->handler], true); } - elseif (in_array($key, ['cipher', 'mode', 'key', 'handler', 'handlers', 'digests'], true)) + + if (in_array($key, ['cipher', 'mode', 'key', 'handler', 'handlers', 'digests'], true)) { return $this->{$key}; } diff --git a/system/Encryption/Handlers/OpenSSLHandler.php b/system/Encryption/Handlers/OpenSSLHandler.php index 3470949c4aae..0c953840910b 100644 --- a/system/Encryption/Handlers/OpenSSLHandler.php +++ b/system/Encryption/Handlers/OpenSSLHandler.php @@ -57,7 +57,7 @@ class OpenSSLHandler extends BaseHandler // -------------------------------------------------------------------- /** - * Initialize OpenSSL + * Initialize OpenSSL, remembering parameters * * @param array $params Configuration parameters * @return void @@ -86,6 +86,11 @@ public function __construct($params = null) } } + if ( ! empty($params['key'])) + { + $this->key = $params['key']; + } + if (isset($this->cipher, $this->mode)) { // This is mostly for the stream mode, which doesn't get suffixed in OpenSSL diff --git a/system/Encryption/Handlers/SodiumHandler.php b/system/Encryption/Handlers/SodiumHandler.php index d273d40c5711..7281b560b222 100644 --- a/system/Encryption/Handlers/SodiumHandler.php +++ b/system/Encryption/Handlers/SodiumHandler.php @@ -37,6 +37,14 @@ */ use \Sodium; +/** + * Handler for Sodium cryptography extension. + * + * Conditions: + * - libsodium should be installed on your system + * - the PECL extension should be installed (https://pecl.php.net/package/libsodium) for PHP < 7.2 + * - Paragonie's sodium_compat should handle the situation (https://packagist.org/packages/paragonie/sodium_compat) + */ class SodiumHandler extends BaseHandler { // -------------------------------------------------------------------- @@ -56,7 +64,7 @@ public function __construct($params = null) $this->cipher = 'N/A'; $this->mode = 'N/A'; - if (sodium_init()) + if (function_exists('sodium_init') && sodium_init()) { $this->logger->info('Encryption: Sodium initialized.'); } diff --git a/tests/system/Encryption/EncryptionTest.php b/tests/system/Encryption/EncryptionTest.php index c0b619bb59d2..3ea0e609d902 100644 --- a/tests/system/Encryption/EncryptionTest.php +++ b/tests/system/Encryption/EncryptionTest.php @@ -36,302 +36,57 @@ public function testConstructor() // -------------------------------------------------------------------- /** - * hkdf() test - * - * Applies test vectors described in Appendix A(1-3) RFC5869. - * Described vectors 4-7 SHA-1, which we don't support and are - * therefore excluded. - * - * Because our implementation is a single method instead of being - * split into hkdf_extract() and hkdf_expand(), we cannot test for - * the PRK value. As long as the OKM is correct though, it's fine. - * - * @link https://tools.ietf.org/rfc/rfc5869.txt + * Ensure that the Services will give us an encrypter */ - public function testHkdf() + public function testService() { - $vectors = [ - // A.1: Basic test case with SHA-256 - [ - 'digest' => 'sha256', - 'ikm' => "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b", - 'salt' => "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c", - 'length' => 42, - 'info' => "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9", - // 'prk' => "\x07\x77\x09\x36\x2c\x2e\x32\xdf\x0d\xdc\x3f\x0d\xc4\x7b\xba\x63\x90\xb6\xc7\x3b\xb5\x0f\x9c\x31\x22\xec\x84\x4a\xd7\xc2\xb3\xe5", - 'okm' => "\x3c\xb2\x5f\x25\xfa\xac\xd5\x7a\x90\x43\x4f\x64\xd0\x36\x2f\x2a\x2d\x2d\x0a\x90\xcf\x1a\x5a\x4c\x5d\xb0\x2d\x56\xec\xc4\xc5\xbf\x34\x00\x72\x08\xd5\xb8\x87\x18\x58\x65" - ], - // A.2: Test with SHA-256 and longer inputs/outputs - [ - 'digest' => 'sha256', - 'ikm' => "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f", - 'salt' => "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf", - 'length' => 82, - 'info' => "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff", - // 'prk' => "\x06\xa6\xb8\x8c\x58\x53\x36\x1a\x06\x10\x4c\x9c\xeb\x35\xb4\x5c\xef\x76\x00\x14\x90\x46\x71\x01\x4a\x19\x3f\x40\xc1\x5f\xc2\x44", - 'okm' => "\xb1\x1e\x39\x8d\xc8\x03\x27\xa1\xc8\xe7\xf7\x8c\x59\x6a\x49\x34\x4f\x01\x2e\xda\x2d\x4e\xfa\xd8\xa0\x50\xcc\x4c\x19\xaf\xa9\x7c\x59\x04\x5a\x99\xca\xc7\x82\x72\x71\xcb\x41\xc6\x5e\x59\x0e\x09\xda\x32\x75\x60\x0c\x2f\x09\xb8\x36\x77\x93\xa9\xac\xa3\xdb\x71\xcc\x30\xc5\x81\x79\xec\x3e\x87\xc1\x4c\x01\xd5\xc1\xf3\x43\x4f\x1d\x87", - ], - // A.3: Test with SHA-256 and zero-length salt/info - [ - 'digest' => 'sha256', - 'ikm' => "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b", - 'salt' => '', - 'length' => 42, - 'info' => '', - // 'prk' => "\x19\xef\x24\xa3\x2c\x71\x7b\x16\x7f\x33\xa9\x1d\x6f\x64\x8b\xdf\x96\x59\x67\x76\xaf\xdb\x63\x77\xac\x43\x4c\x1c\x29\x3c\xcb\x04", - 'okm' => "\x8d\xa4\xe7\x75\xa5\x63\xc1\x8f\x71\x5f\x80\x2a\x06\x3c\x5a\x31\xb8\xa1\x1f\x5c\x5e\xe1\x87\x9e\xc3\x45\x4e\x5f\x3c\x73\x8d\x2d\x9d\x20\x13\x95\xfa\xa4\xb6\x1a\x96\xc8", - ] - ]; - - $this->encrypter = \Config\Services::encrypter(); - - foreach ($vectors as $test) - { - $this->assertEquals( - $test['okm'], $this->encrypter->hkdf( - $test['ikm'], $test['digest'], $test['salt'], $test['length'], $test['info'] - ) - ); - } - - // Test default length, it must match the digest size - $hkdf_result = $this->encrypter->hkdf('foobar', 'sha512'); - $this->assertEquals( - 64, defined('MB_OVERLOAD_STRING') ? mb_strlen($hkdf_result, '8bit') : strlen($hkdf_result) - ); - - // Test maximum length (RFC5869 says that it must be up to 255 times the digest size) - $hkdf_result = $this->encrypter->hkdf('foobar', 'sha384', null, 48 * 255); - $this->assertEquals( - 12240, defined('MB_OVERLOAD_STRING') ? mb_strlen($hkdf_result, '8bit') : strlen($hkdf_result) - ); - $this->assertFalse($this->encrypter->hkdf('foobar', 'sha224', null, 28 * 255 + 1)); - - // CI-specific test for an invalid digest - $this->assertFalse($this->encrypter->hkdf('fobar', 'sha1')); - } - - // -------------------------------------------------------------------- - - /** - * _get_params() test - */ - public function testGetParams() - { - $key = str_repeat("\x0", 32); - - // Invalid custom parameters - $params = [ - // No cipher, mode or key - ['cipher' => 'aes-256', 'mode' => 'cbc'], - ['cipher' => 'aes-256', 'key' => $key], - ['mode' => 'cbc', 'key' => $key], - // No HMAC key or not a valid digest - ['cipher' => 'aes-256', 'mode' => 'cbc', 'key' => $key], - ['cipher' => 'aes-256', 'mode' => 'cbc', 'key' => $key, 'hmac_digest' => 'sha1', 'hmac_key' => $key], - // Invalid mode - ['cipher' => 'aes-256', 'mode' => 'foo', 'key' => $key, 'hmac_digest' => 'sha256', 'hmac_key' => $key] - ]; - - $this->encrypter = \Config\Services::encrypter($params); - - for ($i = 0, $c = count($params); $i < $c; $i ++ ) - { - $this->assertFalse($this->encrypter->getParams($params[$i])); - } - - // Valid parameters - $params = [ - 'cipher' => 'aes-256', - 'mode' => 'cbc', - 'key' => str_repeat("\x0", 32), - 'hmac_key' => str_repeat("\x0", 32) - ]; - - $this->assertTrue(is_array($this->encrypter->getParams($params))); - - $params['base64'] = TRUE; - $params['hmac_digest'] = 'sha512'; - - // Including all parameters - $params = [ - 'cipher' => 'aes-256', - 'mode' => 'cbc', - 'key' => str_repeat("\x0", 32), - 'raw_data' => TRUE, - 'hmac_key' => str_repeat("\x0", 32), - 'hmac_digest' => 'sha256' - ]; - - $output = $this->encrypter->getParams($params); - unset($output['handle'], $output['cipher'], $params['raw_data'], $params['cipher']); - $params['base64'] = FALSE; - $this->assertEquals($params, $output); - - // HMAC disabled - unset($params['hmac_key'], $params['hmac_digest']); - $params['hmac'] = $params['raw_data'] = FALSE; - $params['cipher'] = 'aes-256'; - $output = $this->encrypter->getParams($params); - unset($output['handle'], $output['cipher'], $params['hmac'], $params['raw_data'], $params['cipher']); - $params['base64'] = TRUE; - $params['hmac_digest'] = $params['hmac_key'] = null; - $this->assertEquals($params, $output); - } - - // -------------------------------------------------------------------- - - /** - * initialize(), encrypt(), decrypt() test - * - * Testing the three methods separately is not realistic as they are - * designed to work together. - * - * @depends test_hkdf - * @depends test__get_params - */ - public function testInitializeEncryptDecrypt() - { - $message = 'This is a plain-text message.'; - $key = "\xd0\xc9\x08\xc4\xde\x52\x12\x6e\xf8\xcc\xdb\x03\xea\xa0\x3a\x5c"; - $key = $key.$key; - - // Default state (AES-256/Rijndael-256 in CBC mode) - $encrypter = $this->encryption->initialize(array('key' => $key)); - - // Was the key properly set? - $this->assertEquals($key, $encrypter->key); - - $this->assertEquals($message, $encrypter->decrypt($encrypter->encrypt($message))); + // Try with an empty value + $config = new \Config\Encryption(); + $this->encrypt = \Config\Services::encrypter($config); + $this->assertNull($this->encrypt->key); - // Try DES in ECB mode, just for the sake of changing stuff - $encrypter = $this->encryption->initialize(array('cipher' => 'des', 'mode' => 'ecb', 'key' => substr($key, 0, 8))); - $this->assertEquals($message, $encrypter->decrypt($encrypter->encrypt($message))); + $config->key = str_repeat("\x0", 32); + $this->encrypt = \Config\Services::encrypter($config); + $this->assertEquals(str_repeat("\x0", 32), $this->encrypt->key); } // -------------------------------------------------------------------- - /** - * encrypt(), decrypt test with custom parameters - * - * @depends test___get_params + * AES-256 appears to be the only common cipher. + * Let's make sure it works across all our handlers. */ - public function testEncryptDecryptCustom() + public function testPortability() { - $message = 'Another plain-text message.'; - - $encrypter = $this->encryption->initialize(); + $message = 'This is a message encrypted with driver and decrypted using another.'; - // A random invalid parameter - $this->assertFalse($encrypter->encrypt($message, array('foo'))); - $this->assertFalse($encrypter->decrypt($message, array('foo'))); - - // No HMAC, binary output - $params = [ - 'cipher' => 'tripledes', - 'mode' => 'cfb', - 'key' => str_repeat("\x1", 16), - 'base64' => FALSE, - 'hmac' => FALSE + // Format is: , , + $portable = [ + ['aes-256', 'cbc', 32], ]; - - $ciphertext = $encrypter->encrypt($message, $params); - - $this->assertEquals($message, $encrypter->decrypt($ciphertext, $params)); + $handlers = $this->encryption->handlers; + + foreach ($portable as &$test) + foreach ($handlers as $encrypting) + foreach ($handlers as $decrypting) + { + if ($encrypting == $decrypting) + continue; + + $params = [ + 'driver' => $encrypting, + 'cipher' => $test[0], + 'mode' => $test[1], + 'key' => openssl_random_pseudo_bytes($test[2]) + ]; + + $encrypter = $this->encryption->initialize($params); + $ciphertext = $encrypter->encrypt($message); + + $params['driver'] = $decrypting; + + $decrypter = $this->encryption->initialize($params); + $this->assertEquals($message, $decrypter->decrypt($ciphertext), 'From ' . $encrypting . ' to ' . $decrypting); + } } - // -------------------------------------------------------------------- - -// /** -// * Make sure all our supported drivers handle all the portable cipher/mode combinations -// */ -// public function testPortability() -// { -// $message = 'This is a message encrypted via MCrypt and decrypted via OpenSSL, or vice-versa.'; -// -// // Format is: , , -// $portable = [ -// ['aes-128', 'cbc', 16], -// ['aes-128', 'cfb', 16], -// ['aes-128', 'cfb8', 16], -// ['aes-128', 'ofb', 16], -// ['aes-128', 'ecb', 16], -// ['aes-128', 'ctr', 16], -// ['aes-192', 'cbc', 24], -// ['aes-192', 'cfb', 24], -// ['aes-192', 'cfb8', 24], -// ['aes-192', 'ofb', 24], -// ['aes-192', 'ecb', 24], -// ['aes-192', 'ctr', 24], -// ['aes-256', 'cbc', 32], -// ['aes-256', 'cfb', 32], -// ['aes-256', 'cfb8', 32], -// ['aes-256', 'ofb', 32], -// ['aes-256', 'ecb', 32], -// ['aes-256', 'ctr', 32], -// ['des', 'cbc', 7], -// ['des', 'cfb', 7], -// ['des', 'cfb8', 7], -// ['des', 'ofb', 7], -// ['des', 'ecb', 7], -// ['tripledes', 'cbc', 7], -// ['tripledes', 'cfb', 7], -// ['tripledes', 'cfb8', 7], -// ['tripledes', 'ofb', 7], -// ['tripledes', 'cbc', 14], -// ['tripledes', 'cfb', 14], -// ['tripledes', 'cfb8', 14], -// ['tripledes', 'ofb', 14], -// ['tripledes', 'cbc', 21], -// ['tripledes', 'cfb', 21], -// ['tripledes', 'cfb8', 21], -// ['tripledes', 'ofb', 21], -// ['blowfish', 'cbc', 16], -// ['blowfish', 'cfb', 16], -// ['blowfish', 'ofb', 16], -// ['blowfish', 'ecb', 16], -// ['blowfish', 'cbc', 56], -// ['blowfish', 'cfb', 56], -// ['blowfish', 'ofb', 56], -// ['blowfish', 'ecb', 56], -// ['cast5', 'cbc', 11], -// ['cast5', 'cfb', 11], -// ['cast5', 'ofb', 11], -// ['cast5', 'ecb', 11], -// ['cast5', 'cbc', 16], -// ['cast5', 'cfb', 16], -// ['cast5', 'ofb', 16], -// ['cast5', 'ecb', 16], -// ['rc4', 'stream', 5], -// ['rc4', 'stream', 8], -// ['rc4', 'stream', 16], -// ['rc4', 'stream', 32], -// ['rc4', 'stream', 64], -// ['rc4', 'stream', 128], -// ['rc4', 'stream', 256] -// ]; -// $handler_index = ['openssl', 'libsodium']; -// -// foreach ($portable as &$test) -// { -// // Add some randomness to the selected handler -// $handler = mt_rand(0, sizeof($handler_index) - 1); -// $params = [ -// 'driver' => $handler_index[$handler], -// 'cipher' => $test[0], -// 'mode' => $test[1], -// 'key' => openssl_random_pseudo_bytes($test[2]) -// ]; -// -// $encrypter = $this->encryption->initialize($params); -// $ciphertext = $encrypter->encrypt($message); -// -// $handler = mt_rand(0, sizeof($handler_index) - 1); -// $params['driver'] = $handler_index[$handler]; -// -// $encrypter = $this->encryption->initialize($params); -// $this->assertEquals($message, $encrypter->decrypt($ciphertext)); -// } -// } - } diff --git a/tests/system/Encryption/OpenSSLHandlerTest.php b/tests/system/Encryption/OpenSSLHandlerTest.php new file mode 100644 index 000000000000..cde906bd81cb --- /dev/null +++ b/tests/system/Encryption/OpenSSLHandlerTest.php @@ -0,0 +1,234 @@ +encryption = new \CodeIgniter\Encryption\Encryption(); + $this->encrypter = $this->encryption->initialize(['driver' => 'OpenSSL','key'=>'Something other than an empty string']); + } + + // -------------------------------------------------------------------- + + /** + * Sanity test + */ + public function testSanity() { + $this->assertEquals('aes-256',$this->encrypter->cipher); + $this->assertEquals('cbc',$this->encrypter->mode); + $this->assertEquals('Something other than an empty string',$this->encrypter->key); + $this->assertNull($this->encrypter->handle); + } + + // -------------------------------------------------------------------- + + /** + * hkdf() test + * + * Applies test vectors described in Appendix A(1-3) RFC5869. + * Described vectors 4-7 SHA-1, which we don't support and are + * therefore excluded. + * + * Because our implementation is a single method instead of being + * split into hkdf_extract() and hkdf_expand(), we cannot test for + * the PRK value. As long as the OKM is correct though, it's fine. + * + * @link https://tools.ietf.org/rfc/rfc5869.txt + */ + public function testHkdf() + { + $vectors = [ + // A.1: Basic test case with SHA-256 + [ + 'digest' => 'sha256', + 'ikm' => "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b", + 'salt' => "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c", + 'length' => 42, + 'info' => "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9", + // 'prk' => "\x07\x77\x09\x36\x2c\x2e\x32\xdf\x0d\xdc\x3f\x0d\xc4\x7b\xba\x63\x90\xb6\xc7\x3b\xb5\x0f\x9c\x31\x22\xec\x84\x4a\xd7\xc2\xb3\xe5", + 'okm' => "\x3c\xb2\x5f\x25\xfa\xac\xd5\x7a\x90\x43\x4f\x64\xd0\x36\x2f\x2a\x2d\x2d\x0a\x90\xcf\x1a\x5a\x4c\x5d\xb0\x2d\x56\xec\xc4\xc5\xbf\x34\x00\x72\x08\xd5\xb8\x87\x18\x58\x65" + ], + // A.2: Test with SHA-256 and longer inputs/outputs + [ + 'digest' => 'sha256', + 'ikm' => "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f", + 'salt' => "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf", + 'length' => 82, + 'info' => "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff", + // 'prk' => "\x06\xa6\xb8\x8c\x58\x53\x36\x1a\x06\x10\x4c\x9c\xeb\x35\xb4\x5c\xef\x76\x00\x14\x90\x46\x71\x01\x4a\x19\x3f\x40\xc1\x5f\xc2\x44", + 'okm' => "\xb1\x1e\x39\x8d\xc8\x03\x27\xa1\xc8\xe7\xf7\x8c\x59\x6a\x49\x34\x4f\x01\x2e\xda\x2d\x4e\xfa\xd8\xa0\x50\xcc\x4c\x19\xaf\xa9\x7c\x59\x04\x5a\x99\xca\xc7\x82\x72\x71\xcb\x41\xc6\x5e\x59\x0e\x09\xda\x32\x75\x60\x0c\x2f\x09\xb8\x36\x77\x93\xa9\xac\xa3\xdb\x71\xcc\x30\xc5\x81\x79\xec\x3e\x87\xc1\x4c\x01\xd5\xc1\xf3\x43\x4f\x1d\x87", + ], + // A.3: Test with SHA-256 and zero-length salt/info + [ + 'digest' => 'sha256', + 'ikm' => "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b", + 'salt' => '', + 'length' => 42, + 'info' => '', + // 'prk' => "\x19\xef\x24\xa3\x2c\x71\x7b\x16\x7f\x33\xa9\x1d\x6f\x64\x8b\xdf\x96\x59\x67\x76\xaf\xdb\x63\x77\xac\x43\x4c\x1c\x29\x3c\xcb\x04", + 'okm' => "\x8d\xa4\xe7\x75\xa5\x63\xc1\x8f\x71\x5f\x80\x2a\x06\x3c\x5a\x31\xb8\xa1\x1f\x5c\x5e\xe1\x87\x9e\xc3\x45\x4e\x5f\x3c\x73\x8d\x2d\x9d\x20\x13\x95\xfa\xa4\xb6\x1a\x96\xc8", + ] + ]; + + foreach ($vectors as $test) + { + $this->assertEquals( + $test['okm'], $this->encrypter->hkdf( + $test['ikm'], $test['digest'], $test['salt'], $test['length'], $test['info'] + ) + ); + } + + // Test default length, it must match the digest size + $hkdf_result = $this->encrypter->hkdf('foobar', 'sha512'); + $this->assertEquals( + 64, defined('MB_OVERLOAD_STRING') ? mb_strlen($hkdf_result, '8bit') : strlen($hkdf_result) + ); + + // Test maximum length (RFC5869 says that it must be up to 255 times the digest size) + $hkdf_result = $this->encrypter->hkdf('foobar', 'sha384', null, 48 * 255); + $this->assertEquals( + 12240, defined('MB_OVERLOAD_STRING') ? mb_strlen($hkdf_result, '8bit') : strlen($hkdf_result) + ); + $this->assertFalse($this->encrypter->hkdf('foobar', 'sha224', null, 28 * 255 + 1)); + + // CI-specific test for an invalid digest + $this->assertFalse($this->encrypter->hkdf('fobar', 'sha1')); + } + + // -------------------------------------------------------------------- + + /** + * _get_params() test + */ + public function testGetParams() + { + // make sure we don't actually need parameters + $this->assertTrue(is_array($this->encrypter->getParams([]))); + + $key = str_repeat("\x0", 32); + + // Invalid custom parameters + $params = [ + // No cipher, mode or key + ['cipher' => 'aes-256', 'mode' => 'cbc'], + ['cipher' => 'aes-256', 'key' => $key], + ['mode' => 'cbc', 'key' => $key], + // No HMAC key or not a valid digest + ['cipher' => 'aes-256', 'mode' => 'cbc', 'key' => $key], + ['cipher' => 'aes-256', 'mode' => 'cbc', 'key' => $key, 'hmac_digest' => 'sha1', 'hmac_key' => $key], + // Invalid mode + ['cipher' => 'aes-256', 'mode' => 'foo', 'key' => $key, 'hmac_digest' => 'sha256', 'hmac_key' => $key] + ]; + + for ($i = 0, $c = count($params); $i < $c; $i ++ ) + { + $this->assertFalse($this->encrypter->getParams($params[$i])); + } + + // Valid parameters + $params = [ + 'cipher' => 'aes-256', + 'mode' => 'cbc', + 'key' => str_repeat("\x0", 32), + 'hmac_key' => str_repeat("\x0", 32) + ]; + + $this->assertTrue(is_array($this->encrypter->getParams($params))); + + // why do we have the next 2 lines? + $params['base64'] = TRUE; + $params['hmac_digest'] = 'sha512'; + + // Including all parameters + $params = [ + 'cipher' => 'aes-256', + 'mode' => 'cbc', + 'key' => str_repeat("\x0", 32), + 'raw_data' => TRUE, + 'hmac_key' => str_repeat("\x0", 32), + 'hmac_digest' => 'sha256' + ]; + + $output = $this->encrypter->getParams($params); + unset($output['handle'], $output['cipher'], $params['raw_data'], $params['cipher']); + $params['base64'] = FALSE; + $this->assertEquals($params, $output); + + // HMAC disabled + unset($params['hmac_key'], $params['hmac_digest']); + $params['hmac'] = $params['raw_data'] = FALSE; + $params['cipher'] = 'aes-256'; + $output = $this->encrypter->getParams($params); + unset($output['handle'], $output['cipher'], $params['hmac'], $params['raw_data'], $params['cipher']); + $params['base64'] = TRUE; + $params['hmac_digest'] = $params['hmac_key'] = null; + $this->assertEquals($params, $output); + } + + // -------------------------------------------------------------------- + + /** + * initialize(), encrypt(), decrypt() test + * + * Testing the three methods separately is not realistic as they are + * designed to work together. + * + * @depends test_hkdf + * @depends test__get_params + */ + public function testInitializeEncryptDecrypt() + { + $message = 'This is a plain-text message.'; + $key = "\xd0\xc9\x08\xc4\xde\x52\x12\x6e\xf8\xcc\xdb\x03\xea\xa0\x3a\x5c"; + $key = $key . $key; + + // Default state (AES-256/Rijndael-256 in CBC mode) + $encrypter = $this->encryption->initialize(array('key' => $key)); + + // Was the key properly set? + $this->assertEquals($key, $encrypter->key); + + $this->assertEquals($message, $encrypter->decrypt($encrypter->encrypt($message))); + + // Try DES in ECB mode, just for the sake of changing stuff + $encrypter = $this->encryption->initialize(array('cipher' => 'des', 'mode' => 'ecb', 'key' => substr($key, 0, 8))); + $this->assertEquals($message, $encrypter->decrypt($encrypter->encrypt($message))); + } + + // -------------------------------------------------------------------- + + /** + * encrypt(), decrypt test with custom parameters + * + * @depends test___get_params + */ + public function testEncryptDecryptCustom() + { + $message = 'Another plain-text message.'; + + $encrypter = $this->encryption->initialize(); + + // A random invalid parameter + $this->assertFalse($encrypter->encrypt($message, array('foo'))); + $this->assertFalse($encrypter->decrypt($message, array('foo'))); + + // No HMAC, binary output + $params = [ + 'cipher' => 'tripledes', + 'mode' => 'cfb', + 'key' => str_repeat("\x1", 16), + 'base64' => FALSE, + 'hmac' => FALSE + ]; + + $ciphertext = $encrypter->encrypt($message, $params); + + $this->assertEquals($message, $encrypter->decrypt($ciphertext, $params)); + } + +} diff --git a/tests/system/Encryption/SodiumHandlerTest.php b/tests/system/Encryption/SodiumHandlerTest.php new file mode 100644 index 000000000000..6cfd8eaf8079 --- /dev/null +++ b/tests/system/Encryption/SodiumHandlerTest.php @@ -0,0 +1,229 @@ +encryption = new \CodeIgniter\Encryption\Encryption(); + $this->encrypter = $this->encryption->initialize(['driver' => 'Sodium','key'=>'Something other than an empty string']); + } + + // -------------------------------------------------------------------- + + /** + * Sanity test + */ + public function testSanity() { + $this->assertEquals('aes-256',$this->encrypter->cipher); + $this->assertEquals('cbc',$this->encrypter->mode); + $this->assertEquals('Something other than an empty string',$this->encrypter->key); + $this->assertNull($this->encrypter->handle); + } + + // -------------------------------------------------------------------- + + /** + * hkdf() test + * + * Applies test vectors described in Appendix A(1-3) RFC5869. + * Described vectors 4-7 SHA-1, which we don't support and are + * therefore excluded. + * + * Because our implementation is a single method instead of being + * split into hkdf_extract() and hkdf_expand(), we cannot test for + * the PRK value. As long as the OKM is correct though, it's fine. + * + * @link https://tools.ietf.org/rfc/rfc5869.txt + */ + public function testHkdf() + { + $vectors = [ + // A.1: Basic test case with SHA-256 + [ + 'digest' => 'sha256', + 'ikm' => "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b", + 'salt' => "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c", + 'length' => 42, + 'info' => "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9", + // 'prk' => "\x07\x77\x09\x36\x2c\x2e\x32\xdf\x0d\xdc\x3f\x0d\xc4\x7b\xba\x63\x90\xb6\xc7\x3b\xb5\x0f\x9c\x31\x22\xec\x84\x4a\xd7\xc2\xb3\xe5", + 'okm' => "\x3c\xb2\x5f\x25\xfa\xac\xd5\x7a\x90\x43\x4f\x64\xd0\x36\x2f\x2a\x2d\x2d\x0a\x90\xcf\x1a\x5a\x4c\x5d\xb0\x2d\x56\xec\xc4\xc5\xbf\x34\x00\x72\x08\xd5\xb8\x87\x18\x58\x65" + ], + // A.2: Test with SHA-256 and longer inputs/outputs + [ + 'digest' => 'sha256', + 'ikm' => "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f", + 'salt' => "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf", + 'length' => 82, + 'info' => "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff", + // 'prk' => "\x06\xa6\xb8\x8c\x58\x53\x36\x1a\x06\x10\x4c\x9c\xeb\x35\xb4\x5c\xef\x76\x00\x14\x90\x46\x71\x01\x4a\x19\x3f\x40\xc1\x5f\xc2\x44", + 'okm' => "\xb1\x1e\x39\x8d\xc8\x03\x27\xa1\xc8\xe7\xf7\x8c\x59\x6a\x49\x34\x4f\x01\x2e\xda\x2d\x4e\xfa\xd8\xa0\x50\xcc\x4c\x19\xaf\xa9\x7c\x59\x04\x5a\x99\xca\xc7\x82\x72\x71\xcb\x41\xc6\x5e\x59\x0e\x09\xda\x32\x75\x60\x0c\x2f\x09\xb8\x36\x77\x93\xa9\xac\xa3\xdb\x71\xcc\x30\xc5\x81\x79\xec\x3e\x87\xc1\x4c\x01\xd5\xc1\xf3\x43\x4f\x1d\x87", + ], + // A.3: Test with SHA-256 and zero-length salt/info + [ + 'digest' => 'sha256', + 'ikm' => "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b", + 'salt' => '', + 'length' => 42, + 'info' => '', + // 'prk' => "\x19\xef\x24\xa3\x2c\x71\x7b\x16\x7f\x33\xa9\x1d\x6f\x64\x8b\xdf\x96\x59\x67\x76\xaf\xdb\x63\x77\xac\x43\x4c\x1c\x29\x3c\xcb\x04", + 'okm' => "\x8d\xa4\xe7\x75\xa5\x63\xc1\x8f\x71\x5f\x80\x2a\x06\x3c\x5a\x31\xb8\xa1\x1f\x5c\x5e\xe1\x87\x9e\xc3\x45\x4e\x5f\x3c\x73\x8d\x2d\x9d\x20\x13\x95\xfa\xa4\xb6\x1a\x96\xc8", + ] + ]; + + foreach ($vectors as $test) + { + $this->assertEquals( + $test['okm'], $this->encrypter->hkdf( + $test['ikm'], $test['digest'], $test['salt'], $test['length'], $test['info'] + ) + ); + } + + // Test default length, it must match the digest size + $hkdf_result = $this->encrypter->hkdf('foobar', 'sha512'); + $this->assertEquals( + 64, defined('MB_OVERLOAD_STRING') ? mb_strlen($hkdf_result, '8bit') : strlen($hkdf_result) + ); + + // Test maximum length (RFC5869 says that it must be up to 255 times the digest size) + $hkdf_result = $this->encrypter->hkdf('foobar', 'sha384', null, 48 * 255); + $this->assertEquals( + 12240, defined('MB_OVERLOAD_STRING') ? mb_strlen($hkdf_result, '8bit') : strlen($hkdf_result) + ); + $this->assertFalse($this->encrypter->hkdf('foobar', 'sha224', null, 28 * 255 + 1)); + + // CI-specific test for an invalid digest + $this->assertFalse($this->encrypter->hkdf('fobar', 'sha1')); + } + + // -------------------------------------------------------------------- + + /** + * _get_params() test + */ + public function testGetParams() + { + // make sure we don't actually need parameters + $this->assertTrue(is_array($this->encrypter->getParams([]))); + + $key = str_repeat("\x0", 32); + + // Invalid custom parameters + $params = [ + // No cipher, mode or key + ['cipher' => 'aes-256', 'mode' => 'cbc'], + ['cipher' => 'aes-256', 'key' => $key], + ['mode' => 'cbc', 'key' => $key], + // No HMAC key or not a valid digest + ['cipher' => 'aes-256', 'mode' => 'cbc', 'key' => $key], + ['cipher' => 'aes-256', 'mode' => 'cbc', 'key' => $key, 'hmac_digest' => 'sha1', 'hmac_key' => $key], + // Invalid mode + ['cipher' => 'aes-256', 'mode' => 'foo', 'key' => $key, 'hmac_digest' => 'sha256', 'hmac_key' => $key] + ]; + + for ($i = 0, $c = count($params); $i < $c; $i ++ ) + { + $this->assertFalse($this->encrypter->getParams($params[$i])); + } + + // Valid parameters + $params = [ + 'cipher' => 'aes-256', + 'mode' => 'cbc', + 'key' => str_repeat("\x0", 32), + 'hmac_key' => str_repeat("\x0", 32) + ]; + + $this->assertTrue(is_array($this->encrypter->getParams($params))); + + // why do we have the next 2 lines? + $params['base64'] = TRUE; + $params['hmac_digest'] = 'sha512'; + + // Including all parameters + $params = [ + 'cipher' => 'aes-256', + 'mode' => 'cbc', + 'key' => str_repeat("\x0", 32), + 'raw_data' => TRUE, + 'hmac_key' => str_repeat("\x0", 32), + 'hmac_digest' => 'sha256' + ]; + + $output = $this->encrypter->getParams($params); + unset($output['handle'], $output['cipher'], $params['raw_data'], $params['cipher']); + $params['base64'] = FALSE; + $this->assertEquals($params, $output); + + // HMAC disabled + unset($params['hmac_key'], $params['hmac_digest']); + $params['hmac'] = $params['raw_data'] = FALSE; + $params['cipher'] = 'aes-256'; + $output = $this->encrypter->getParams($params); + unset($output['handle'], $output['cipher'], $params['hmac'], $params['raw_data'], $params['cipher']); + $params['base64'] = TRUE; + $params['hmac_digest'] = $params['hmac_key'] = null; + $this->assertEquals($params, $output); + } + + // -------------------------------------------------------------------- + + /** + * initialize(), encrypt(), decrypt() test + * + * Testing the three methods separately is not realistic as they are + * designed to work together. + */ + public function testInitializeEncryptDecrypt() + { + $message = 'This is a plain-text message.'; + $key = "\xd0\xc9\x08\xc4\xde\x52\x12\x6e\xf8\xcc\xdb\x03\xea\xa0\x3a\x5c"; + $key = $key . $key; + + // Default state (AES-256/Rijndael-256 in CBC mode) + $encrypter = $this->encryption->initialize(array('key' => $key)); + + // Was the key properly set? + $this->assertEquals($key, $encrypter->key); + + $this->assertEquals($message, $encrypter->decrypt($encrypter->encrypt($message))); + + // Try DES in ECB mode, just for the sake of changing stuff + $encrypter = $this->encryption->initialize(array('cipher' => 'des', 'mode' => 'ecb', 'key' => substr($key, 0, 8))); + $this->assertEquals($message, $encrypter->decrypt($encrypter->encrypt($message))); + } + + // -------------------------------------------------------------------- + + /** + * encrypt(), decrypt test with custom parameters + */ + public function testEncryptDecryptCustom() + { + $message = 'Another plain-text message.'; + + $encrypter = $this->encryption->initialize(); + + // A random invalid parameter + $this->assertFalse($encrypter->encrypt($message, array('foo'))); + $this->assertFalse($encrypter->decrypt($message, array('foo'))); + + // No HMAC, binary output + $params = [ + 'cipher' => 'tripledes', + 'mode' => 'cfb', + 'key' => str_repeat("\x1", 16), + 'base64' => FALSE, + 'hmac' => FALSE + ]; + + $ciphertext = $encrypter->encrypt($message, $params); + + $this->assertEquals($message, $encrypter->decrypt($ciphertext, $params)); + } + +} From 269dacfe65bffcdd69616ae3db2147157c517f27 Mon Sep 17 00:00:00 2001 From: Master Yoda Date: Tue, 11 Jul 2017 23:06:15 -0700 Subject: [PATCH 16/18] Defer SodiumHandler for now --- system/Encryption/Encryption.php | 2 - system/Encryption/Handlers/BaseHandler.php | 8 +- system/Encryption/Handlers/OpenSSLHandler.php | 2 +- system/Encryption/Handlers/SodiumHandler.php | 144 ----------- tests/system/Encryption/EncryptionTest.php | 77 +++--- tests/system/Encryption/SodiumHandlerTest.php | 229 ------------------ .../source/libraries/encryption.rst | 12 - 7 files changed, 44 insertions(+), 430 deletions(-) delete mode 100644 system/Encryption/Handlers/SodiumHandler.php delete mode 100644 tests/system/Encryption/SodiumHandlerTest.php diff --git a/system/Encryption/Encryption.php b/system/Encryption/Encryption.php index bebba7566d6f..ef2dd085130c 100644 --- a/system/Encryption/Encryption.php +++ b/system/Encryption/Encryption.php @@ -91,7 +91,6 @@ class Encryption */ protected $drivers = [ 'openssl' => 'OpenSSL', - 'libsodium' => 'Sodium' ]; /** @@ -152,7 +151,6 @@ public function __construct($params = []) // determine what is installed $this->handlers = [ 'OpenSSL' => extension_loaded('openssl'), - 'Sodium' => extension_loaded('libsodium') ]; if ( ! $this->handlers['OpenSSL']) diff --git a/system/Encryption/Handlers/BaseHandler.php b/system/Encryption/Handlers/BaseHandler.php index 603676389fd9..76c34c6b4c8f 100644 --- a/system/Encryption/Handlers/BaseHandler.php +++ b/system/Encryption/Handlers/BaseHandler.php @@ -151,7 +151,7 @@ public function getParams($params) 'key' => null, 'base64' => true, 'hmac_digest' => 'sha512', - 'hmackey' => null + 'hmac_key' => null ] : false; } @@ -172,12 +172,12 @@ public function getParams($params) // if the HMAC parameter is false, zap the HMAC digest & key if (isset($params['hmac']) && $params['hmac'] === false) { - $params['hmac_digest'] = $params['hmackey'] = null; + $params['hmac_digest'] = $params['hmac_key'] = null; } else { // make sure we have an HMAC key. WHY? - if ( ! isset($params['hmackey'])) + if ( ! isset($params['hmac_key'])) { return false; } @@ -205,7 +205,7 @@ public function getParams($params) 'key' => $params['key'], 'base64' => isset($params['raw_data']) ? ! $params['raw_data'] : false, 'hmac_digest' => $params['hmac_digest'], - 'hmackey' => $params['hmackey'] + 'hmac_key' => $params['hmac_key'] ]; // and adjust the handle appropriately diff --git a/system/Encryption/Handlers/OpenSSLHandler.php b/system/Encryption/Handlers/OpenSSLHandler.php index 0c953840910b..7fe467d9378a 100644 --- a/system/Encryption/Handlers/OpenSSLHandler.php +++ b/system/Encryption/Handlers/OpenSSLHandler.php @@ -123,7 +123,7 @@ public function encrypt($data, array $params = null) return false; } - $iv = ($iv_size = opensslcipher_iv_length($params['cipher'])) ? $this->createKey($iv_size) : null; + $iv = ($iv_size = openssl_cipher_iv_length($params['cipher'])) ? $this->createKey($iv_size) : null; $data = openssl_encrypt( $data, $params['cipher'], $params['key'], OPENSSL_RAW_DATA, $iv diff --git a/system/Encryption/Handlers/SodiumHandler.php b/system/Encryption/Handlers/SodiumHandler.php deleted file mode 100644 index 7281b560b222..000000000000 --- a/system/Encryption/Handlers/SodiumHandler.php +++ /dev/null @@ -1,144 +0,0 @@ -cipher = 'N/A'; - $this->mode = 'N/A'; - - if (function_exists('sodium_init') && sodium_init()) - { - $this->logger->info('Encryption: Sodium initialized.'); - } - else - { - $this->logger->error('Encryption: Unable to initialize Sodium.'); - } - } - - /** - * Encrypt - * - * @param string $data Input data - * @param array $params Input parameters - * @return string - */ - public function encrypt($data, array $params = null) - { - // allow key to be over-ridden - $key = empty($params['key']) ? $this->key : $params['key']; - - $nonce = randombytes_buf(CRYPTO_SECRETBOX_NONCEBYTES); - - $ciphertext = crypto_secretbox($data, $nonce, $key); - - if ($ciphertext === false) - { - return false; - } - - return $nonce . $ciphertext; - } - - // -------------------------------------------------------------------- - - /** - * Decrypt - * - * @param string $data Encrypted data, with nonce pre-fixed - * @param array $params Input parameters - * @return string - */ - public function decrypt($data, array $params = null) - { - // allow key to be over-ridden - $key = empty($params['key']) ? $this->key : $params['key']; - - // split the data into nonce & ciphertext - $nonce = self::substr($data, 0, CRYPTO_SECRETBOX_NONCEBYTES); - $data = self::substr($data, CRYPTO_SECRETBOX_NONCEBYTES); - - $plaintext = crypto_secretbox_open($data, $nonce, $key); - - if ($plaintext === false) - { - throw new EncryptionException("Bad ciphertext"); - } - - return $plaintext; - } - - // -------------------------------------------------------------------- - - /** - * Cipher alias - * - * Tries to translate cipher names as appropriate for this handler - * - * @param string $cipher Cipher name - * @return void - */ - protected function cipherAlias(&$cipher) - { - $cipher = 'N/A'; - } - -} diff --git a/tests/system/Encryption/EncryptionTest.php b/tests/system/Encryption/EncryptionTest.php index 3ea0e609d902..594ad3708d94 100644 --- a/tests/system/Encryption/EncryptionTest.php +++ b/tests/system/Encryption/EncryptionTest.php @@ -50,43 +50,44 @@ public function testService() $this->assertEquals(str_repeat("\x0", 32), $this->encrypt->key); } - // -------------------------------------------------------------------- - /** - * AES-256 appears to be the only common cipher. - * Let's make sure it works across all our handlers. - */ - public function testPortability() - { - $message = 'This is a message encrypted with driver and decrypted using another.'; - - // Format is: , , - $portable = [ - ['aes-256', 'cbc', 32], - ]; - $handlers = $this->encryption->handlers; - - foreach ($portable as &$test) - foreach ($handlers as $encrypting) - foreach ($handlers as $decrypting) - { - if ($encrypting == $decrypting) - continue; - - $params = [ - 'driver' => $encrypting, - 'cipher' => $test[0], - 'mode' => $test[1], - 'key' => openssl_random_pseudo_bytes($test[2]) - ]; - - $encrypter = $this->encryption->initialize($params); - $ciphertext = $encrypter->encrypt($message); - - $params['driver'] = $decrypting; - - $decrypter = $this->encryption->initialize($params); - $this->assertEquals($message, $decrypter->decrypt($ciphertext), 'From ' . $encrypting . ' to ' . $decrypting); - } - } +// // -------------------------------------------------------------------- +// //FIXME We need more than one handler in order to include this test +// /** +// * AES-256 appears to be the only common cipher. +// * Let's make sure it works across all our handlers. +// */ +// public function testPortability() +// { +// $message = 'This is a message encrypted with driver and decrypted using another.'; +// +// // Format is: , , +// $portable = [ +// ['aes-256', 'cbc', 32], +// ]; +// $handlers = $this->encryption->handlers; +// +// foreach ($portable as &$test) +// foreach ($handlers as $encrypting) +// foreach ($handlers as $decrypting) +// { +// if ($encrypting == $decrypting) +// continue; +// +// $params = [ +// 'driver' => $encrypting, +// 'cipher' => $test[0], +// 'mode' => $test[1], +// 'key' => openssl_random_pseudo_bytes($test[2]) +// ]; +// +// $encrypter = $this->encryption->initialize($params); +// $ciphertext = $encrypter->encrypt($message); +// +// $params['driver'] = $decrypting; +// +// $decrypter = $this->encryption->initialize($params); +// $this->assertEquals($message, $decrypter->decrypt($ciphertext), 'From ' . $encrypting . ' to ' . $decrypting); +// } +// } } diff --git a/tests/system/Encryption/SodiumHandlerTest.php b/tests/system/Encryption/SodiumHandlerTest.php deleted file mode 100644 index 6cfd8eaf8079..000000000000 --- a/tests/system/Encryption/SodiumHandlerTest.php +++ /dev/null @@ -1,229 +0,0 @@ -encryption = new \CodeIgniter\Encryption\Encryption(); - $this->encrypter = $this->encryption->initialize(['driver' => 'Sodium','key'=>'Something other than an empty string']); - } - - // -------------------------------------------------------------------- - - /** - * Sanity test - */ - public function testSanity() { - $this->assertEquals('aes-256',$this->encrypter->cipher); - $this->assertEquals('cbc',$this->encrypter->mode); - $this->assertEquals('Something other than an empty string',$this->encrypter->key); - $this->assertNull($this->encrypter->handle); - } - - // -------------------------------------------------------------------- - - /** - * hkdf() test - * - * Applies test vectors described in Appendix A(1-3) RFC5869. - * Described vectors 4-7 SHA-1, which we don't support and are - * therefore excluded. - * - * Because our implementation is a single method instead of being - * split into hkdf_extract() and hkdf_expand(), we cannot test for - * the PRK value. As long as the OKM is correct though, it's fine. - * - * @link https://tools.ietf.org/rfc/rfc5869.txt - */ - public function testHkdf() - { - $vectors = [ - // A.1: Basic test case with SHA-256 - [ - 'digest' => 'sha256', - 'ikm' => "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b", - 'salt' => "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c", - 'length' => 42, - 'info' => "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9", - // 'prk' => "\x07\x77\x09\x36\x2c\x2e\x32\xdf\x0d\xdc\x3f\x0d\xc4\x7b\xba\x63\x90\xb6\xc7\x3b\xb5\x0f\x9c\x31\x22\xec\x84\x4a\xd7\xc2\xb3\xe5", - 'okm' => "\x3c\xb2\x5f\x25\xfa\xac\xd5\x7a\x90\x43\x4f\x64\xd0\x36\x2f\x2a\x2d\x2d\x0a\x90\xcf\x1a\x5a\x4c\x5d\xb0\x2d\x56\xec\xc4\xc5\xbf\x34\x00\x72\x08\xd5\xb8\x87\x18\x58\x65" - ], - // A.2: Test with SHA-256 and longer inputs/outputs - [ - 'digest' => 'sha256', - 'ikm' => "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f", - 'salt' => "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf", - 'length' => 82, - 'info' => "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff", - // 'prk' => "\x06\xa6\xb8\x8c\x58\x53\x36\x1a\x06\x10\x4c\x9c\xeb\x35\xb4\x5c\xef\x76\x00\x14\x90\x46\x71\x01\x4a\x19\x3f\x40\xc1\x5f\xc2\x44", - 'okm' => "\xb1\x1e\x39\x8d\xc8\x03\x27\xa1\xc8\xe7\xf7\x8c\x59\x6a\x49\x34\x4f\x01\x2e\xda\x2d\x4e\xfa\xd8\xa0\x50\xcc\x4c\x19\xaf\xa9\x7c\x59\x04\x5a\x99\xca\xc7\x82\x72\x71\xcb\x41\xc6\x5e\x59\x0e\x09\xda\x32\x75\x60\x0c\x2f\x09\xb8\x36\x77\x93\xa9\xac\xa3\xdb\x71\xcc\x30\xc5\x81\x79\xec\x3e\x87\xc1\x4c\x01\xd5\xc1\xf3\x43\x4f\x1d\x87", - ], - // A.3: Test with SHA-256 and zero-length salt/info - [ - 'digest' => 'sha256', - 'ikm' => "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b", - 'salt' => '', - 'length' => 42, - 'info' => '', - // 'prk' => "\x19\xef\x24\xa3\x2c\x71\x7b\x16\x7f\x33\xa9\x1d\x6f\x64\x8b\xdf\x96\x59\x67\x76\xaf\xdb\x63\x77\xac\x43\x4c\x1c\x29\x3c\xcb\x04", - 'okm' => "\x8d\xa4\xe7\x75\xa5\x63\xc1\x8f\x71\x5f\x80\x2a\x06\x3c\x5a\x31\xb8\xa1\x1f\x5c\x5e\xe1\x87\x9e\xc3\x45\x4e\x5f\x3c\x73\x8d\x2d\x9d\x20\x13\x95\xfa\xa4\xb6\x1a\x96\xc8", - ] - ]; - - foreach ($vectors as $test) - { - $this->assertEquals( - $test['okm'], $this->encrypter->hkdf( - $test['ikm'], $test['digest'], $test['salt'], $test['length'], $test['info'] - ) - ); - } - - // Test default length, it must match the digest size - $hkdf_result = $this->encrypter->hkdf('foobar', 'sha512'); - $this->assertEquals( - 64, defined('MB_OVERLOAD_STRING') ? mb_strlen($hkdf_result, '8bit') : strlen($hkdf_result) - ); - - // Test maximum length (RFC5869 says that it must be up to 255 times the digest size) - $hkdf_result = $this->encrypter->hkdf('foobar', 'sha384', null, 48 * 255); - $this->assertEquals( - 12240, defined('MB_OVERLOAD_STRING') ? mb_strlen($hkdf_result, '8bit') : strlen($hkdf_result) - ); - $this->assertFalse($this->encrypter->hkdf('foobar', 'sha224', null, 28 * 255 + 1)); - - // CI-specific test for an invalid digest - $this->assertFalse($this->encrypter->hkdf('fobar', 'sha1')); - } - - // -------------------------------------------------------------------- - - /** - * _get_params() test - */ - public function testGetParams() - { - // make sure we don't actually need parameters - $this->assertTrue(is_array($this->encrypter->getParams([]))); - - $key = str_repeat("\x0", 32); - - // Invalid custom parameters - $params = [ - // No cipher, mode or key - ['cipher' => 'aes-256', 'mode' => 'cbc'], - ['cipher' => 'aes-256', 'key' => $key], - ['mode' => 'cbc', 'key' => $key], - // No HMAC key or not a valid digest - ['cipher' => 'aes-256', 'mode' => 'cbc', 'key' => $key], - ['cipher' => 'aes-256', 'mode' => 'cbc', 'key' => $key, 'hmac_digest' => 'sha1', 'hmac_key' => $key], - // Invalid mode - ['cipher' => 'aes-256', 'mode' => 'foo', 'key' => $key, 'hmac_digest' => 'sha256', 'hmac_key' => $key] - ]; - - for ($i = 0, $c = count($params); $i < $c; $i ++ ) - { - $this->assertFalse($this->encrypter->getParams($params[$i])); - } - - // Valid parameters - $params = [ - 'cipher' => 'aes-256', - 'mode' => 'cbc', - 'key' => str_repeat("\x0", 32), - 'hmac_key' => str_repeat("\x0", 32) - ]; - - $this->assertTrue(is_array($this->encrypter->getParams($params))); - - // why do we have the next 2 lines? - $params['base64'] = TRUE; - $params['hmac_digest'] = 'sha512'; - - // Including all parameters - $params = [ - 'cipher' => 'aes-256', - 'mode' => 'cbc', - 'key' => str_repeat("\x0", 32), - 'raw_data' => TRUE, - 'hmac_key' => str_repeat("\x0", 32), - 'hmac_digest' => 'sha256' - ]; - - $output = $this->encrypter->getParams($params); - unset($output['handle'], $output['cipher'], $params['raw_data'], $params['cipher']); - $params['base64'] = FALSE; - $this->assertEquals($params, $output); - - // HMAC disabled - unset($params['hmac_key'], $params['hmac_digest']); - $params['hmac'] = $params['raw_data'] = FALSE; - $params['cipher'] = 'aes-256'; - $output = $this->encrypter->getParams($params); - unset($output['handle'], $output['cipher'], $params['hmac'], $params['raw_data'], $params['cipher']); - $params['base64'] = TRUE; - $params['hmac_digest'] = $params['hmac_key'] = null; - $this->assertEquals($params, $output); - } - - // -------------------------------------------------------------------- - - /** - * initialize(), encrypt(), decrypt() test - * - * Testing the three methods separately is not realistic as they are - * designed to work together. - */ - public function testInitializeEncryptDecrypt() - { - $message = 'This is a plain-text message.'; - $key = "\xd0\xc9\x08\xc4\xde\x52\x12\x6e\xf8\xcc\xdb\x03\xea\xa0\x3a\x5c"; - $key = $key . $key; - - // Default state (AES-256/Rijndael-256 in CBC mode) - $encrypter = $this->encryption->initialize(array('key' => $key)); - - // Was the key properly set? - $this->assertEquals($key, $encrypter->key); - - $this->assertEquals($message, $encrypter->decrypt($encrypter->encrypt($message))); - - // Try DES in ECB mode, just for the sake of changing stuff - $encrypter = $this->encryption->initialize(array('cipher' => 'des', 'mode' => 'ecb', 'key' => substr($key, 0, 8))); - $this->assertEquals($message, $encrypter->decrypt($encrypter->encrypt($message))); - } - - // -------------------------------------------------------------------- - - /** - * encrypt(), decrypt test with custom parameters - */ - public function testEncryptDecryptCustom() - { - $message = 'Another plain-text message.'; - - $encrypter = $this->encryption->initialize(); - - // A random invalid parameter - $this->assertFalse($encrypter->encrypt($message, array('foo'))); - $this->assertFalse($encrypter->decrypt($message, array('foo'))); - - // No HMAC, binary output - $params = [ - 'cipher' => 'tripledes', - 'mode' => 'cfb', - 'key' => str_repeat("\x1", 16), - 'base64' => FALSE, - 'hmac' => FALSE - ]; - - $ciphertext = $encrypter->encrypt($message, $params); - - $this->assertEquals($message, $encrypter->decrypt($ciphertext, $params)); - } - -} diff --git a/user_guide_src/source/libraries/encryption.rst b/user_guide_src/source/libraries/encryption.rst index f0552e4de36d..f1b86ff170dc 100644 --- a/user_guide_src/source/libraries/encryption.rst +++ b/user_guide_src/source/libraries/encryption.rst @@ -18,7 +18,6 @@ Such extensions may need to be explicitly enabled in your instance of PHP. The following extensions are currently supported: - `OpenSSL `_ -- `Sodium `_. .. note:: Support for the ``MCrypt`` extension has been dropped, as that has been deprecated as of PHP 7.2. @@ -199,17 +198,6 @@ Seed 128 / 16 CBC, CFB, OFB, ECB ============== ============================== ========================================= -Sodium Notes ------------- - -Sodium is a modern, easy-to-use software library for encryption, decryption, signatures, password hashing and more. - -Sodium automatically uses AES-256 if it detects hardware acceleration/ -Otherwise, it will use the ChaCha20 cipher. - -You will need *libsodium* installed, as well as the PECL *Libsodium extenstion*, -in order to use this handler. - Message Length ============== From caec4518011c4641a52cef234c852d24bddfa3f6 Mon Sep 17 00:00:00 2001 From: Master Yoda Date: Wed, 12 Jul 2017 03:10:38 -0700 Subject: [PATCH 17/18] Fix Autoloader::loadLegacy, and increase code coverage --- system/Autoloader/Autoloader.php | 2 +- tests/system/Autoloader/AutoloaderTest.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index c2b849152eb8..2667b1a7148b 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -287,7 +287,7 @@ protected function loadLegacy($class) { // If there is a namespace on this class, then // we cannot load it from traditional locations. - if (strpos('\\', $class) !== false) + if (strpos($class, '\\') !== false) { return false; } diff --git a/tests/system/Autoloader/AutoloaderTest.php b/tests/system/Autoloader/AutoloaderTest.php index c995a8601427..28586c89e010 100644 --- a/tests/system/Autoloader/AutoloaderTest.php +++ b/tests/system/Autoloader/AutoloaderTest.php @@ -1,7 +1,6 @@ assertFalse($this->loader->loadClass('App\Missing\Classname')); + $this->assertFalse($this->loader->loadClass('\App\Missing\Classname')); } //-------------------------------------------------------------------- @@ -187,7 +187,7 @@ public function testLoadLegacy() $this->assertFalse((bool) $this->loader->loadClass('\nester\anotherLibrary')); $this->assertFalse((bool) $this->loader->loadClass('\Shouldnt\Find\This')); // should not be able to find these legacy classes - namespaced - $this->assertFalse((bool) $this->loader->loadClass('anotherLibrary')); + $this->assertFalse($this->loader->loadClass('\Some\CoolModel')); } //-------------------------------------------------------------------- From df82b97a91bfdc1b06f4cd6b6ea73560f1a40975 Mon Sep 17 00:00:00 2001 From: Master Yoda Date: Wed, 12 Jul 2017 08:21:24 -0700 Subject: [PATCH 18/18] Update FileLocator - less code, better coverage --- system/Autoloader/FileLocator.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/system/Autoloader/FileLocator.php b/system/Autoloader/FileLocator.php index e4aeddbba6d4..09d3c61de789 100644 --- a/system/Autoloader/FileLocator.php +++ b/system/Autoloader/FileLocator.php @@ -258,12 +258,8 @@ public function listFiles(string $path): array $tempFiles = get_filenames($fullPath, true); //CLI::newLine($tempFiles); - if ( ! count($tempFiles)) - { - continue; - } - - $files = array_merge($files, $tempFiles); + if (count($tempFiles)) + $files = array_merge($files, $tempFiles); } return $files;