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()) {
$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());
}

View File

@ -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;
}
}

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()
{
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
*/