replacing mcrypt with openssl. first draft. untested. NOTE: it is not sufficient to pass current unit tests. must also interoperate with encrypted data

This commit is contained in:
tom--
2015-02-09 17:11:53 -05:00
parent 681a7739ff
commit 5736891a0b

View File

@@ -20,7 +20,8 @@ use Yii;
* - Data tampering prevention: [[hashData()]] and [[validateData()]] * - Data tampering prevention: [[hashData()]] and [[validateData()]]
* - Password validation: [[generatePasswordHash()]] and [[validatePassword()]] * - Password validation: [[generatePasswordHash()]] and [[validatePassword()]]
* *
* > Note: this class requires 'mcrypt' PHP extension. For the highest security level PHP version >= 5.5.0 is recommended. * > Note: this class requires 'OpenSSL' PHP extension for random key/string gneration on Windows and
* for encryption/decryption on all platforms. For the highest security level PHP version >= 5.5.0 is recommended.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @author Tom Worster <fsb@thefsb.org> * @author Tom Worster <fsb@thefsb.org>
@@ -30,18 +31,13 @@ use Yii;
class Security extends Component class Security extends Component
{ {
/** /**
* Cipher algorithm for mcrypt module. * Block cipher algorithm for encryption and decryption.
* AES has 128-bit block size and three key sizes: 128, 192 and 256 bits.
* mcrypt offers the Rijndael cipher with block sizes of 128, 192 and 256
* bits but only the 128-bit Rijndael is standardized in AES.
* So to use AES in mycrypt, specify `'rijndael-128'` cipher and mcrypt
* chooses the appropriate AES based on the length of the supplied key.
*/ */
const MCRYPT_CIPHER = 'rijndael-128'; const BLOCK_CIPHER = 'AES';
/** /**
* Block cipher operation mode for mcrypt module. * Block cipher operation mode.
*/ */
const MCRYPT_MODE = 'cbc'; const BLOCK_MODE = 'CBC';
/** /**
* Size in bytes of encryption key, message authentication key and KDF salt. * Size in bytes of encryption key, message authentication key and KDF salt.
*/ */
@@ -142,37 +138,22 @@ class Security extends Component
} }
/** /**
* Initializes the mcrypt module. * Returns a string for use in the $method argument of openssl_en/decrypt functions.
* @return resource the mcrypt module handle. *
* @throws InvalidConfigException if mcrypt extension is not installed * No checks are made. This function will produce nonsense that OpenSSL will reject
* @throws Exception if mcrypt initialization fails * if the cipher is not AES, the key size is not 16, 24 or 32, or if the mode is
* unsupported.
*
* Tbh, this is a foot gun. But we need it if we allow the user to change key size
* or block mode. And the user must override it top use a cipher other than AES.
*
* @return string OpenSSL cipher method.
*/ */
protected function getCryptModule() protected function opensslCipher()
{ {
if ($this->_cryptModule === null) { return static::BLOCK_CIPHER . '-' . (8 * static::KEY_SIZE) . '-' . static::BLOCK_MODE;
if (!extension_loaded('mcrypt')) {
throw new InvalidConfigException('The mcrypt PHP extension is not installed.');
}
$this->_cryptModule = @mcrypt_module_open(self::MCRYPT_CIPHER, '', self::MCRYPT_MODE, '');
if ($this->_cryptModule === false) {
$this->_cryptModule = null;
throw new Exception('Failed to initialize the mcrypt module.');
}
}
return $this->_cryptModule;
} }
public function __destruct()
{
if ($this->_cryptModule !== null) {
mcrypt_module_close($this->_cryptModule);
$this->_cryptModule = null;
}
}
/** /**
* Encrypts data. * Encrypts data.
* @param string $data data to be encrypted * @param string $data data to be encrypted
@@ -186,8 +167,6 @@ class Security extends Component
*/ */
protected function encrypt($data, $passwordBased, $secret, $info) protected function encrypt($data, $passwordBased, $secret, $info)
{ {
$module = $this->getCryptModule();
$keySalt = $this->generateRandomKey(self::KEY_SIZE); $keySalt = $this->generateRandomKey(self::KEY_SIZE);
if ($passwordBased) { if ($passwordBased) {
$key = $this->pbkdf2(self::KDF_HASH, $secret, $keySalt, $this->derivationIterations, self::KEY_SIZE); $key = $this->pbkdf2(self::KDF_HASH, $secret, $keySalt, $this->derivationIterations, self::KEY_SIZE);
@@ -196,11 +175,9 @@ class Security extends Component
} }
$data = $this->addPadding($data); $data = $this->addPadding($data);
$ivSize = mcrypt_enc_get_iv_size($module); $ivSize = 16;
$iv = mcrypt_create_iv($ivSize, MCRYPT_DEV_URANDOM); $iv = $this->generateRandomKey($ivSize);
mcrypt_generic_init($module, $key, $iv); $encrypted = openssl_encrypt($data, $this->opensslCipher(), $key, OPENSSL_RAW_DATA, $iv);
$encrypted = mcrypt_generic($module, $data);
mcrypt_generic_deinit($module);
$authKey = $this->hkdf(self::KDF_HASH, $key, null, self::AUTH_KEY_INFO, self::KEY_SIZE); $authKey = $this->hkdf(self::KDF_HASH, $key, null, self::AUTH_KEY_INFO, self::KEY_SIZE);
$hashed = $this->hashData($iv . $encrypted, $authKey); $hashed = $this->hashData($iv . $encrypted, $authKey);
@@ -209,7 +186,7 @@ class Security extends Component
* Output: [keySalt][MAC][IV][ciphertext] * Output: [keySalt][MAC][IV][ciphertext]
* - keySalt is KEY_SIZE bytes long * - keySalt is KEY_SIZE bytes long
* - MAC: message authentication code, length same as the output of MAC_HASH * - MAC: message authentication code, length same as the output of MAC_HASH
* - IV: initialization vector, length set by CRYPT_CIPHER and CRYPT_MODE, mcrypt_enc_get_iv_size() * - IV: initialization vector, length 16, i.e. the AES block size
*/ */
return $keySalt . $hashed; return $keySalt . $hashed;
} }
@@ -238,13 +215,10 @@ class Security extends Component
return false; return false;
} }
$module = $this->getCryptModule(); $ivSize = 16;
$ivSize = mcrypt_enc_get_iv_size($module);
$iv = StringHelper::byteSubstr($data, 0, $ivSize); $iv = StringHelper::byteSubstr($data, 0, $ivSize);
$encrypted = StringHelper::byteSubstr($data, $ivSize, null); $encrypted = StringHelper::byteSubstr($data, $ivSize, null);
mcrypt_generic_init($module, $key, $iv); $decrypted = openssl_decrypt($encrypted, $this->opensslCipher(), $key, OPENSSL_RAW_DATA, $iv);
$decrypted = mdecrypt_generic($module, $encrypted);
mcrypt_generic_deinit($module);
return $this->stripPadding($decrypted); return $this->stripPadding($decrypted);
} }
@@ -256,8 +230,7 @@ class Security extends Component
*/ */
protected function addPadding($data) protected function addPadding($data)
{ {
$module = $this->getCryptModule(); $blockSize = 16;
$blockSize = mcrypt_enc_get_block_size($module);
$pad = $blockSize - (StringHelper::byteLength($data) % $blockSize); $pad = $blockSize - (StringHelper::byteLength($data) % $blockSize);
return $data . str_repeat(chr($pad), $pad); return $data . str_repeat(chr($pad), $pad);
@@ -456,19 +429,72 @@ class Security extends Component
* *
* @param integer $length the number of bytes to generate * @param integer $length the number of bytes to generate
* @return string the generated random bytes * @return string the generated random bytes
* @throws InvalidConfigException if mcrypt extension is not installed. * @throws InvalidConfigException if OpenSSL extension is required (e.g. on Windows) but not installed.
* @throws Exception on failure. * @throws Exception on failure.
*/ */
public function generateRandomKey($length = 32) public function generateRandomKey($length = 32)
{ {
if (!extension_loaded('mcrypt')) { /*
throw new InvalidConfigException('The mcrypt PHP extension is not installed.'); * Strategy
*
* The most common platform is Linux, on which /dev/urandom is the best choice. Many other OSs
* implement a device called /dev/urandom for Linux compat and it is good too. So if there is
* a /dev/urandom then it is our first choice regardless of OS.
*
* Nearly all other modern Unix-like systems (the BSDs, Unixes and OS X) have a /dev/random
* that is a good choice. If we didn't get bytes from /dev/urandom then we try this next but
* only if the system is not Linux. Do not try to read /dev/random on Linux.
*
* Finally, OpenSSL can supply CSPR bytes. It is our last resort. On Windows this reads from
* CryptGenRandom, which is the right thing to do. On other systems that don't have a Unix-like
* /dev/u?random , it will deliver bytes from its own CSPRNG that is seeded from kernel sources
* of randomness. Even though it is fast, we don't generally prefer OpenSSL over /dev/u?random
* because an RNG in user space memory is undesirable.
*
* For background, see http://sockpuppet.org/blog/2014/02/25/safely-generate-random-numbers/
*/
$bytes = '';
// If we are on Linux or any OS that mimics the Linux /dev/urandom device, e.g. FreeBSD or OS X,
// then read from /dev/urandom.
if (file_exists('/dev/urandom')) {
$handle = fopen('/dev/urandom', 'r');
if ($handle !== false) {
$bytes .= fread($handle, $length);
fclose($handle);
}
} }
$bytes = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
if ($bytes === false) { if (mb_strlen($bytes, '8bit') >= $length) {
return mb_substr($bytes, 0, $length, '8bit');
}
// If we are not on Linux and there is a /dev/random device then we have a BSD or Unix device
// that won't block. It's not safe to read from /dev/random on Linux.
if (php_uname('s') !== 'Linux' && file_exists('/dev/random')) {
$handle = fopen('/dev/random', 'r');
if ($handle !== false) {
$bytes .= fread($handle, $length);
fclose($handle);
}
}
if (mb_strlen($bytes, '8bit') >= $length) {
return mb_substr($bytes, 0, $length, '8bit');
}
if (!extension_loaded('openssl')) {
throw new InvalidConfigException('The OpenSSL PHP extension is not installed.');
}
$bytes .= openssl_random_pseudo_bytes($length, $cryptoStrong);
if (mb_strlen($bytes, '8bit') < $length || !$cryptoStrong) {
throw new Exception('Unable to generate random bytes.'); throw new Exception('Unable to generate random bytes.');
} }
return $bytes;
return mb_substr($bytes, 0, $length, '8bit');
} }
/** /**
@@ -477,7 +503,7 @@ class Security extends Component
* *
* @param integer $length the length of the key in characters * @param integer $length the length of the key in characters
* @return string the generated random key * @return string the generated random key
* @throws InvalidConfigException if mcrypt extension is not installed. * @throws InvalidConfigException if OpenSSL extension is needed but not installed.
* @throws Exception on failure. * @throws Exception on failure.
*/ */
public function generateRandomString($length = 32) public function generateRandomString($length = 32)