mirror of
https://github.com/Lastorder-DC/rhymix.git
synced 2026-01-04 01:01:41 +09:00
Implement encryption, decryption, and CSPRNG in Security class
This commit is contained in:
parent
544170b530
commit
90dcc4a2e8
4 changed files with 301 additions and 97 deletions
|
|
@ -173,22 +173,22 @@ class Password
|
|||
$hash = explode(':', $hash);
|
||||
$hash[3] = base64_decode($hash[3]);
|
||||
$hash_to_compare = $this->pbkdf2($password, $hash[2], $hash[0], intval($hash[1], 10), strlen($hash[3]));
|
||||
return $this->strcmpConstantTime($hash_to_compare, $hash[3]);
|
||||
return Rhymix\Framework\Security::compareStrings($hash_to_compare, $hash[3]);
|
||||
|
||||
case 'bcrypt':
|
||||
$hash_to_compare = $this->bcrypt($password, $hash);
|
||||
return $this->strcmpConstantTime($hash_to_compare, $hash);
|
||||
return Rhymix\Framework\Security::compareStrings($hash_to_compare, $hash);
|
||||
|
||||
default:
|
||||
if($algorithm && isset(self::$_custom[$algorithm]))
|
||||
{
|
||||
$hash_callback = self::$_custom[$algorithm]['callback'];
|
||||
$hash_to_compare = $hash_callback($password, $hash);
|
||||
return $this->strcmpConstantTime($hash_to_compare, $hash);
|
||||
return Rhymix\Framework\Security::compareStrings($hash_to_compare, $hash);
|
||||
}
|
||||
if(in_array($algorithm, hash_algos()))
|
||||
{
|
||||
return $this->strcmpConstantTime(hash($algorithm, $password), $hash);
|
||||
return Rhymix\Framework\Security::compareStrings(hash($algorithm, $password), $hash);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -280,90 +280,7 @@ class Password
|
|||
*/
|
||||
public function createSecureSalt($length, $format = 'hex')
|
||||
{
|
||||
// Find out how many bytes of entropy we really need
|
||||
switch($format)
|
||||
{
|
||||
case 'hex':
|
||||
$entropy_required_bytes = ceil($length / 2);
|
||||
break;
|
||||
case 'alnum':
|
||||
case 'printable':
|
||||
$entropy_required_bytes = ceil($length * 3 / 4);
|
||||
break;
|
||||
default:
|
||||
$entropy_required_bytes = $length;
|
||||
}
|
||||
|
||||
// Cap entropy to 256 bits from any one source, because anything more is meaningless
|
||||
$entropy_capped_bytes = min(32, $entropy_required_bytes);
|
||||
|
||||
// Find and use the most secure way to generate a random string
|
||||
$entropy = false;
|
||||
$is_windows = (defined('PHP_OS') && strtoupper(substr(PHP_OS, 0, 3)) === 'WIN');
|
||||
if(function_exists('random_bytes')) // PHP 7
|
||||
{
|
||||
$entropy = random_bytes($entropy_capped_bytes);
|
||||
}
|
||||
elseif(function_exists('openssl_random_pseudo_bytes') && (!$is_windows || version_compare(PHP_VERSION, '5.4', '>=')))
|
||||
{
|
||||
$entropy = openssl_random_pseudo_bytes($entropy_capped_bytes);
|
||||
}
|
||||
elseif(function_exists('mcrypt_create_iv') && (!$is_windows || version_compare(PHP_VERSION, '5.3.7', '>=')))
|
||||
{
|
||||
$entropy = mcrypt_create_iv($entropy_capped_bytes, MCRYPT_DEV_URANDOM);
|
||||
}
|
||||
elseif(function_exists('mcrypt_create_iv') && $is_windows)
|
||||
{
|
||||
$entropy = mcrypt_create_iv($entropy_capped_bytes, MCRYPT_RAND);
|
||||
}
|
||||
elseif(!$is_windows && @is_readable('/dev/urandom'))
|
||||
{
|
||||
$fp = fopen('/dev/urandom', 'rb');
|
||||
if (function_exists('stream_set_read_buffer')) // This function does not exist in HHVM
|
||||
{
|
||||
stream_set_read_buffer($fp, 0); // Prevent reading several KB of unnecessary data from urandom
|
||||
}
|
||||
$entropy = fread($fp, $entropy_capped_bytes);
|
||||
fclose($fp);
|
||||
}
|
||||
|
||||
// Use built-in source of entropy if an error occurs while using other functions
|
||||
if($entropy === false || strlen($entropy) < $entropy_capped_bytes)
|
||||
{
|
||||
$entropy = '';
|
||||
for($i = 0; $i < $entropy_capped_bytes; $i += 2)
|
||||
{
|
||||
$entropy .= pack('S', rand(0, 65536) ^ mt_rand(0, 65535));
|
||||
}
|
||||
}
|
||||
|
||||
// Mixing (see RFC 4086 section 5)
|
||||
$output = '';
|
||||
for($i = 0; $i < $entropy_required_bytes; $i += 32)
|
||||
{
|
||||
$output .= hash('sha256', $entropy . $i . rand(), true);
|
||||
}
|
||||
|
||||
// Encode and return the random string
|
||||
switch($format)
|
||||
{
|
||||
case 'hex':
|
||||
return substr(bin2hex($output), 0, $length);
|
||||
case 'binary':
|
||||
return substr($output, 0, $length);
|
||||
case 'printable':
|
||||
$salt = '';
|
||||
for($i = 0; $i < $length; $i++)
|
||||
{
|
||||
$salt .= chr(33 + (crc32(sha1($i . $output)) % 94));
|
||||
}
|
||||
return $salt;
|
||||
case 'alnum':
|
||||
default:
|
||||
$salt = substr(base64_encode($output), 0, $length);
|
||||
$replacements = chr(rand(65, 90)) . chr(rand(97, 122)) . rand(0, 9);
|
||||
return strtr($salt, '+/=', $replacements);
|
||||
}
|
||||
return Rhymix\Framework\Security::getRandom($length, $format);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -446,13 +363,7 @@ class Password
|
|||
*/
|
||||
function strcmpConstantTime($a, $b)
|
||||
{
|
||||
$diff = strlen($a) ^ strlen($b);
|
||||
$maxlen = min(strlen($a), strlen($b));
|
||||
for($i = 0; $i < $maxlen; $i++)
|
||||
{
|
||||
$diff |= ord($a[$i]) ^ ord($b[$i]);
|
||||
}
|
||||
return $diff === 0;
|
||||
return Rhymix\Framework\Security::compareStrings($a, $b);
|
||||
}
|
||||
}
|
||||
/* End of file : Password.class.php */
|
||||
|
|
|
|||
|
|
@ -43,6 +43,225 @@ class Security
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a string using AES.
|
||||
*
|
||||
* @param string $plaintext
|
||||
* @param string $key (optional)
|
||||
* @param bool $force_compat (optional)
|
||||
* @return string|false
|
||||
*/
|
||||
public static function encrypt($plaintext, $key = null, $force_compat = false)
|
||||
{
|
||||
// Get the encryption key.
|
||||
$key = $key ?: config('crypto.encryption_key');
|
||||
$key = substr(hash('sha256', $key, true), 0, 16);
|
||||
|
||||
// Use defuse/php-encryption if possible.
|
||||
if (!$force_compat && version_compare(\PHP_VERSION, '5.4.0', '>=') && function_exists('openssl_encrypt'))
|
||||
{
|
||||
try
|
||||
{
|
||||
return base64_encode(\Crypto::Encrypt($plaintext, $key));
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, use the CryptoCompat class.
|
||||
return base64_encode(\CryptoCompat::encrypt($plaintext, $key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt a string using AES.
|
||||
*
|
||||
* @param string $plaintext
|
||||
* @param string $key (optional)
|
||||
* @param bool $force_compat (optional)
|
||||
* @return string|false
|
||||
*/
|
||||
public static function decrypt($ciphertext, $key = null, $force_compat = false)
|
||||
{
|
||||
// Get the encryption key.
|
||||
$key = $key ?: config('crypto.encryption_key');
|
||||
$key = substr(hash('sha256', $key, true), 0, 16);
|
||||
|
||||
// Check whether the ciphertext is valid.
|
||||
$ciphertext = @base64_decode($ciphertext);
|
||||
if (strlen($ciphertext) < 48)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use defuse/php-encryption if possible.
|
||||
if (!$force_compat && version_compare(\PHP_VERSION, '5.4.0', '>=') && function_exists('openssl_decrypt'))
|
||||
{
|
||||
try
|
||||
{
|
||||
return \Crypto::Decrypt($ciphertext, $key);
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, use the CryptoCompat class.
|
||||
return \CryptoCompat::decrypt($ciphertext, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a cryptographically secure random string.
|
||||
*
|
||||
* @param int $length
|
||||
* @param string $format
|
||||
* @return string
|
||||
*/
|
||||
public function getRandom($length = 32, $format = 'alnum')
|
||||
{
|
||||
// Find out how many bytes of entropy we really need.
|
||||
switch($format)
|
||||
{
|
||||
case 'binary':
|
||||
$entropy_required_bytes = $length;
|
||||
break;
|
||||
case 'hex':
|
||||
$entropy_required_bytes = ceil($length / 2);
|
||||
break;
|
||||
case 'alnum':
|
||||
case 'printable':
|
||||
default:
|
||||
$entropy_required_bytes = ceil($length * 3 / 4);
|
||||
break;
|
||||
}
|
||||
|
||||
// Cap entropy to 256 bits from any one source, because anything more is meaningless.
|
||||
$entropy_capped_bytes = min(32, $entropy_required_bytes);
|
||||
|
||||
// Find and use the most secure way to generate a random string.
|
||||
$entropy = false;
|
||||
$is_windows = (defined('\PHP_OS') && strtoupper(substr(\PHP_OS, 0, 3)) === 'WIN');
|
||||
if(function_exists('random_bytes')) // PHP 7
|
||||
{
|
||||
$entropy = random_bytes($entropy_capped_bytes);
|
||||
}
|
||||
elseif(function_exists('openssl_random_pseudo_bytes') && (!$is_windows || version_compare(\PHP_VERSION, '5.4', '>=')))
|
||||
{
|
||||
$entropy = openssl_random_pseudo_bytes($entropy_capped_bytes);
|
||||
}
|
||||
elseif(function_exists('mcrypt_create_iv') && (!$is_windows || version_compare(\PHP_VERSION, '5.3.7', '>=')))
|
||||
{
|
||||
$entropy = mcrypt_create_iv($entropy_capped_bytes, \MCRYPT_DEV_URANDOM);
|
||||
}
|
||||
elseif(function_exists('mcrypt_create_iv') && $is_windows)
|
||||
{
|
||||
$entropy = mcrypt_create_iv($entropy_capped_bytes, \MCRYPT_RAND);
|
||||
}
|
||||
elseif(!$is_windows && @is_readable('/dev/urandom'))
|
||||
{
|
||||
$fp = fopen('/dev/urandom', 'rb');
|
||||
if (function_exists('stream_set_read_buffer')) // This function does not exist in HHVM.
|
||||
{
|
||||
stream_set_read_buffer($fp, 0); // Prevent reading several KB of unnecessary data from urandom.
|
||||
}
|
||||
$entropy = fread($fp, $entropy_capped_bytes);
|
||||
fclose($fp);
|
||||
}
|
||||
|
||||
// Use built-in source of entropy if an error occurs while using other functions.
|
||||
if($entropy === false || strlen($entropy) < $entropy_capped_bytes)
|
||||
{
|
||||
$entropy = '';
|
||||
for($i = 0; $i < $entropy_capped_bytes; $i += 2)
|
||||
{
|
||||
$entropy .= pack('S', rand(0, 65536) ^ mt_rand(0, 65535));
|
||||
}
|
||||
}
|
||||
|
||||
// Mixing (see RFC 4086 section 5)
|
||||
$output = '';
|
||||
for($i = 0; $i < $entropy_required_bytes; $i += 32)
|
||||
{
|
||||
$output .= hash('sha256', $entropy . $i . rand(), true);
|
||||
}
|
||||
|
||||
// Encode and return the random string.
|
||||
switch($format)
|
||||
{
|
||||
case 'binary':
|
||||
return substr($output, 0, $length);
|
||||
case 'printable':
|
||||
$salt = '';
|
||||
for($i = 0; $i < $length; $i++)
|
||||
{
|
||||
$salt .= chr(33 + (crc32(sha1($i . $output)) % 94));
|
||||
}
|
||||
return $salt;
|
||||
case 'hex':
|
||||
return substr(bin2hex($output), 0, $length);
|
||||
case 'alnum':
|
||||
default:
|
||||
$salt = substr(base64_encode($output), 0, $length);
|
||||
$replacements = chr(rand(65, 90)) . chr(rand(97, 122)) . rand(0, 9);
|
||||
return strtr($salt, '+/=', $replacements);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a cryptographically secure random number between $min and $max.
|
||||
*
|
||||
* @param int $min
|
||||
* @param int $max
|
||||
* @return int
|
||||
*/
|
||||
public static function getRandomNumber($min = 0, $max = 0x7fffffff)
|
||||
{
|
||||
if (function_exists('random_int'))
|
||||
{
|
||||
return random_int($min, $max);
|
||||
}
|
||||
else
|
||||
{
|
||||
$bytes_required = min(4, ceil(log($max - $min, 2) / 8) + 1);
|
||||
$bytes = self::getRandom($bytes_required, 'binary');
|
||||
$offset = abs(hexdec(bin2hex($bytes)) % ($max - $min + 1));
|
||||
return intval($min + $offset);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random UUID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getRandomUUID()
|
||||
{
|
||||
$randpool = self::getRandom(16, 'binary');
|
||||
$randpool[6] = chr(ord($randpool[6]) & 0x0f | 0x40);
|
||||
$randpool[8] = chr(ord($randpool[8]) & 0x3f | 0x80);
|
||||
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($randpool), 4));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two strings in constant time.
|
||||
*
|
||||
* @param string $a
|
||||
* @param string $b
|
||||
* @return bool
|
||||
*/
|
||||
public static function compareStrings($a, $b)
|
||||
{
|
||||
$diff = strlen($a) ^ strlen($b);
|
||||
$maxlen = min(strlen($a), strlen($b));
|
||||
for($i = 0; $i < $maxlen; $i++)
|
||||
{
|
||||
$diff |= ord($a[$i]) ^ ord($b[$i]);
|
||||
}
|
||||
return $diff === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current request seems to be a CSRF attack.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class CryptoCompat
|
|||
// Validate MAC
|
||||
$mac_key = self::_defuseCompatibleHKDF($key, self::ENCRYPTION_MAC_INFO);
|
||||
$mac_compare = hash_hmac(self::ENCRYPTION_MAC_ALGO, ($iv . $ciphertext), $mac_key, true);
|
||||
if (!Password::strcmpConstantTime($mac, $mac_compare))
|
||||
if (!Rhymix\Framework\Security::compareStrings($mac, $mac_compare))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -80,7 +80,7 @@ class CryptoCompat
|
|||
*/
|
||||
protected static function _createIV()
|
||||
{
|
||||
return hex2bin(Password::createSecureSalt(self::ENCRYPTION_BLOCK_SIZE * 2, 'hex'));
|
||||
return Rhymix\Framework\Security::getRandom(self::ENCRYPTION_BLOCK_SIZE, 'binary');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -17,6 +17,80 @@ class SecurityTest extends \Codeception\TestCase\Test
|
|||
$this->assertEquals('foo(bar).xls', Rhymix\Framework\Security::sanitize('foo<bar>.xls', 'filename'));
|
||||
}
|
||||
|
||||
public function testEncryption()
|
||||
{
|
||||
$plaintext = Rhymix\Framework\Security::getRandom();
|
||||
|
||||
// Encryption with default key.
|
||||
$encrypted = Rhymix\Framework\Security::encrypt($plaintext);
|
||||
$this->assertNotEquals(false, $encrypted);
|
||||
$decrypted = Rhymix\Framework\Security::decrypt($encrypted);
|
||||
$this->assertEquals($plaintext, $decrypted);
|
||||
|
||||
// Encryption with custom key.
|
||||
$key = Rhymix\Framework\Security::getRandom();
|
||||
$encrypted = Rhymix\Framework\Security::encrypt($plaintext, $key);
|
||||
$this->assertNotEquals(false, $encrypted);
|
||||
$decrypted = Rhymix\Framework\Security::decrypt($encrypted, $key);
|
||||
$this->assertEquals($plaintext, $decrypted);
|
||||
|
||||
// Encryption with defuse/php-encryption and decryption with CryptoCompat.
|
||||
$encrypted = Rhymix\Framework\Security::encrypt($plaintext);
|
||||
$this->assertNotEquals(false, $encrypted);
|
||||
$decrypted = Rhymix\Framework\Security::decrypt($encrypted, null, true);
|
||||
$this->assertEquals($plaintext, $decrypted);
|
||||
|
||||
// Encryption with CryptoCompat and decryption with defuse/php-encryption.
|
||||
$encrypted = Rhymix\Framework\Security::encrypt($plaintext, null, true);
|
||||
$this->assertNotEquals(false, $encrypted);
|
||||
$decrypted = Rhymix\Framework\Security::decrypt($encrypted);
|
||||
$this->assertEquals($plaintext, $decrypted);
|
||||
|
||||
// Test invalid ciphertext.
|
||||
$decrypted = Rhymix\Framework\Security::decrypt('1234' . substr($encrypted, 4));
|
||||
$this->assertEquals(false, $decrypted);
|
||||
$decrypted = Rhymix\Framework\Security::decrypt(substr($encrypted, strlen($encrypted) - 4) . 'abcd');
|
||||
$this->assertEquals(false, $decrypted);
|
||||
$decrypted = Rhymix\Framework\Security::decrypt($plaintext);
|
||||
$this->assertEquals(false, $decrypted);
|
||||
}
|
||||
|
||||
public function testGetRandom()
|
||||
{
|
||||
$this->assertEquals(1, preg_match('/^[0-9a-zA-Z]{32}$/', Rhymix\Framework\Security::getRandom()));
|
||||
$this->assertEquals(1, preg_match('/^[0-9a-zA-Z]{256}$/', Rhymix\Framework\Security::getRandom(256)));
|
||||
$this->assertEquals(1, preg_match('/^[0-9a-zA-Z]{16}$/', Rhymix\Framework\Security::getRandom(16, 'alnum')));
|
||||
$this->assertEquals(1, preg_match('/^[0-9a-f]{16}$/', Rhymix\Framework\Security::getRandom(16, 'hex')));
|
||||
$this->assertEquals(1, preg_match('/^[\x21-\x7e]{16}$/', Rhymix\Framework\Security::getRandom(16, 'printable')));
|
||||
}
|
||||
|
||||
public function testGetRandomNumber()
|
||||
{
|
||||
for ($i = 0; $i < 10; $i++)
|
||||
{
|
||||
$min = mt_rand(0, 10000);
|
||||
$max = $min + mt_rand(0, 10000);
|
||||
$random = Rhymix\Framework\Security::getRandomNumber($min, $max);
|
||||
$this->assertTrue($random >= $min && $random < $max);
|
||||
}
|
||||
}
|
||||
|
||||
public function testGetRandomUUID()
|
||||
{
|
||||
$regex = '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/';
|
||||
for ($i = 0; $i < 10; $i++)
|
||||
{
|
||||
$this->assertEquals(1, preg_match($regex, Rhymix\Framework\Security::getRandomUUID()));
|
||||
}
|
||||
}
|
||||
|
||||
public function testCompareStrings()
|
||||
{
|
||||
$this->assertTrue(Rhymix\Framework\Security::compareStrings('foobar', 'foobar'));
|
||||
$this->assertFalse(Rhymix\Framework\Security::compareStrings('foobar', 'foobar*'));
|
||||
$this->assertFalse(Rhymix\Framework\Security::compareStrings('foo', 'bar'));
|
||||
}
|
||||
|
||||
public function testCheckCSRF()
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue