'/', '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; } /* * Longest cleartext a credential username or password may be. * Both encrypt functions above return a 16-char IV followed by base64 AES-128-CBC * ciphertext, which expands about 1.37x, so 350 is the most that still fits the * varchar(500) columns. Keep in step with the maxlength on the credential/asset forms. */ define('CREDENTIAL_ENTRY_MAX_LENGTH', 350); /* * Checks a credential's cleartext fields against what the columns can actually store. * Form maxlength is client-side only, so the CSV import, the API and any hand-rolled POST * reach the INSERT with nothing stopping an overlong value - and MySQL rejects it outright * rather than truncating, taking the request down with it. * * Returns the name of the first field that is too long, or an empty string when they all * fit. Only keys actually present are checked, so partial updates are fine. */ function checkCredentialLengths(array $fields) { // Encrypted before storage - ciphertext size follows the BYTE length of the cleartext. $byte_limits = [ 'username' => CREDENTIAL_ENTRY_MAX_LENGTH, 'password' => CREDENTIAL_ENTRY_MAX_LENGTH, ]; // Stored as given - MySQL measures varchar in CHARACTERS, not bytes. $char_limits = [ 'name' => 200, 'description' => 500, 'uri' => 500, 'uri_2' => 500, 'otp_secret' => 200, ]; foreach ($byte_limits as $field => $limit) { if (isset($fields[$field]) && strlen($fields[$field]) > $limit) { return $field; } } foreach ($char_limits as $field => $limit) { if (!isset($fields[$field]) || strlen($fields[$field]) <= $limit) { continue; // byte length caps character count, so this already fits } // Only worth counting characters once the cheap check fails. preg keeps this // free of an mbstring dependency, which nothing else in the tree relies on. if (preg_match_all('/./us', $fields[$field]) > $limit) { return $field; } } return ''; } // 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 '' . '403 Forbidden' . '

403 Forbidden

' . '

CSRF token verification failed. Your session may have expired.

' . '

Return to ITFlow and try again.

' . ''; 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; } // Atomically claim one view against a shared item's view limit. // Returns true only if this request won the view; false means the share is // inactive, expired, or out of views. The UPDATE is the claim, so concurrent // requests cannot all pass - call this before any shared content is disclosed. function claimSharedItemView($item_id) { global $mysqli; $item_id = intval($item_id); mysqli_query($mysqli, "UPDATE shared_items SET item_views = item_views + 1 WHERE item_id = $item_id AND item_active = 1 AND item_expire_at > NOW() AND (COALESCE(item_view_limit, 0) = 0 OR item_views < item_view_limit)" ); // -1 (query error) and 0 (limit reached / revoked / expired) both deny return mysqli_affected_rows($mysqli) === 1; }