feat(session): implement persistent cookies with hmac (replaced AES encryption)

This commit is contained in:
eapl.mx 2024-12-24 14:31:38 -06:00
parent 5e4402c230
commit 1769eeb831
No known key found for this signature in database
3 changed files with 72 additions and 53 deletions

View file

@ -9,12 +9,13 @@ if (!empty($missing_keys)) {
die('Missing required keys in config.ini: ' . implode(', ', $missing_keys)); die('Missing required keys in config.ini: ' . implode(', ', $missing_keys));
} }
# To make it more secure, something like JWT could be used instead
const COOKIE_NAME = 'timeline_login'; const COOKIE_NAME = 'timeline_login';
const ENCRYPTION_METHOD = 'aes-256-cbc'; const ENCRYPTION_METHOD = 'aes-256-cbc';
const EXPIRATION_DAYS = 30; const EXPIRATION_DAYS = 30;
const HASH_LENGTH = 128;
const HASH_ALGORITHM = 'sha512';
session_start([ session_start([
'name' => 'timeline_session', 'name' => 'timeline_session',
'use_strict_mode' => true, 'use_strict_mode' => true,
@ -25,50 +26,80 @@ session_start([
'cookie_samesite' => 'Strict', # Not compatible with PHP < 7.3 'cookie_samesite' => 'Strict', # Not compatible with PHP < 7.3
]); ]);
function hasValidSession(): bool|string { function hasValidSession(): bool {
# If short lived session is valid # If short lived session is valid
if (isset($_SESSION['valid_session'])) { if (isset($_SESSION['session_expiration'])) {
return true; return true;
} }
# TODO: Check if the session has expired
# Add more protection to prevent session fixation
# https://owasp.org/www-community/attacks/Session_fixation
# Otherwise, check the persistent cookie # Otherwise, check the persistent cookie
return isSavedCookieValid(); return isSavedCookieValid();
} }
function encrypt(string $data, string $key, string $method): string { function getCookieData() {
$ivSize = openssl_cipher_iv_length($method); if (!isset($_COOKIE[COOKIE_NAME])) {
$iv = openssl_random_pseudo_bytes($ivSize); #echo "Cookie " . COOKIE_NAME . " not found";
$encrypted = openssl_encrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv);
$encrypted = strtoupper(implode(unpack('H*', $encrypted)));
return $encrypted;
}
function decrypt(string $data, string $key, string $method): string | bool {
$data = pack('H*', $data);
$ivSize = openssl_cipher_iv_length($method);
$iv = openssl_random_pseudo_bytes($ivSize);
$decrypted = openssl_decrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv);
var_dump($decrypted);
if ($decrypted === false) {
return false; return false;
} }
return trim($decrypted); $raw = base64_decode($_COOKIE[COOKIE_NAME]);
} #var_dump($raw);
function saveLoginSuccess() { # Cookie should be at least the size of the hash length.
$_SESSION['valid_session'] = true; # If it's not, we can just bail out
if (strlen($raw) < HASH_LENGTH) {
#echo "Didn't get minimum length";
return false;
}
$config = parse_ini_file('private/config.ini'); $config = parse_ini_file('private/config.ini');
# Set a cookie to remember the user # The cookie data contains the actual data w/ the hash concatonated to the end,
$cookieExpiry = EXPIRATION_DAYS * 24 * 60 * 60 + time(); # since the hash is a fixed length, we can extract the last hash_length chars
$encodedCookieValue = generateCookieValue(strval($cookieExpiry), $config['secret_key']); # to get the hash.
$hash = substr($raw, strlen($raw) - HASH_LENGTH, HASH_LENGTH);
$data = substr($raw, 0, - (HASH_LENGTH));
setcookie(COOKIE_NAME, $encodedCookieValue, [ # Calculate what the hash should be, based on the data. If the data has not been
# tampered with, $hash and $hash_calculated will be the same
$hash_calculated = hash_hmac(HASH_ALGORITHM, $data, $config['secret_key']);
# If we calculate a different hash, we can't trust the data.
if ($hash_calculated !== $hash) {
#echo "Different HASH";
return False;
}
# Is it expired ?
if (intval($data) < time()) {
#echo "Cookie expired";
return False;
}
return $data;
}
function makePersistentCookie() {
$config = parse_ini_file('private/config.ini');
$cookieExpiry = EXPIRATION_DAYS * 24 * 60 * 60 + time(); # X days
#$cookieExpiry = 10 + time(); # Debug value - 5 minutes
# Calculate a hash for the data and append it to the end of the data string
$cookieValue = strval($cookieExpiry);
$hash = hash_hmac(HASH_ALGORITHM, $cookieValue, $config['secret_key']);
$cookieValue .= $hash;
$cookieValue = base64_encode($cookieValue);
# Also create the short-timed session
$_SESSION['session_expiration'] = $cookieExpiry;
return setcookie(COOKIE_NAME, $cookieValue, [
'expires' => $cookieExpiry, 'expires' => $cookieExpiry,
'secure' => $config['secure_cookies'], 'secure' => $config['secure_cookies'],
'httponly' => true, 'httponly' => true,
@ -76,35 +107,22 @@ function saveLoginSuccess() {
]); ]);
} }
function generateCookieValue($value, $secretKey) { function saveLogin() {
$key = bin2hex($secretKey); makePersistentCookie();
$encrypted = encrypt($value, $key, ENCRYPTION_METHOD);
return $encrypted;
} }
function isSavedCookieValid() { function isSavedCookieValid() {
if (!isset($_COOKIE[COOKIE_NAME])) { $cookieExpiry = getCookieData();
return false;
} if ($cookieExpiry === false) {
$config = parse_ini_file('private/config.ini');
$encoded_cookie_value = $_COOKIE[COOKIE_NAME];
$key = bin2hex($config['secret_key']);
$cookieVal = decrypt($encoded_cookie_value, $key, ENCRYPTION_METHOD);
if ($cookieVal === false) {
deletePersistentCookie(); deletePersistentCookie();
return false; return false;
} }
# TODO: Check that the cookie is not expired # Refresh session
$_SESSION['session_expiration'] = intval($cookieExpiry);
saveLoginSuccess(); # Extend expiracy for previous cookie return true;
return true; # If it was decoded correctly, it's a valid session
} }
function deletePersistentCookie() { function deletePersistentCookie() {

View file

@ -14,14 +14,14 @@ if (isset($_POST['submit_pass']) && $_POST['pass']) {
if ($passwordInForm == $passwordInConfig) { if ($passwordInForm == $passwordInConfig) {
$_SESSION['password'] = $passwordInForm; $_SESSION['password'] = $passwordInForm;
saveLoginSuccess(); saveLogin();
} elseif ($isCodeValid = verifyTOTP( } elseif ($isCodeValid = verifyTOTP(
$config['totp_secret'], $config['totp_secret'],
$passwordInForm, $passwordInForm,
intval($config['totp_digits']) intval($config['totp_digits'])
)) { )) {
$_SESSION['password'] = 'valid_totp'; $_SESSION['password'] = 'valid_totp';
saveLoginSuccess(); saveLogin();
} else { } else {
$error = 'Incorrect Password'; $error = 'Incorrect Password';
} }

View file

@ -4,6 +4,7 @@
require_once "libs/persistent_session.php"; require_once "libs/persistent_session.php";
if (!hasValidSession()) { if (!hasValidSession()) {
echo "Not a valid session - Go to /login";
#header("Location: /login"); #header("Location: /login");
exit; exit;
} }