Utilize multiple encryption ciphers for remember me

This commit is contained in:
Sukhwinder Dhillon 2021-08-09 16:39:46 +02:00 committed by Johannes Meyer
parent 1e1b4b74ad
commit 8c22514758
3 changed files with 164 additions and 78 deletions

View File

@ -45,15 +45,13 @@ class AuthenticationController extends Controller
if (RememberMe::hasCookie() && $this->hasDb()) { if (RememberMe::hasCookie() && $this->hasDb()) {
$authenticated = false; $authenticated = false;
$iv = null;
try { try {
$rememberMeOld = RememberMe::fromCookie(); $rememberMeOld = RememberMe::fromCookie();
$iv = $rememberMeOld->getAesCrypt()->getIv();
$authenticated = $rememberMeOld->authenticate(); $authenticated = $rememberMeOld->authenticate();
if ($authenticated) { if ($authenticated) {
$rememberMe = $rememberMeOld->renew(); $rememberMe = $rememberMeOld->renew();
$this->getResponse()->setCookie($rememberMe->getCookie()); $this->getResponse()->setCookie($rememberMe->getCookie());
$rememberMe->persist($iv); $rememberMe->persist($rememberMeOld->getAesCrypt()->getIv());
} }
} catch (RuntimeException $e) { } catch (RuntimeException $e) {
Logger::error("Can't authenticate user via remember me cookie: %s", $e->getMessage()); Logger::error("Can't authenticate user via remember me cookie: %s", $e->getMessage());
@ -62,7 +60,6 @@ class AuthenticationController extends Controller
} }
if (! $authenticated) { if (! $authenticated) {
(new RememberMe())->remove(bin2hex($iv));
$this->getResponse()->setCookie(RememberMe::forget()); $this->getResponse()->setCookie(RememberMe::forget());
} }
} }
@ -110,12 +107,6 @@ class AuthenticationController extends Controller
$this->getResponse()->setHttpResponseCode(401); $this->getResponse()->setHttpResponseCode(401);
} else { } else {
if (RememberMe::hasCookie() && $this->hasDb()) { if (RememberMe::hasCookie() && $this->hasDb()) {
try {
(new RememberMe())->remove(bin2hex(RememberMe::fromCookie()->getAesCrypt()->getIV()));
} catch (RuntimeException $e) {
// pass
}
$this->getResponse()->setCookie(RememberMe::forget()); $this->getResponse()->setCookie(RememberMe::forget());
} }

View File

@ -14,7 +14,7 @@ use RuntimeException;
* ```php * ```php
* *
* // Encryption * // Encryption
* $encryptedData = new AesCrypt()->encrypt($data); // Accepts a string * $encryptedData = (new AesCrypt())->encrypt($data); // Accepts a string
* *
* *
* // Encrypt and encode to Base64 * // Encrypt and encode to Base64
@ -23,7 +23,7 @@ use RuntimeException;
* *
* // Decryption * // Decryption
* $aesCrypt = (new AesCrypt()) * $aesCrypt = (new AesCrypt())
* ->setTag($tag) * ->setTag($tag) // if exists
* ->setIV($iv) * ->setIV($iv)
* ->setKey($key); * ->setKey($key);
* *
@ -35,15 +35,25 @@ use RuntimeException;
* ->setIV($iv) * ->setIV($iv)
* ->setKey($key); * ->setKey($key);
* *
* $decryptedData = $aesCrypt->->decryptFromBase64($data); * $decryptedData = $aesCrypt->decryptFromBase64($data);
* ``` * ```
* *
*/ */
class AesCrypt 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 */ /** @var string The encryption key */
private $key; private $key;
/** @var int The length of the key */
private $keyLength;
/** @var string The initialization vector which is not NULL */ /** @var string The initialization vector which is not NULL */
private $iv; private $iv;
@ -51,21 +61,15 @@ class AesCrypt
private $tag; private $tag;
/** @var string The cipher method */ /** @var string The cipher method */
private $method = 'AES-128-GCM'; private $method;
const GCM_SUPPORT_VERSION = '7.1'; public function __construct($keyLength = 128)
public function __construct($random_bytes_len = 128)
{ {
if (version_compare(PHP_VERSION, self::GCM_SUPPORT_VERSION, '<')) { $this->keyLength = $keyLength;
$this->method = 'AES-128-CBC';
}
$this->key = random_bytes($random_bytes_len);
} }
/** /**
* Force set the method * Set the method
* *
* @return $this * @return $this
*/ */
@ -76,6 +80,45 @@ class AesCrypt
return $this; 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 * Set the key
* *
@ -93,12 +136,11 @@ class AesCrypt
* *
* @return string * @return string
* *
* @throws RuntimeException If the key is not set
*/ */
public function getKey() public function getKey()
{ {
if (empty($this->key)) { if (empty($this->key)) {
throw new RuntimeException('No key set'); $this->key = random_bytes($this->keyLength);
} }
return $this->key; return $this->key;
@ -121,12 +163,11 @@ class AesCrypt
* *
* @return string * @return string
* *
* @throws RuntimeException If the IV is not set
*/ */
public function getIV() public function getIV()
{ {
if (empty($this->iv)) { if (empty($this->iv)) {
$len = openssl_cipher_iv_length($this->method); $len = openssl_cipher_iv_length($this->getMethod());
$this->iv = random_bytes($len); $this->iv = random_bytes($len);
} }
@ -137,9 +178,20 @@ class AesCrypt
* Set the Tag * Set the Tag
* *
* @return $this * @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) 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) { if (strlen($tag) !== 16) {
throw new UnexpectedValueException(sprintf( throw new UnexpectedValueException(sprintf(
'expects tag length to be 16, got instead %s', '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 * @param string $data
* *
@ -179,11 +231,11 @@ class AesCrypt
*/ */
public function decrypt($data) public function decrypt($data)
{ {
if ($this->method === 'AES-128-CBC') { if (! $this->isAuthenticatedEncryptionRequired()) {
return $this->decryptCBC($data); 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) { if ($decrypt === false) {
throw new RuntimeException('Decryption failed'); 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 * @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 * @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 * @param string $data
* *
* @return string encrypted data * @return string encrypted string
* *
* @throws RuntimeException If decryption fails * @throws RuntimeException If decryption fails
*/ */
public function encrypt($data) public function encrypt($data)
{ {
if ($this->method === 'AES-128-CBC') { if (! $this->isAuthenticatedEncryptionRequired()) {
return $this->encryptCBC($data); 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) { if ($encrypt === false) {
throw new RuntimeException('Encryption failed'); 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 * @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 * @throws RuntimeException If encryption fails
*/ */
@ -244,18 +300,23 @@ class AesCrypt
return base64_encode($this->encrypt($data)); 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); $c = base64_decode($data);
$hmac = substr($c, 0, 32); $hmac = substr($c, 0, 32);
$data = substr($c, 32); $data = substr($c, 32);
$decrypt = openssl_decrypt($data, $this->method, $this->getKey(), 0, $this->getIV()); $decrypt = openssl_decrypt($data, $this->getMethod(), $this->getKey(), 0, $this->getIV());
$calcHmac = hash_hmac('sha256', $data, $this->getKey(), true); $calcHmac = hash_hmac('sha256', $this->getIV() . $data, $this->getKey(), true);
if ($decrypt === false || ! hash_equals($hmac, $calcHmac)) { if ($decrypt === false || ! hash_equals($hmac, $calcHmac)) {
throw new RuntimeException('Decryption failed'); throw new RuntimeException('Decryption failed');
@ -264,16 +325,45 @@ class AesCrypt
return $decrypt; 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) { if ($encrypt === false) {
throw new RuntimeException('Encryption failed'); 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); 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;
}
} }

View File

@ -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() 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]); unset($_COOKIE[static::COOKIE]);
return (new Cookie(static::COOKIE)) return (new Cookie(static::COOKIE))
@ -92,19 +98,15 @@ class RememberMe
->setKey(hex2bin($rs->passphrase)) ->setKey(hex2bin($rs->passphrase))
->setIV($iv); ->setIV($iv);
if (version_compare(PHP_VERSION, AesCrypt::GCM_SUPPORT_VERSION, '>=')) { if (count($data) > 1) {
$tag = array_pop($data); $rememberMe->aesCrypt->setTag(
base64_decode(array_pop($data))
if (empty($data)) { );
$rememberMe->aesCrypt = (new AesCrypt()) } elseif ($rememberMe->aesCrypt->isAuthenticatedEncryptionRequired()) {
->setMethod('AES-128-CBC') throw new RuntimeException(
->setKey(hex2bin($rs->passphrase)) "The given decryption method needs a tag, but is not specified. "
->setIV($iv); . "You have probably updated the PHP version."
);
$data[0] = $tag; // encryptedPass
} else {
$rememberMe->aesCrypt->setTag(base64_decode($tag));
}
} }
$rememberMe->username = $rs->username; $rememberMe->username = $rs->username;
@ -125,7 +127,7 @@ class RememberMe
{ {
$aesCrypt = new AesCrypt(); $aesCrypt = new AesCrypt();
$rememberMe = new static(); $rememberMe = new static();
$rememberMe->encryptedPassword = $aesCrypt->encryptToBase64($password); $rememberMe->encryptedPassword = $aesCrypt->encrypt($password);
$rememberMe->username = $username; $rememberMe->username = $username;
$rememberMe->aesCrypt = $aesCrypt; $rememberMe->aesCrypt = $aesCrypt;
@ -159,7 +161,7 @@ class RememberMe
base64_encode($this->aesCrypt->getIV()), 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())); array_splice($values, 1, 0, base64_encode($this->aesCrypt->getTag()));
} }
@ -208,7 +210,6 @@ class RememberMe
*/ */
public function authenticate() public function authenticate()
{ {
$password = $this->aesCrypt->decryptFromBase64($this->encryptedPassword);
$auth = Auth::getInstance(); $auth = Auth::getInstance();
$authChain = $auth->getAuthChain(); $authChain = $auth->getAuthChain();
$authChain->setSkipExternalBackends(true); $authChain->setSkipExternalBackends(true);
@ -217,7 +218,11 @@ class RememberMe
$user->setDomain(Config::app()->get('authentication', 'default_domain')); $user->setDomain(Config::app()->get('authentication', 'default_domain'));
} }
$authenticated = $authChain->authenticate($user, $password); $authenticated = $authChain->authenticate(
$user,
$this->aesCrypt->decrypt($this->encryptedPassword)
);
if ($authenticated) { if ($authenticated) {
$auth->setAuthenticated($user); $auth->setAuthenticated($user);
} }
@ -228,9 +233,9 @@ class RememberMe
/** /**
* Persist the remember me information into the database * 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 * @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 * @param string $iv
* *
@ -278,14 +283,14 @@ class RememberMe
{ {
return static::fromCredentials( return static::fromCredentials(
$this->username, $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() 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 * @param $username
* *
* @return array * @return array Array of database entries
*/ */
public static function getAllByUsername($username) public static function getAllByUsername($username)
{ {
@ -325,7 +330,7 @@ class RememberMe
} }
/** /**
* Get the encrypton/decryption instance * Get the AesCrypt instance
* *
* @return AesCrypt * @return AesCrypt
*/ */