From 474209dd2b3f14d3c5b08c6eda975ce887eabc46 Mon Sep 17 00:00:00 2001 From: Ivan Carlos Date: Sun, 11 Jan 2026 23:45:56 +0000 Subject: [PATCH] Upload files to "/" --- authenticator.php | 243 ++++++++++++++++++++++++++++++++++++++++++++++ manifest.json | 2 +- plugin.php | 125 +++++++++++++++++++++++- 3 files changed, 365 insertions(+), 5 deletions(-) create mode 100644 authenticator.php diff --git a/authenticator.php b/authenticator.php new file mode 100644 index 0000000..e323ef9 --- /dev/null +++ b/authenticator.php @@ -0,0 +1,243 @@ +_getBase32LookupTable(); + + // Valid secret lengths are 80 to 640 bits + if ($secretLength < 16 || $secretLength > 128) { + throw new Exception('Bad secret length'); + } + $secret = ''; + $rnd = false; + if (function_exists('random_bytes')) { + $rnd = random_bytes($secretLength); + } elseif (function_exists('mcrypt_create_iv')) { + $rnd = mcrypt_create_iv($secretLength, MCRYPT_DEV_URANDOM); + } elseif (function_exists('openssl_random_pseudo_bytes')) { + $rnd = openssl_random_pseudo_bytes($secretLength, $cryptoStrong); + if (!$cryptoStrong) { + $rnd = false; + } + } + if ($rnd !== false) { + for ($i = 0; $i < $secretLength; ++$i) { + $secret .= $validChars[ord($rnd[$i]) & 31]; + } + } else { + throw new Exception('No source of secure random'); + } + + return $secret; + } + + /** + * Calculate the code, with given secret and point in time. + * + * @param string $secret + * @param int|null $timeSlice + * + * @return string + */ + public function getCode($secret, $timeSlice = null) + { + if ($timeSlice === null) { + $timeSlice = floor(time() / 30); + } + + $secretkey = $this->_base32Decode($secret); + + // Pack time into binary string + $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice); + // Hash it with users secret key + $hm = hash_hmac('SHA1', $time, $secretkey, true); + // Use last nipple of result as index/offset + $offset = ord(substr($hm, -1)) & 0x0F; + // grab 4 bytes of the result + $hashpart = substr($hm, $offset, 4); + + // Unpak binary value + $value = unpack('N', $hashpart); + $value = $value[1]; + // Only 32 bits + $value = $value & 0x7FFFFFFF; + + $modulo = pow(10, $this->_codeLength); + + return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT); + } + + /** + * Get QR-Code URL for image, from google charts. + * + * @param string $name + * @param string $secret + * @param string $title + * @param array $params + * + * @return string + */ + public function getQRCodeGoogleUrl($name, $secret, $title = null, $params = array()) + { + $width = !empty($params['width']) && (int) $params['width'] > 0 ? (int) $params['width'] : 200; + $height = !empty($params['height']) && (int) $params['height'] > 0 ? (int) $params['height'] : 200; + $level = !empty($params['level']) && array_search($params['level'], array('L', 'M', 'Q', 'H')) !== false ? $params['level'] : 'M'; + + $urlencoded = urlencode('otpauth://totp/'.$name.'?secret='.$secret.''); + if (isset($title)) { + $urlencoded .= urlencode('&issuer='.urlencode($title)); + } + + return "https://api.qrserver.com/v1/create-qr-code/?data=$urlencoded&size=${width}x${height}&ecc=$level"; + } + + /** + * Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now. + * + * @param string $secret + * @param string $code + * @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after) + * @param int|null $currentTimeSlice time slice if we want use other that time() + * + * @return bool + */ + public function verifyCode($secret, $code, $discrepancy = 1, $currentTimeSlice = null) + { + if ($currentTimeSlice === null) { + $currentTimeSlice = floor(time() / 30); + } + + if (strlen($code) != 6) { + return false; + } + + for ($i = -$discrepancy; $i <= $discrepancy; ++$i) { + $calculatedCode = $this->getCode($secret, $currentTimeSlice + $i); + if ($this->timingSafeEquals($calculatedCode, $code)) { + return true; + } + } + + return false; + } + + /** + * Set the code length, should be >=6. + * + * @param int $length + * + * @return PHPGangsta_GoogleAuthenticator + */ + public function setCodeLength($length) + { + $this->_codeLength = $length; + + return $this; + } + + /** + * Helper class to decode base32. + * + * @param $secret + * + * @return bool|string + */ + protected function _base32Decode($secret) + { + if (empty($secret)) { + return ''; + } + + $base32chars = $this->_getBase32LookupTable(); + $base32charsFlipped = array_flip($base32chars); + + $paddingCharCount = substr_count($secret, $base32chars[32]); + $allowedValues = array(6, 4, 3, 1, 0); + if (!in_array($paddingCharCount, $allowedValues)) { + return false; + } + for ($i = 0; $i < 4; ++$i) { + if ($paddingCharCount == $allowedValues[$i] && + substr($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i])) { + return false; + } + } + $secret = str_replace('=', '', $secret); + $secret = str_split($secret); + $binaryString = ''; + for ($i = 0; $i < count($secret); $i = $i + 8) { + $x = ''; + if (!in_array($secret[$i], $base32chars)) { + return false; + } + for ($j = 0; $j < 8; ++$j) { + $x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT); + } + $eightBits = str_split($x, 8); + for ($z = 0; $z < count($eightBits); ++$z) { + $binaryString .= (($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48) ? $y : ''; + } + } + + return $binaryString; + } + + /** + * Get array with all 32 characters for decoding from/encoding to base32. + * + * @return array + */ + protected function _getBase32LookupTable() + { + return array( + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7 + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15 + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23 + 'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31 + '=', // padding char + ); + } + + /** + * A timing safe equals comparison + * more info here: http://blog.ircmaxell.com/2014/11/its-all-about-time.html. + * + * @param string $safeString The internal (safe) value to be checked + * @param string $userString The user submitted (unsafe) value + * + * @return bool True if the two strings are identical + */ + private function timingSafeEquals($safeString, $userString) + { + if (function_exists('hash_equals')) { + return hash_equals($safeString, $userString); + } + $safeLen = strlen($safeString); + $userLen = strlen($userString); + + if ($userLen != $safeLen) { + return false; + } + + $result = 0; + + for ($i = 0; $i < $userLen; ++$i) { + $result |= (ord($safeString[$i]) ^ ord($userString[$i])); + } + + // They are only identical strings if $result is exactly 0... + return $result === 0; + } +} diff --git a/manifest.json b/manifest.json index 708743a..1e20cef 100644 --- a/manifest.json +++ b/manifest.json @@ -1,4 +1,4 @@ { - "version": "2.3.1", + "version": "2.0.1", "author": "Ivan Carlos" } diff --git a/plugin.php b/plugin.php index b376538..480fc47 100644 --- a/plugin.php +++ b/plugin.php @@ -2,8 +2,8 @@ /* Plugin Name: ICC Webmaster Settings Plugin URI: https://git.icc.gg/ivancarlos/yourlsiccwebmastersettings -Description: Customize Logo, Title, Footer, CSS & Favicons. Add reCAPTCHA v3, HTTP Redirect, 301/302 Redirects, Dash/Underscore, Force Lowercase & Remove Share features -Version: 2.3 +Description: Customize logo, title, footer, CSS, favicons, add 2FA & reCAPTCHA, HTTP, 301/302 redirects, allow dash/underscore, force lowercase, remove share buttons. +Version: 3.0 Author: Ivan Carlos Author URI: https://ivancarlos.com.br/ */ @@ -15,6 +15,9 @@ if (!defined('YOURLS_ABSPATH')) // Default redirect delay in seconds (used when option unset) define('ICC_MRDR_DEFAULT_DELAY', 1); +// Load 2FA library +require_once 'authenticator.php'; + // Register unified config page yourls_add_action('plugins_loaded', 'icc_config_add_page'); function icc_config_add_page() @@ -96,6 +99,67 @@ function icc_config_do_page() $icc_force_lowercase_enabled = yourls_get_option('icc_force_lowercase_enabled'); $force_lowercase_checked = $icc_force_lowercase_enabled ? 'checked' : ''; + // 2FA options + $icc_2fa_tokens = json_decode(yourls_get_option('icc_2fa_tokens', '{}'), true); + $user_2fa = isset($icc_2fa_tokens[YOURLS_USER]) ? $icc_2fa_tokens[YOURLS_USER] : ['active' => false, 'secret' => '', 'type' => '']; + $is_2fa_active = $user_2fa['active']; + + // Handle 2FA Actions + $twofa_message = ''; + if (isset($_POST['icc_2fa_activate'])) { + $ga = new PHPGangsta_GoogleAuthenticator(); + $secret = $ga->createSecret(); + $icc_2fa_tokens[YOURLS_USER] = [ + 'active' => false, + 'secret' => $secret, + 'type' => 'otp' + ]; + yourls_update_option('icc_2fa_tokens', json_encode($icc_2fa_tokens)); + $user_2fa = $icc_2fa_tokens[YOURLS_USER]; + } elseif (isset($_POST['icc_2fa_verify'])) { + $token = isset($_POST['icc_2fa_token']) ? trim($_POST['icc_2fa_token']) : ''; + $ga = new PHPGangsta_GoogleAuthenticator(); + if ($ga->verifyCode($user_2fa['secret'], $token, 2)) { + $icc_2fa_tokens[YOURLS_USER]['active'] = true; + yourls_update_option('icc_2fa_tokens', json_encode($icc_2fa_tokens)); + $is_2fa_active = true; + $twofa_message = '

2FA Activated successfully!

'; + } else { + $twofa_message = '

Invalid token. Please try again.

'; + } + } elseif (isset($_POST['icc_2fa_deactivate'])) { + $icc_2fa_tokens[YOURLS_USER]['active'] = false; + $icc_2fa_tokens[YOURLS_USER]['secret'] = ''; + yourls_update_option('icc_2fa_tokens', json_encode($icc_2fa_tokens)); + $is_2fa_active = false; + $twofa_message = '

2FA Deactivated.

'; + } + + $twofa_html = ''; + if ($is_2fa_active) { + $twofa_html = '

2FA is currently enabled.

+
+ +
'; + } else { + if (isset($_POST['icc_2fa_activate']) || (isset($_POST['icc_2fa_verify']) && !$is_2fa_active)) { + $ga = new PHPGangsta_GoogleAuthenticator(); + $qrCodeUrl = $ga->getQRCodeGoogleUrl('YOURLS (' . YOURLS_USER . ')', $user_2fa['secret']); + $twofa_html = '

1. Scan this QR code with your Authenticator app (Google Authenticator, Authy, etc.):

+

+

2. Enter the 6-digit code from the app to verify:

+
+ + +
'; + } else { + $twofa_html = '

2FA is currently disabled.

+
+ +
'; + } + } + echo <<Webmaster Settings
@@ -176,10 +240,14 @@ function icc_config_do_page()

+ +
+

2FA (Two-Factor Authentication)

+{$twofa_message} +{$twofa_html}

Ivan Carlos » -Buy Me a Coffee » -Patreon

+Buy Me a Coffee

HTML; } @@ -515,3 +583,52 @@ function icc_shunt_share_box($shunt) { return true; } + +// 2FA Support Logic + +// Add 2FA input to the login form +yourls_add_action('login_form_bottom', 'icc_2fa_add_input'); +function icc_2fa_add_input() +{ + echo '

+
+ +

'; +} + +// Attach 2FA validate function +yourls_add_filter('is_valid_user', 'icc_2fa_validate'); +function icc_2fa_validate($is_valid) +{ + // If user failed to properly authenticate, return + if (!$is_valid) { + return false; + } + + // If cookies are set, we are already logged in OR if this is an API request, skip 2fa + if (isset($_COOKIE[yourls_cookie_name()]) || yourls_is_API()) { + return $is_valid; + } + + $icc_2fa_tokens = json_decode(yourls_get_option('icc_2fa_tokens', '{}'), true); + + if (!isset($icc_2fa_tokens[YOURLS_USER]) || !$icc_2fa_tokens[YOURLS_USER]['active']) { + // User has not enabled 2fa + return $is_valid; + } + + // User has enabled 2FA + if ($icc_2fa_tokens[YOURLS_USER]['type'] == 'otp') { + $token = isset($_REQUEST['icc_2fa_otp']) ? trim($_REQUEST['icc_2fa_otp']) : ''; + if (empty($token)) { + return false; + } + + $ga = new PHPGangsta_GoogleAuthenticator(); + if ($ga->verifyCode($icc_2fa_tokens[YOURLS_USER]['secret'], $token, 2)) { + return true; + } + } + + return false; +}