diff --git a/application/controllers/AuthenticationController.php b/application/controllers/AuthenticationController.php index dfd02b344..352bf6c95 100644 --- a/application/controllers/AuthenticationController.php +++ b/application/controllers/AuthenticationController.php @@ -45,15 +45,13 @@ class AuthenticationController extends Controller if (RememberMe::hasCookie() && $this->hasDb()) { $authenticated = false; - $iv = null; try { $rememberMeOld = RememberMe::fromCookie(); - $iv = $rememberMeOld->getAesCrypt()->getIv(); $authenticated = $rememberMeOld->authenticate(); if ($authenticated) { $rememberMe = $rememberMeOld->renew(); $this->getResponse()->setCookie($rememberMe->getCookie()); - $rememberMe->persist($iv); + $rememberMe->persist($rememberMeOld->getAesCrypt()->getIv()); } } catch (RuntimeException $e) { Logger::error("Can't authenticate user via remember me cookie: %s", $e->getMessage()); @@ -62,7 +60,6 @@ class AuthenticationController extends Controller } if (! $authenticated) { - (new RememberMe())->remove(bin2hex($iv)); $this->getResponse()->setCookie(RememberMe::forget()); } } @@ -110,12 +107,6 @@ class AuthenticationController extends Controller $this->getResponse()->setHttpResponseCode(401); } else { if (RememberMe::hasCookie() && $this->hasDb()) { - try { - (new RememberMe())->remove(bin2hex(RememberMe::fromCookie()->getAesCrypt()->getIV())); - } catch (RuntimeException $e) { - // pass - } - $this->getResponse()->setCookie(RememberMe::forget()); } diff --git a/library/Icinga/Crypt/AesCrypt.php b/library/Icinga/Crypt/AesCrypt.php index cbaa58f07..b2ba0eaca 100644 --- a/library/Icinga/Crypt/AesCrypt.php +++ b/library/Icinga/Crypt/AesCrypt.php @@ -14,7 +14,7 @@ use RuntimeException; * ```php * * // Encryption - * $encryptedData = new AesCrypt()->encrypt($data); // Accepts a string + * $encryptedData = (new AesCrypt())->encrypt($data); // Accepts a string * * * // Encrypt and encode to Base64 @@ -23,7 +23,7 @@ use RuntimeException; * * // Decryption * $aesCrypt = (new AesCrypt()) - * ->setTag($tag) + * ->setTag($tag) // if exists * ->setIV($iv) * ->setKey($key); * @@ -35,15 +35,25 @@ use RuntimeException; * ->setIV($iv) * ->setKey($key); * - * $decryptedData = $aesCrypt->->decryptFromBase64($data); + * $decryptedData = $aesCrypt->decryptFromBase64($data); * ``` * */ class AesCrypt { + /** @var array The list of cipher methods */ + const METHODS = [ + 'aes-256-gcm', + 'aes-256-cbc', + 'aes-256-ctr' + ]; + /** @var string The encryption key */ private $key; + /** @var int The length of the key */ + private $keyLength; + /** @var string The initialization vector which is not NULL */ private $iv; @@ -51,21 +61,15 @@ class AesCrypt private $tag; /** @var string The cipher method */ - private $method = 'AES-128-GCM'; + private $method; - const GCM_SUPPORT_VERSION = '7.1'; - - public function __construct($random_bytes_len = 128) + public function __construct($keyLength = 128) { - if (version_compare(PHP_VERSION, self::GCM_SUPPORT_VERSION, '<')) { - $this->method = 'AES-128-CBC'; - } - - $this->key = random_bytes($random_bytes_len); + $this->keyLength = $keyLength; } /** - * Force set the method + * Set the method * * @return $this */ @@ -76,6 +80,45 @@ class AesCrypt return $this; } + /** + * Get the method + * + * @return string + */ + public function getMethod() + { + if ($this->method === null) { + $this->method = $this->getSupportedMethod(); + } + + return $this->method; + } + + /** + * Get supported method + * + * @return string + * + * @throws RuntimeException If none of the methods listed in the METHODS array is available + */ + protected function getSupportedMethod() + { + $availableMethods = openssl_get_cipher_methods(); + $methods = self::METHODS; + + if (! $this->isAuthenticatedEncryptionSupported()) { + unset($methods[0]); + } + + foreach ($methods as $method) { + if (in_array($method, $availableMethods)) { + return $method; + } + } + + throw new RuntimeException('No supported method found'); + } + /** * Set the key * @@ -93,12 +136,11 @@ class AesCrypt * * @return string * - * @throws RuntimeException If the key is not set */ public function getKey() { if (empty($this->key)) { - throw new RuntimeException('No key set'); + $this->key = random_bytes($this->keyLength); } return $this->key; @@ -121,12 +163,11 @@ class AesCrypt * * @return string * - * @throws RuntimeException If the IV is not set */ public function getIV() { if (empty($this->iv)) { - $len = openssl_cipher_iv_length($this->method); + $len = openssl_cipher_iv_length($this->getMethod()); $this->iv = random_bytes($len); } @@ -137,9 +178,20 @@ class AesCrypt * Set the Tag * * @return $this + * + * @throws RuntimeException If a tag is available but authenticated encryption (AE) is not supported. + * + * @throws UnexpectedValueException If tag length is less then 16 */ public function setTag($tag) { + if (! $this->isAuthenticatedEncryptionSupported()) { + throw new RuntimeException(sprintf( + "The given decryption method is not supported in php version '%s'", + PHP_VERSION + )); + } + if (strlen($tag) !== 16) { throw new UnexpectedValueException(sprintf( 'expects tag length to be 16, got instead %s', @@ -169,7 +221,7 @@ class AesCrypt } /** - * Decrypt the given data using the key, iv and tag + * Decrypt the given string * * @param string $data * @@ -179,11 +231,11 @@ class AesCrypt */ public function decrypt($data) { - if ($this->method === 'AES-128-CBC') { - return $this->decryptCBC($data); + if (! $this->isAuthenticatedEncryptionRequired()) { + return $this->nonAEDecrypt($data); } - $decrypt = openssl_decrypt($data, $this->method, $this->getKey(), 0, $this->getIV(), $this->getTag()); + $decrypt = openssl_decrypt($data, $this->getMethod(), $this->getKey(), 0, $this->getIV(), $this->getTag()); if ($decrypt === false) { throw new RuntimeException('Decryption failed'); @@ -193,11 +245,13 @@ class AesCrypt } /** - * Decode from Base64 and decrypt the given data using the key, iv and tag + * Decode from Base64 and decrypt the given string * * @param string $data * - * @return string decrypted data + * @return string The base64 decoded and decrypted string + * + * @deprecated Use decrypt() instead as it also returns a base64 decoded string * * @throws RuntimeException If decryption fails */ @@ -207,21 +261,21 @@ class AesCrypt } /** - * Encrypt the given data using the key, iv and tag + * Encrypt the given string * * @param string $data * - * @return string encrypted data + * @return string encrypted string * * @throws RuntimeException If decryption fails */ public function encrypt($data) { - if ($this->method === 'AES-128-CBC') { - return $this->encryptCBC($data); + if (! $this->isAuthenticatedEncryptionRequired()) { + return $this->nonAEEncrypt($data); } - $encrypt = openssl_encrypt($data, $this->method, $this->getkey(), 0, $this->getIV(), $this->tag); + $encrypt = openssl_encrypt($data, $this->getMethod(), $this->getKey(), 0, $this->getIV(), $this->tag); if ($encrypt === false) { throw new RuntimeException('Encryption failed'); @@ -231,11 +285,13 @@ class AesCrypt } /** - * Encrypt the given string using the the key, iv, tag and encode to Base64 + * Encrypt the given string and encode to Base64 * * @param string $data * - * @return string encrypted and encoded to Base64 data + * @return string encrypted and base64 encoded string + * + * @deprecated Use encrypt() instead as it also returns a base64 encoded string * * @throws RuntimeException If encryption fails */ @@ -244,18 +300,23 @@ class AesCrypt return base64_encode($this->encrypt($data)); } - private function decryptCBC($data) + /** + * Decrypt the given string with non Authenticated encryption (AE) cipher method + * + * @param string $data + * + * @return string decrypted string + * + * @throws RuntimeException If decryption fails + */ + private function nonAEDecrypt($data) { - if (strlen($this->getIV()) !== 16) { - throw new RuntimeException('Decryption failed'); - } - $c = base64_decode($data); $hmac = substr($c, 0, 32); $data = substr($c, 32); - $decrypt = openssl_decrypt($data, $this->method, $this->getKey(), 0, $this->getIV()); - $calcHmac = hash_hmac('sha256', $data, $this->getKey(), true); + $decrypt = openssl_decrypt($data, $this->getMethod(), $this->getKey(), 0, $this->getIV()); + $calcHmac = hash_hmac('sha256', $this->getIV() . $data, $this->getKey(), true); if ($decrypt === false || ! hash_equals($hmac, $calcHmac)) { throw new RuntimeException('Decryption failed'); @@ -264,16 +325,45 @@ class AesCrypt return $decrypt; } - private function encryptCBC($data) + /** + * Encrypt the given string with non Authenticated encryption (AE) cipher method + * + * @param string $data + * + * @return string encrypted string + * + * @throws RuntimeException If encryption fails + */ + private function nonAEEncrypt($data) { - $encrypt = openssl_encrypt($data, $this->method, $this->getkey(), 0, $this->getIV()); + $encrypt = openssl_encrypt($data, $this->getMethod(), $this->getKey(), 0, $this->getIV()); if ($encrypt === false) { throw new RuntimeException('Encryption failed'); } - $hmac = hash_hmac('sha256', $encrypt, $this->getkey(), true); + $hmac = hash_hmac('sha256', $this->getIV() . $encrypt, $this->getKey(), true); return base64_encode($hmac . $encrypt); } + + /** + * Whether the Authenticated encryption (a tag) is required + * + * @return bool True if required false otherwise + */ + public function isAuthenticatedEncryptionRequired() + { + return $this->getMethod() === 'aes-256-gcm'; + } + + /** + * Whether the php version supports Authenticated encryption (AE) or not + * + * @return bool True if supported false otherwise + */ + public function isAuthenticatedEncryptionSupported() + { + return PHP_VERSION_ID >= 70100; + } } diff --git a/library/Icinga/Web/RememberMe.php b/library/Icinga/Web/RememberMe.php index d11d1030b..b4a1f8337 100644 --- a/library/Icinga/Web/RememberMe.php +++ b/library/Icinga/Web/RememberMe.php @@ -50,12 +50,18 @@ class RememberMe } /** - * Unset the remember me cookie from PHP's `$_COOKIE` superglobal and return the invalidation cookie + * Remove the database entry if exists and unset the remember me cookie from PHP's `$_COOKIE` superglobal * - * @return Cookie Cookie which has to be sent to client in oder to remove the remember me cookie + * @return Cookie The invalidation cookie which has to be sent to client in oder to remove the remember me cookie */ public static function forget() { + if (self::hasCookie()) { + $data = explode('|', $_COOKIE[static::COOKIE]); + $iv = base64_decode(array_pop($data)); + (new self())->remove(bin2hex($iv)); + } + unset($_COOKIE[static::COOKIE]); return (new Cookie(static::COOKIE)) @@ -92,19 +98,15 @@ class RememberMe ->setKey(hex2bin($rs->passphrase)) ->setIV($iv); - if (version_compare(PHP_VERSION, AesCrypt::GCM_SUPPORT_VERSION, '>=')) { - $tag = array_pop($data); - - if (empty($data)) { - $rememberMe->aesCrypt = (new AesCrypt()) - ->setMethod('AES-128-CBC') - ->setKey(hex2bin($rs->passphrase)) - ->setIV($iv); - - $data[0] = $tag; // encryptedPass - } else { - $rememberMe->aesCrypt->setTag(base64_decode($tag)); - } + if (count($data) > 1) { + $rememberMe->aesCrypt->setTag( + base64_decode(array_pop($data)) + ); + } elseif ($rememberMe->aesCrypt->isAuthenticatedEncryptionRequired()) { + throw new RuntimeException( + "The given decryption method needs a tag, but is not specified. " + . "You have probably updated the PHP version." + ); } $rememberMe->username = $rs->username; @@ -125,7 +127,7 @@ class RememberMe { $aesCrypt = new AesCrypt(); $rememberMe = new static(); - $rememberMe->encryptedPassword = $aesCrypt->encryptToBase64($password); + $rememberMe->encryptedPassword = $aesCrypt->encrypt($password); $rememberMe->username = $username; $rememberMe->aesCrypt = $aesCrypt; @@ -159,7 +161,7 @@ class RememberMe base64_encode($this->aesCrypt->getIV()), ]; - if (version_compare(PHP_VERSION, AesCrypt::GCM_SUPPORT_VERSION, '>=')) { + if ($this->aesCrypt->isAuthenticatedEncryptionRequired()) { array_splice($values, 1, 0, base64_encode($this->aesCrypt->getTag())); } @@ -208,7 +210,6 @@ class RememberMe */ public function authenticate() { - $password = $this->aesCrypt->decryptFromBase64($this->encryptedPassword); $auth = Auth::getInstance(); $authChain = $auth->getAuthChain(); $authChain->setSkipExternalBackends(true); @@ -217,7 +218,11 @@ class RememberMe $user->setDomain(Config::app()->get('authentication', 'default_domain')); } - $authenticated = $authChain->authenticate($user, $password); + $authenticated = $authChain->authenticate( + $user, + $this->aesCrypt->decrypt($this->encryptedPassword) + ); + if ($authenticated) { $auth->setAuthenticated($user); } @@ -228,9 +233,9 @@ class RememberMe /** * Persist the remember me information into the database * - * Any previous stored information is automatically removed. + * To remove any previous stored information, set the iv * - * @param string|null $iv + * @param string|null $iv To remove a specific iv record from the database * * @return $this */ @@ -254,7 +259,7 @@ class RememberMe } /** - * Remove remember me information from the database + * Remove remember me information from the database on the basis of iv * * @param string $iv * @@ -278,14 +283,14 @@ class RememberMe { return static::fromCredentials( $this->username, - $this->aesCrypt->decryptFromBase64($this->encryptedPassword) + $this->aesCrypt->decrypt($this->encryptedPassword) ); } /** - * Get all users using rememberme cookie + * Get all users using remember me cookie * - * @return array + * @return array Array of users */ public static function getAllUser() { @@ -303,11 +308,11 @@ class RememberMe } /** - * Get all rememberme cookies of the given user + * Get all remember me entries from the database of the given user. * * @param $username * - * @return array + * @return array Array of database entries */ public static function getAllByUsername($username) { @@ -325,7 +330,7 @@ class RememberMe } /** - * Get the encrypton/decryption instance + * Get the AesCrypt instance * * @return AesCrypt */