290 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			290 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| /**
 | |
|  * @package   FOF
 | |
|  * @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
 | |
|  * @license   GNU General Public License version 3, or later
 | |
|  */
 | |
| 
 | |
| namespace FOF40\Encrypt;
 | |
| 
 | |
| defined('_JEXEC') || die;
 | |
| 
 | |
| use FOF40\Encrypt\AesAdapter\AdapterInterface;
 | |
| use FOF40\Encrypt\AesAdapter\OpenSSL;
 | |
| 
 | |
| /**
 | |
|  * A simple abstraction to AES encryption
 | |
|  *
 | |
|  * Usage:
 | |
|  *
 | |
|  * // Create a new instance.
 | |
|  * $aes = new Aes();
 | |
|  * // Set the encryption password. It's expanded to a key automatically.
 | |
|  * $aes->setPassword('yourPassword');
 | |
|  * // Encrypt something.
 | |
|  * $cipherText = $aes->encryptString($sourcePlainText);
 | |
|  * // Decrypt something
 | |
|  * $plainText = $aes->decryptString($sourceCipherText);
 | |
|  */
 | |
| class Aes
 | |
| {
 | |
| 	/**
 | |
| 	 * The cipher key.
 | |
| 	 *
 | |
| 	 * @var   string
 | |
| 	 */
 | |
| 	private $key = '';
 | |
| 
 | |
| 	/**
 | |
| 	 * The AES encryption adapter in use.
 | |
| 	 *
 | |
| 	 * @var  AdapterInterface
 | |
| 	 */
 | |
| 	private $adapter;
 | |
| 
 | |
| 	/**
 | |
| 	 * Initialise the AES encryption object.
 | |
| 	 *
 | |
| 	 * @param   string   $mode     Encryption mode. Can be ebc or cbc. We recommend using cbc.
 | |
| 	 */
 | |
| 	public function __construct(string $mode = 'cbc')
 | |
| 	{
 | |
| 		$this->adapter = new OpenSSL();
 | |
| 
 | |
| 		$this->adapter->setEncryptionMode($mode);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Is AES encryption supported by this PHP installation?
 | |
| 	 *
 | |
| 	 * @return boolean
 | |
| 	 */
 | |
| 	public static function isSupported(): bool
 | |
| 	{
 | |
| 		$adapter = new OpenSSL();
 | |
| 
 | |
| 		if (!$adapter->isSupported())
 | |
| 		{
 | |
| 			return false;
 | |
| 		}
 | |
| 
 | |
| 		if (!\function_exists('base64_encode'))
 | |
| 		{
 | |
| 			return false;
 | |
| 		}
 | |
| 
 | |
| 		if (!\function_exists('base64_decode'))
 | |
| 		{
 | |
| 			return false;
 | |
| 		}
 | |
| 
 | |
| 		if (!\function_exists('hash_algos'))
 | |
| 		{
 | |
| 			return false;
 | |
| 		}
 | |
| 
 | |
| 		$algorithms = \hash_algos();
 | |
| 
 | |
| 		return in_array('sha256', $algorithms);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Sets the password for this instance.
 | |
| 	 *
 | |
| 	 * @param   string  $password  The password (either user-provided password or binary encryption key) to use
 | |
| 	 */
 | |
| 	public function setPassword(string $password)
 | |
| 	{
 | |
| 		$this->key = $password;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Encrypts a string using AES
 | |
| 	 *
 | |
| 	 * @param   string  $stringToEncrypt  The plaintext to encrypt
 | |
| 	 * @param   bool    $base64encoded    Should I Base64-encode the result?
 | |
| 	 *
 | |
| 	 * @return   string  The cryptotext. Please note that the first 16 bytes of
 | |
| 	 *                   the raw string is the IV (initialisation vector) which
 | |
| 	 *                   is necessary for decoding the string.
 | |
| 	 */
 | |
| 	public function encryptString(string $stringToEncrypt, bool $base64encoded = true): string
 | |
| 	{
 | |
| 		$blockSize = $this->adapter->getBlockSize();
 | |
| 		$randVal   = new Randval();
 | |
| 		$iv        = $randVal->generate($blockSize);
 | |
| 
 | |
| 		$key        = $this->getExpandedKey($blockSize, $iv);
 | |
| 		$cipherText = $this->adapter->encrypt($stringToEncrypt, $key, $iv);
 | |
| 
 | |
| 		// Optionally pass the result through Base64 encoding
 | |
| 		if ($base64encoded)
 | |
| 		{
 | |
| 			$cipherText = base64_encode($cipherText);
 | |
| 		}
 | |
| 
 | |
| 		// Return the result
 | |
| 		return $cipherText;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Decrypts a ciphertext into a plaintext string using AES
 | |
| 	 *
 | |
| 	 * @param   string  $stringToDecrypt  The ciphertext to decrypt. The first 16 bytes of the raw string must contain
 | |
| 	 *                                    the IV (initialisation vector).
 | |
| 	 * @param   bool    $base64encoded    Should I Base64-decode the data before decryption?
 | |
| 	 * @param   bool    $legacy           Use legacy key expansion? Use it to decrypt date encrypted with FOF 3.
 | |
| 	 *
 | |
| 	 * @return   string  The plain text string
 | |
| 	 */
 | |
| 	public function decryptString(string $stringToDecrypt, bool $base64encoded = true, bool $legacy = false): string
 | |
| 	{
 | |
| 		if ($base64encoded)
 | |
| 		{
 | |
| 			$stringToDecrypt = base64_decode($stringToDecrypt);
 | |
| 		}
 | |
| 
 | |
| 		// Extract IV
 | |
| 		$iv_size = $this->adapter->getBlockSize();
 | |
| 		$strLen  = function_exists('mb_strlen') ? mb_strlen($stringToDecrypt, 'ASCII') : strlen($stringToDecrypt);
 | |
| 
 | |
| 		// If the string is not big enough to have an Initialization Vector in front then, clearly, it is not encrypted.
 | |
| 		if ($strLen < $iv_size)
 | |
| 		{
 | |
| 			return '';
 | |
| 		}
 | |
| 
 | |
| 		// Get the IV, the key and decrypt the string
 | |
| 		$iv  = substr($stringToDecrypt, 0, $iv_size);
 | |
| 		$key = $this->getExpandedKey($iv_size, $iv, $legacy);
 | |
| 
 | |
| 		return $this->adapter->decrypt($stringToDecrypt, $key);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Performs key expansion using PBKDF2
 | |
| 	 *
 | |
| 	 * CAVEAT: If your password ($this->key) is the same size as $blockSize you don't get key expansion. Practically,
 | |
| 	 * it means that you should avoid using 16 byte passwords.
 | |
| 	 *
 | |
| 	 * @param   int     $blockSize  Block size in bytes. This should always be 16 since we only deal with 128-bit AES
 | |
| 	 *                              here.
 | |
| 	 * @param   string  $iv         The initial vector. Use Randval::generate($blockSize)
 | |
| 	 * @param   bool    $legacy     Use legacy key expansion? Only ever use to decrypt data encrypted with FOF 3.
 | |
| 	 *
 | |
| 	 * @return string
 | |
| 	 */
 | |
| 	public function getExpandedKey(int $blockSize, string $iv, bool $legacy = false): string
 | |
| 	{
 | |
| 		$key        = $legacy ? $this->legacyKey($this->key) : $this->key;
 | |
| 		$passLength = strlen($key);
 | |
| 
 | |
| 		if (function_exists('mb_strlen'))
 | |
| 		{
 | |
| 			$passLength = mb_strlen($key, 'ASCII');
 | |
| 		}
 | |
| 
 | |
| 		if ($passLength !== $blockSize)
 | |
| 		{
 | |
| 			$iterations = 1000;
 | |
| 			$salt       = $this->adapter->resizeKey($iv, 16);
 | |
| 			$key        = hash_pbkdf2('sha256', $this->key, $salt, $iterations, $blockSize, true);
 | |
| 		}
 | |
| 
 | |
| 		return $key;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Process the password the same way FOF 3 did.
 | |
| 	 *
 | |
| 	 * This is a very bad idea. It would get a password, calculate its SHA-256 and throw half of it away. The rest was
 | |
| 	 * used as the encryption key. In FOF 4 we use a far more sane key expansion using PKKDF2 with SHA-256 and 1000
 | |
| 	 * rounds.
 | |
| 	 *
 | |
| 	 * @param   $password
 | |
| 	 *
 | |
| 	 * @return  string
 | |
| 	 * @since   4.0.0
 | |
| 	 */
 | |
| 	private function legacyKey($password): string
 | |
| 	{
 | |
| 		$passLength = strlen($password);
 | |
| 
 | |
| 		if (function_exists('mb_strlen'))
 | |
| 		{
 | |
| 			$passLength = mb_strlen($password, 'ASCII');
 | |
| 		}
 | |
| 
 | |
| 		if ($passLength === 32)
 | |
| 		{
 | |
| 			return $password;
 | |
| 		}
 | |
| 
 | |
| 		// Legacy mode was doing something stupid, requiring a key of 32 bytes. DO NOT USE LEGACY MODE!
 | |
| 		// Legacy mode: use the sha256 of the password
 | |
| 		$key = hash('sha256', $password, true);
 | |
| 		// We have to trim or zero pad the password (we end up throwing half of it away in Rijndael-128 / AES...)
 | |
| 		$key = $this->adapter->resizeKey($key, $this->adapter->getBlockSize());
 | |
| 
 | |
| 		return $key;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Compatibility mode for servers lacking the hash_pbkdf2 PHP function (typically, the hash extension is installed but
 | |
|  * PBKDF2 was not compiled into it). This is really slow but since it's used sparingly you shouldn't notice a
 | |
|  * substantial performance degradation under most circumstances.
 | |
|  */
 | |
| if (!function_exists('hash_pbkdf2'))
 | |
| {
 | |
| 	function hash_pbkdf2($algo, $password, $salt, $count, $length = 0, $raw_output = false)
 | |
| 	{
 | |
| 		if (!in_array(strtolower($algo), hash_algos()))
 | |
| 		{
 | |
| 			trigger_error(__FUNCTION__ . '(): Unknown hashing algorithm: ' . $algo, E_USER_WARNING);
 | |
| 		}
 | |
| 
 | |
| 		if (!is_numeric($count))
 | |
| 		{
 | |
| 			trigger_error(__FUNCTION__ . '(): expects parameter 4 to be long, ' . gettype($count) . ' given', E_USER_WARNING);
 | |
| 		}
 | |
| 
 | |
| 		if (!is_numeric($length))
 | |
| 		{
 | |
| 			trigger_error(__FUNCTION__ . '(): expects parameter 5 to be long, ' . gettype($length) . ' given', E_USER_WARNING);
 | |
| 		}
 | |
| 
 | |
| 		if ($count <= 0)
 | |
| 		{
 | |
| 			trigger_error(__FUNCTION__ . '(): Iterations must be a positive integer: ' . $count, E_USER_WARNING);
 | |
| 		}
 | |
| 
 | |
| 		if ($length < 0)
 | |
| 		{
 | |
| 			trigger_error(__FUNCTION__ . '(): Length must be greater than or equal to 0: ' . $length, E_USER_WARNING);
 | |
| 		}
 | |
| 
 | |
| 		$output      = '';
 | |
| 		$block_count = $length ? ceil($length / strlen(hash($algo, '', $raw_output))) : 1;
 | |
| 
 | |
| 		for ($i = 1; $i <= $block_count; $i++)
 | |
| 		{
 | |
| 			$last = $xorsum = hash_hmac($algo, $salt . pack('N', $i), $password, true);
 | |
| 
 | |
| 			for ($j = 1; $j < $count; $j++)
 | |
| 			{
 | |
| 				$xorsum ^= ($last = hash_hmac($algo, $last, $password, true));
 | |
| 			}
 | |
| 
 | |
| 			$output .= $xorsum;
 | |
| 		}
 | |
| 
 | |
| 		if (!$raw_output)
 | |
| 		{
 | |
| 			$output = bin2hex($output);
 | |
| 		}
 | |
| 
 | |
| 		return $length ? substr($output, 0, $length) : $output;
 | |
| 	}
 | |
| }
 |