diff --git a/application/controllers/AuthenticationController.php b/application/controllers/AuthenticationController.php index 4b299ed25..dfd02b344 100644 --- a/application/controllers/AuthenticationController.php +++ b/application/controllers/AuthenticationController.php @@ -45,13 +45,15 @@ 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($rememberMeOld->getAesCrypt()->getIv()); + $rememberMe->persist($iv); } } catch (RuntimeException $e) { Logger::error("Can't authenticate user via remember me cookie: %s", $e->getMessage()); @@ -60,6 +62,7 @@ class AuthenticationController extends Controller } if (! $authenticated) { + (new RememberMe())->remove(bin2hex($iv)); $this->getResponse()->setCookie(RememberMe::forget()); } } @@ -108,7 +111,7 @@ class AuthenticationController extends Controller } else { if (RememberMe::hasCookie() && $this->hasDb()) { try { - (new RememberMe())->remove(RememberMe::fromCookie()->getAesCrypt()->getIV()); + (new RememberMe())->remove(bin2hex(RememberMe::fromCookie()->getAesCrypt()->getIV())); } catch (RuntimeException $e) { // pass } diff --git a/application/controllers/ManageUserDevicesController.php b/application/controllers/ManageUserDevicesController.php index 998295c6c..db054d10f 100644 --- a/application/controllers/ManageUserDevicesController.php +++ b/application/controllers/ManageUserDevicesController.php @@ -74,7 +74,7 @@ class ManageUserDevicesController extends CompatController public function deleteAction() { - (new RememberMe())->removeSpecific($this->params->getRequired('fingerprint')); + (new RememberMe())->remove($this->params->getRequired('fingerprint')); $this->redirectNow( Url::fromPath('manage-user-devices/devices') diff --git a/application/controllers/MyDevicesController.php b/application/controllers/MyDevicesController.php index 593eda2bf..e0fb98a52 100644 --- a/application/controllers/MyDevicesController.php +++ b/application/controllers/MyDevicesController.php @@ -67,7 +67,7 @@ class MyDevicesController extends CompatController public function deleteAction() { - (new RememberMe())->removeSpecific($this->params->getRequired('fingerprint')); + (new RememberMe())->remove($this->params->getRequired('fingerprint')); $this->redirectNow('my-devices'); } diff --git a/etc/schema/mysql-upgrades/2.9.1.sql b/etc/schema/mysql-upgrades/2.9.1.sql new file mode 100644 index 000000000..a7af6ce20 --- /dev/null +++ b/etc/schema/mysql-upgrades/2.9.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE `icingaweb_rememberme` + MODIFY random_iv varchar(32) NOT NULL; diff --git a/etc/schema/mysql.schema.sql b/etc/schema/mysql.schema.sql index e45ef9a62..62168c39d 100644 --- a/etc/schema/mysql.schema.sql +++ b/etc/schema/mysql.schema.sql @@ -45,7 +45,7 @@ CREATE TABLE `icingaweb_rememberme`( id int(10) unsigned NOT NULL AUTO_INCREMENT, username varchar(254) COLLATE utf8mb4_unicode_ci NOT NULL, passphrase varchar(256) NOT NULL, - random_iv varchar(24) NOT NULL, + random_iv varchar(32) NOT NULL, http_user_agent text NOT NULL, expires_at timestamp NULL DEFAULT NULL, ctime timestamp NULL DEFAULT NULL, diff --git a/etc/schema/pgsql-upgrades/2.9.1.sql b/etc/schema/pgsql-upgrades/2.9.1.sql new file mode 100644 index 000000000..02c683fdc --- /dev/null +++ b/etc/schema/pgsql-upgrades/2.9.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE ONLY "icingaweb_rememberme" + ALTER COLUMN random_iv type character varying(32); diff --git a/etc/schema/pgsql.schema.sql b/etc/schema/pgsql.schema.sql index a1a9072c1..c1b9fa2ef 100644 --- a/etc/schema/pgsql.schema.sql +++ b/etc/schema/pgsql.schema.sql @@ -105,7 +105,7 @@ CREATE TABLE "icingaweb_rememberme" ( "id" serial, "username" character varying(254) NOT NULL, "passphrase" character varying(256) NOT NULL, - "random_iv" character varying(24) NOT NULL, + "random_iv" character varying(32) NOT NULL, "http_user_agent" text NOT NULL, "expires_at" timestamp NULL DEFAULT NULL, "ctime" timestamp NULL DEFAULT NULL, diff --git a/library/Icinga/Crypt/AesCrypt.php b/library/Icinga/Crypt/AesCrypt.php index 30607daba..cbaa58f07 100644 --- a/library/Icinga/Crypt/AesCrypt.php +++ b/library/Icinga/Crypt/AesCrypt.php @@ -51,15 +51,31 @@ class AesCrypt private $tag; /** @var string The cipher method */ - private $method = 'aes-128-gcm'; + private $method = 'AES-128-GCM'; + + const GCM_SUPPORT_VERSION = '7.1'; public function __construct($random_bytes_len = 128) { - $len = openssl_cipher_iv_length($this->method); - $this->iv = random_bytes($len); + if (version_compare(PHP_VERSION, self::GCM_SUPPORT_VERSION, '<')) { + $this->method = 'AES-128-CBC'; + } + $this->key = random_bytes($random_bytes_len); } + /** + * Force set the method + * + * @return $this + */ + public function setMethod($method) + { + $this->method = $method; + + return $this; + } + /** * Set the key * @@ -110,7 +126,8 @@ class AesCrypt public function getIV() { if (empty($this->iv)) { - throw new RuntimeException('No iv set'); + $len = openssl_cipher_iv_length($this->method); + $this->iv = random_bytes($len); } return $this->iv; @@ -162,8 +179,13 @@ class AesCrypt */ public function decrypt($data) { + if ($this->method === 'AES-128-CBC') { + return $this->decryptCBC($data); + } + $decrypt = openssl_decrypt($data, $this->method, $this->getKey(), 0, $this->getIV(), $this->getTag()); - if (is_bool($decrypt) && $decrypt === false) { + + if ($decrypt === false) { throw new RuntimeException('Decryption failed'); } @@ -195,9 +217,13 @@ class AesCrypt */ public function encrypt($data) { + if ($this->method === 'AES-128-CBC') { + return $this->encryptCBC($data); + } + $encrypt = openssl_encrypt($data, $this->method, $this->getkey(), 0, $this->getIV(), $this->tag); - if (is_bool($encrypt) && $encrypt === false) { + if ($encrypt === false) { throw new RuntimeException('Encryption failed'); } @@ -217,4 +243,37 @@ class AesCrypt { return base64_encode($this->encrypt($data)); } + + private function decryptCBC($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); + + if ($decrypt === false || ! hash_equals($hmac, $calcHmac)) { + throw new RuntimeException('Decryption failed'); + } + + return $decrypt; + } + + private function encryptCBC($data) + { + $encrypt = openssl_encrypt($data, $this->method, $this->getkey(), 0, $this->getIV()); + + if ($encrypt === false) { + throw new RuntimeException('Encryption failed'); + } + + $hmac = hash_hmac('sha256', $encrypt, $this->getkey(), true); + + return base64_encode($hmac . $encrypt); + } } diff --git a/library/Icinga/Web/RememberMe.php b/library/Icinga/Web/RememberMe.php index 608a0bb16..d11d1030b 100644 --- a/library/Icinga/Web/RememberMe.php +++ b/library/Icinga/Web/RememberMe.php @@ -72,7 +72,6 @@ class RememberMe { $data = explode('|', $_COOKIE[static::COOKIE]); $iv = base64_decode(array_pop($data)); - $tag = base64_decode(array_pop($data)); $select = (new Select()) ->from(static::TABLE) @@ -91,8 +90,23 @@ class RememberMe $rememberMe->aesCrypt = (new AesCrypt()) ->setKey(hex2bin($rs->passphrase)) - ->setTag($tag) ->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)); + } + } + $rememberMe->username = $rs->username; $rememberMe->encryptedPassword = $data[0]; @@ -140,14 +154,19 @@ class RememberMe */ public function getCookie() { + $values = [ + $this->encryptedPassword, + base64_encode($this->aesCrypt->getIV()), + ]; + + if (version_compare(PHP_VERSION, AesCrypt::GCM_SUPPORT_VERSION, '>=')) { + array_splice($values, 1, 0, base64_encode($this->aesCrypt->getTag())); + } + return (new Cookie(static::COOKIE)) ->setExpire($this->getExpiresAt()) ->setHttpOnly(true) - ->setValue(implode('|', [ - $this->encryptedPassword, - base64_encode($this->aesCrypt->getTag()), - base64_encode($this->aesCrypt->getIV()), - ])); + ->setValue(implode('|', $values)); } /** @@ -218,7 +237,7 @@ class RememberMe public function persist($iv = null) { if ($iv) { - $this->remove($iv); + $this->remove(bin2hex($iv)); } $this->getDb()->insert(static::TABLE, [ @@ -244,7 +263,7 @@ class RememberMe public function remove($iv) { $this->getDb()->delete(static::TABLE, [ - 'random_iv = ?' => bin2hex($iv) + 'random_iv = ?' => $iv ]); return $this; @@ -263,24 +282,6 @@ class RememberMe ); } - /** - * Remove specific remember me information from the database - * - * @param string $username - * - * @param $iv - * - * @return $this - */ - public function removeSpecific($iv) - { - $this->getDb()->delete(static::TABLE, [ - 'random_iv = ?' => $iv - ]); - - return $this; - } - /** * Get all users using rememberme cookie * diff --git a/library/Icinga/Web/RememberMeUserDevicesList.php b/library/Icinga/Web/RememberMeUserDevicesList.php index 3ffc18660..66609de72 100644 --- a/library/Icinga/Web/RememberMeUserDevicesList.php +++ b/library/Icinga/Web/RememberMeUserDevicesList.php @@ -5,7 +5,7 @@ namespace Icinga\Web; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; -use ipl\Web\Url; +use ipl\Web\Url as iplWebUrl; //alias is needed for php5.6 use ipl\Web\Widget\Icon; use ipl\Web\Widget\Link; @@ -124,7 +124,7 @@ class RememberMeUserDevicesList extends BaseHtmlElement $link = (new Link( new Icon('trash'), - Url::fromPath($this->getUrl()) + iplWebUrl::fromPath($this->getUrl()) ->addParams( [ 'name' => $this->getUsername(), diff --git a/library/Icinga/Web/RememberMeUserList.php b/library/Icinga/Web/RememberMeUserList.php index 3cb8a9ddf..bb95dc9b5 100644 --- a/library/Icinga/Web/RememberMeUserList.php +++ b/library/Icinga/Web/RememberMeUserList.php @@ -5,7 +5,7 @@ namespace Icinga\Web; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; -use ipl\Web\Url; +use ipl\Web\Url as iplWebUrl; //alias is needed for php5.6 use ipl\Web\Widget\Link; /** @@ -91,7 +91,7 @@ class RememberMeUserList extends BaseHtmlElement $element = Html::tag('tr'); $link = new Link( $user->username, - Url::fromPath($this->getUrl())->addParams(['name' => $user->username]), + iplWebUrl::fromPath($this->getUrl())->addParams(['name' => $user->username]), ['title' => sprintf(t('Device list of %s'), $user->username)] );