mirror of
https://github.com/itflow-org/itflow
synced 2026-08-05 15:17:17 +00:00
generateUserSessionKey() built its AES-128 key with randomString(16), which draws 12 random bytes and base64url-encodes them into 16 printable characters. Those 16 characters were handed to openssl_encrypt as the 16-byte key, so the key that wraps the site master key in the user's session carried 96 bits of entropy instead of 128. Same for the IV. The key and IV are now random_bytes(16) and stay raw for the cipher. Base64 is applied only for transport - the cookie and the session copy of the IV - and decoded again at the three read sites. Sessions issued before this change will not decode and are not supported; anyone logged in at deploy time is prompted to log in again. Nothing is stored long term in this path, so there is nothing to migrate. Also guards encryptCredentialEntry against a master key that failed to decrypt. openssl_encrypt accepts false as a key and silently encrypts under an empty key, so a session whose cookie had expired could write a credential that no key would ever recover. It now returns false instead. This was reachable before this change too, whenever a cookie expired mid-session.
228 lines
10 KiB
PHP
228 lines
10 KiB
PHP
<?php
|
|
|
|
// Cryptography, key management, credential encryption & token validation
|
|
// Split from the former monolithic functions.php
|
|
|
|
|
|
// Function to generate both crypto & URL safe random strings
|
|
function randomString(int $length = 16): string {
|
|
$bytes = random_bytes((int) ceil($length * 3 / 4));
|
|
return substr(
|
|
rtrim(strtr(base64_encode($bytes), '+/', '-_'), '='),
|
|
0,
|
|
$length
|
|
);
|
|
}
|
|
|
|
// Generate a cryptographically secure 32-char base32 secret for TOTP
|
|
function generateTotpSecret() {
|
|
$chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
$key = '';
|
|
for ($i = 0; $i < 32; $i++) {
|
|
$key .= $chars[random_int(0, strlen($chars) - 1)];
|
|
}
|
|
return $key;
|
|
}
|
|
|
|
// Called during initial setup
|
|
// Encrypts the master key with the user's password
|
|
function setupFirstUserSpecificKey($user_password, $site_encryption_master_key) {
|
|
$iv = randomString();
|
|
$salt = randomString();
|
|
|
|
//Generate 128-bit (16 byte/char) kdhash of the users password
|
|
$user_password_kdhash = hash_pbkdf2('sha256', $user_password, $salt, 100000, 16);
|
|
|
|
//Encrypt the master key with the users kdf'd hash and the IV
|
|
$ciphertext = openssl_encrypt($site_encryption_master_key, 'aes-128-cbc', $user_password_kdhash, 0, $iv);
|
|
|
|
return $salt . $iv . $ciphertext;
|
|
}
|
|
|
|
/*
|
|
* For additional users / password changes (and now the API)
|
|
* New Users: Requires the admin setting up their account have a Specific/Session key configured
|
|
* Password Changes: Will use the current info in the session.
|
|
*/
|
|
function encryptUserSpecificKey($user_password) {
|
|
$iv = randomString();
|
|
$salt = randomString();
|
|
|
|
// Get the session info.
|
|
$user_encryption_session_ciphertext = $_SESSION['user_encryption_session_ciphertext'];
|
|
$user_encryption_session_iv = base64_decode($_SESSION['user_encryption_session_iv'] ?? '');
|
|
$user_encryption_session_key = base64_decode($_COOKIE['user_encryption_session_key'] ?? '');
|
|
|
|
// Decrypt the session key to get the master key
|
|
$site_encryption_master_key = openssl_decrypt($user_encryption_session_ciphertext, 'aes-128-cbc', $user_encryption_session_key, 0, $user_encryption_session_iv);
|
|
|
|
// Generate 128-bit (16 byte/char) kdhash of the users (new) password
|
|
$user_password_kdhash = hash_pbkdf2('sha256', $user_password, $salt, 100000, 16);
|
|
|
|
// Encrypt the master key with the users kdf'd hash and the IV
|
|
$ciphertext = openssl_encrypt($site_encryption_master_key, 'aes-128-cbc', $user_password_kdhash, 0, $iv);
|
|
|
|
return $salt . $iv . $ciphertext;
|
|
}
|
|
|
|
// Given a ciphertext (incl. IV) and the user's (or API key) password, returns the site master key
|
|
// Ran at login, to facilitate generateUserSessionKey
|
|
function decryptUserSpecificKey($user_encryption_ciphertext, $user_password) {
|
|
//Get the IV, salt and ciphertext
|
|
$salt = substr($user_encryption_ciphertext, 0, 16);
|
|
$iv = substr($user_encryption_ciphertext, 16, 16);
|
|
$ciphertext = substr($user_encryption_ciphertext, 32);
|
|
|
|
//Generate 128-bit (16 byte/char) kdhash of the users password
|
|
$user_password_kdhash = hash_pbkdf2('sha256', $user_password, $salt, 100000, 16);
|
|
|
|
//Use this hash to get the original/master key
|
|
return openssl_decrypt($ciphertext, 'aes-128-cbc', $user_password_kdhash, 0, $iv);
|
|
}
|
|
|
|
/*
|
|
Generates what is probably best described as a session key (ephemeral-ish)
|
|
- Allows us to store the master key on the server whilst the user is using the application, without prompting to type their password everytime they want to decrypt a credential
|
|
- Ciphertext/IV is stored on the server in the users' session, encryption key is controlled/provided by the user as a cookie
|
|
- Only the user can decrypt their session ciphertext to get the master key
|
|
- Encryption key never hits the disk in cleartext
|
|
|
|
*/
|
|
function generateUserSessionKey($site_encryption_master_key) {
|
|
$user_encryption_session_key = random_bytes(16);
|
|
$user_encryption_session_iv = random_bytes(16);
|
|
$user_encryption_session_ciphertext = openssl_encrypt($site_encryption_master_key, 'aes-128-cbc', $user_encryption_session_key, 0, $user_encryption_session_iv);
|
|
|
|
// Store ciphertext in the user's session
|
|
$_SESSION['user_encryption_session_ciphertext'] = $user_encryption_session_ciphertext;
|
|
$_SESSION['user_encryption_session_iv'] = base64_encode($user_encryption_session_iv);
|
|
|
|
// Give the user "their" key as a cookie
|
|
include 'config.php';
|
|
|
|
if ($config_https_only) {
|
|
setcookie("user_encryption_session_key", base64_encode($user_encryption_session_key), ['path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'None']);
|
|
} else {
|
|
setcookie("user_encryption_session_key", base64_encode($user_encryption_session_key), 0, "/");
|
|
$_SESSION['alert_message'] = "Unencrypted connection flag set: Using non-secure cookies.";
|
|
}
|
|
}
|
|
|
|
// Decrypts an encrypted password (website/asset credentials), returns it as a string
|
|
function decryptCredentialEntry($credential_password_ciphertext) {
|
|
|
|
// Split the credential into IV and Ciphertext
|
|
$credential_iv = substr($credential_password_ciphertext, 0, 16);
|
|
$credential_ciphertext = $salt = substr($credential_password_ciphertext, 16);
|
|
|
|
// Get the user session info.
|
|
$user_encryption_session_ciphertext = $_SESSION['user_encryption_session_ciphertext'];
|
|
$user_encryption_session_iv = base64_decode($_SESSION['user_encryption_session_iv'] ?? '');
|
|
$user_encryption_session_key = base64_decode($_COOKIE['user_encryption_session_key'] ?? '');
|
|
|
|
// Decrypt the session key to get the master key
|
|
$site_encryption_master_key = openssl_decrypt($user_encryption_session_ciphertext, 'aes-128-cbc', $user_encryption_session_key, 0, $user_encryption_session_iv);
|
|
|
|
// Decrypt the credential password using the master key
|
|
return openssl_decrypt($credential_ciphertext, 'aes-128-cbc', $site_encryption_master_key, 0, $credential_iv);
|
|
}
|
|
|
|
// Encrypts a website/asset credential password
|
|
function encryptCredentialEntry($credential_password_cleartext) {
|
|
$iv = randomString();
|
|
|
|
// Get the user session info.
|
|
$user_encryption_session_ciphertext = $_SESSION['user_encryption_session_ciphertext'];
|
|
$user_encryption_session_iv = base64_decode($_SESSION['user_encryption_session_iv'] ?? '');
|
|
$user_encryption_session_key = base64_decode($_COOKIE['user_encryption_session_key'] ?? '');
|
|
|
|
//Decrypt the session key to get the master key
|
|
$site_encryption_master_key = openssl_decrypt($user_encryption_session_ciphertext, 'aes-128-cbc', $user_encryption_session_key, 0, $user_encryption_session_iv);
|
|
|
|
// Never write a credential under an empty key if the session didn't open
|
|
if (empty($site_encryption_master_key)) {
|
|
return false;
|
|
}
|
|
|
|
//Encrypt the website/asset credential using the master key
|
|
$ciphertext = openssl_encrypt($credential_password_cleartext, 'aes-128-cbc', $site_encryption_master_key, 0, $iv);
|
|
|
|
return $iv . $ciphertext;
|
|
}
|
|
|
|
function apiDecryptCredentialEntry($credential_ciphertext, $api_key_decrypt_hash, #[\SensitiveParameter]$api_key_decrypt_password)
|
|
{
|
|
// Split the Credential entry (username/password) into IV and Ciphertext
|
|
$credential_iv = substr($credential_ciphertext, 0, 16);
|
|
$credential_ciphertext = $salt = substr($credential_ciphertext, 16);
|
|
|
|
// Decrypt the api hash to get the master key
|
|
$site_encryption_master_key = decryptUserSpecificKey($api_key_decrypt_hash, $api_key_decrypt_password);
|
|
|
|
// Decrypt the credential password using the master key
|
|
return openssl_decrypt($credential_ciphertext, 'aes-128-cbc', $site_encryption_master_key, 0, $credential_iv);
|
|
}
|
|
|
|
function apiEncryptCredentialEntry(#[\SensitiveParameter]$credential_cleartext, $api_key_decrypt_hash, #[\SensitiveParameter]$api_key_decrypt_password) {
|
|
$iv = randomString();
|
|
|
|
// Decrypt the api hash to get the master key
|
|
$site_encryption_master_key = decryptUserSpecificKey($api_key_decrypt_hash, $api_key_decrypt_password);
|
|
|
|
// Encrypt the credential using the master key
|
|
$ciphertext = openssl_encrypt($credential_cleartext, 'aes-128-cbc', $site_encryption_master_key, 0, $iv);
|
|
|
|
return $iv . $ciphertext;
|
|
}
|
|
|
|
// Cross-Site Request Forgery check for sensitive functions
|
|
// Validates the CSRF token provided matches the one in the users session
|
|
function validateCSRFToken(?string $token = null) {
|
|
// Read the token straight from the request when the caller doesn't pass one
|
|
$token ??= $_POST['csrf_token'] ?? $_GET['csrf_token'] ?? '';
|
|
|
|
if ($token !== '' && hash_equals($_SESSION['csrf_token'] ?? '', $token)) {
|
|
return true;
|
|
}
|
|
|
|
// 403 rather than a redirect. A 302 to index.php means anything following
|
|
// redirects - browsers, curl -L, security scanners - records a final 200
|
|
// and reports the request as accepted.
|
|
if (!headers_sent()) {
|
|
http_response_code(403);
|
|
header("Content-Type: text/html; charset=UTF-8");
|
|
header("X-Content-Type-Options: nosniff");
|
|
header("Cache-Control: no-store");
|
|
}
|
|
|
|
echo '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">'
|
|
. '<title>403 Forbidden</title></head><body>'
|
|
. '<h1>403 Forbidden</h1>'
|
|
. '<p>CSRF token verification failed. Your session may have expired.</p>'
|
|
. '<p><a href="/">Return to ITFlow</a> and try again.</p>'
|
|
. '</body></html>';
|
|
|
|
exit();
|
|
}
|
|
|
|
function validateWhitelabelKey($key) {
|
|
$public_key = "-----BEGIN PUBLIC KEY-----
|
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr0k+4ZJudkdGMCFLx5b9
|
|
H/sOozvWphFJsjVIF0vPVx9J0bTdml65UdS+32JagIHfPtEUTohaMnI3IAxxCDzl
|
|
655qmtjL7RHHdx9UMIKCmtAZOtd2u6rEyZH7vB7cKA49ysKGIaQSGwTQc8DCgsrK
|
|
uxRuX04xq9T7T+zuzROw3Y9WjFy9RwrONqLuG8LqO0j7bk5LKYeLAV7u3E/QiqNx
|
|
lEljN2UVJ3FZ/LkXeg8ORkV+IHs/toRIfPs/4VQnjEwk5BU6DX2STOvbeZnTqwP3
|
|
zgjRYR/zGN5l+az6RB3+0mJRdZdv/y2aRkBlwTxx2gOrPbQAco4a/IOmkE3EbHe7
|
|
6wIDAQAP
|
|
-----END PUBLIC KEY-----";
|
|
|
|
if (openssl_public_decrypt(base64_decode($key), $decrypted, $public_key)) {
|
|
$key_info = json_decode($decrypted, true);
|
|
if ($key_info['expires'] > date('Y-m-d H:i:s', strtotime('-7 day'))) {
|
|
return $key_info;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|