From 04d6d5ca99ebfd1cebb8ce06618fb3811fc1a8aa Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 9 Jan 2020 10:55:03 +0100 Subject: phpmyadmin working --- .../classes/Plugins/Auth/AuthenticationCookie.php | 964 +++++++++++++++++++++ 1 file changed, 964 insertions(+) create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationCookie.php (limited to 'srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationCookie.php') diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationCookie.php b/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationCookie.php new file mode 100644 index 0000000..7a794d0 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationCookie.php @@ -0,0 +1,964 @@ +_use_openssl = ! class_exists(Random::class); + } + + /** + * Forces (not)using of openSSL + * + * @param boolean $use The flag + * + * @return void + */ + public function setUseOpenSSL($use) + { + $this->_use_openssl = $use; + } + + /** + * Displays authentication form + * + * this function MUST exit/quit the application + * + * @global string $conn_error the last connection error + * + * @return boolean|void + */ + public function showLoginForm() + { + global $conn_error; + + $response = Response::getInstance(); + + // When sending login modal after session has expired, send the new token explicitly with the response to update the token in all the forms having a hidden token. + $session_expired = isset($_REQUEST['check_timeout']) || isset($_REQUEST['session_timedout']); + if (! $session_expired && $response->loginPage()) { + if (defined('TESTSUITE')) { + return true; + } else { + exit; + } + } + + // When sending login modal after session has expired, send the new token explicitly with the response to update the token in all the forms having a hidden token. + if ($session_expired) { + $response->setRequestStatus(false); + $response->addJSON( + 'new_token', + $_SESSION[' PMA_token '] + ); + } + + // logged_in response parameter is used to check if the login, using the modal was successful after session expiration + if (isset($_REQUEST['session_timedout'])) { + $response->addJSON( + 'logged_in', + 0 + ); + } + + // No recall if blowfish secret is not configured as it would produce + // garbage + if ($GLOBALS['cfg']['LoginCookieRecall'] + && ! empty($GLOBALS['cfg']['blowfish_secret']) + ) { + $default_user = $this->user; + $default_server = $GLOBALS['pma_auth_server']; + $autocomplete = ''; + } else { + $default_user = ''; + $default_server = ''; + // skip the IE autocomplete feature. + $autocomplete = ' autocomplete="off"'; + } + + // wrap the login form in a div which overlays the whole page. + if ($session_expired) { + echo $this->template->render('login/header', [ + 'theme' => $GLOBALS['PMA_Theme'], + 'add_class' => ' modal_form', + 'session_expired' => 1, + ]); + } else { + echo $this->template->render('login/header', [ + 'theme' => $GLOBALS['PMA_Theme'], + 'add_class' => '', + 'session_expired' => 0, + ]); + } + + if ($GLOBALS['cfg']['DBG']['demo']) { + echo '
'; + echo '' , __('phpMyAdmin Demo Server') , ''; + printf( + __( + 'You are using the demo server. You can do anything here, but ' + . 'please do not change root, debian-sys-maint and pma users. ' + . 'More information is available at %s.' + ), + 'demo.phpmyadmin.net' + ); + echo '
'; + } + + // Show error message + if (! empty($conn_error)) { + Message::rawError((string) $conn_error)->display(); + } elseif (isset($_GET['session_expired']) + && intval($_GET['session_expired']) == 1 + ) { + Message::rawError( + __('Your session has expired. Please log in again.') + )->display(); + } + + // Displays the languages form + $language_manager = LanguageManager::getInstance(); + if (empty($GLOBALS['cfg']['Lang']) && $language_manager->hasChoice()) { + echo "
"; + // use fieldset, don't show doc link + echo $language_manager->getSelectorDisplay(new Template(), true, false); + echo '
'; + } + echo ' +
+ +
+
+ '; + echo ''; + + // Add a hidden element session_timedout which is used to check if the user requested login after session expiration + if ($session_expired) { + echo ''; + } + echo __('Log in'); + echo Util::showDocu('index'); + echo ''; + if ($GLOBALS['cfg']['AllowArbitraryServer']) { + echo ' +
+ + +
'; + } + echo '
+ + +
+
+ + +
'; + if (count($GLOBALS['cfg']['Servers']) > 1) { + echo '
+ +
'; + } else { + echo ' '; + } // end if (server choice) + + echo '
'; + + // binds input field with invisible reCaptcha if enabled + if (empty($GLOBALS['cfg']['CaptchaLoginPrivateKey']) + && empty($GLOBALS['cfg']['CaptchaLoginPublicKey']) + ) { + echo ''; + } else { + echo ''; + echo ''; + } + $_form_params = []; + if (! empty($GLOBALS['target'])) { + $_form_params['target'] = $GLOBALS['target']; + } + if (strlen($GLOBALS['db'])) { + $_form_params['db'] = $GLOBALS['db']; + } + if (strlen($GLOBALS['table'])) { + $_form_params['table'] = $GLOBALS['table']; + } + // do not generate a "server" hidden field as we want the "server" + // drop-down to have priority + echo Url::getHiddenInputs($_form_params, '', 0, 'server'); + echo '
+
'; + + if ($GLOBALS['error_handler']->hasDisplayErrors()) { + echo '
'; + $GLOBALS['error_handler']->dispErrors(); + echo '
'; + } + + // close the wrapping div tag, if the request is after session timeout + if ($session_expired) { + echo $this->template->render('login/footer', ['session_expired' => 1]); + } else { + echo $this->template->render('login/footer', ['session_expired' => 0]); + } + + echo Config::renderFooter(); + + if (! defined('TESTSUITE')) { + exit; + } else { + return true; + } + } + + /** + * Gets authentication credentials + * + * this function DOES NOT check authentication - it just checks/provides + * authentication credentials required to connect to the MySQL server + * usually with $GLOBALS['dbi']->connect() + * + * it returns false if something is missing - which usually leads to + * showLoginForm() which displays login form + * + * it returns true if all seems ok which usually leads to auth_set_user() + * + * it directly switches to showFailure() if user inactivity timeout is reached + * + * @return boolean whether we get authentication settings or not + */ + public function readCredentials() + { + global $conn_error; + + // Initialization + /** + * @global $GLOBALS['pma_auth_server'] the user provided server to + * connect to + */ + $GLOBALS['pma_auth_server'] = ''; + + $this->user = $this->password = ''; + $GLOBALS['from_cookie'] = false; + + if (isset($_POST['pma_username']) && strlen($_POST['pma_username']) > 0) { + // Verify Captcha if it is required. + if (! empty($GLOBALS['cfg']['CaptchaLoginPrivateKey']) + && ! empty($GLOBALS['cfg']['CaptchaLoginPublicKey']) + ) { + if (! empty($_POST["g-recaptcha-response"])) { + if (function_exists('curl_init')) { + $reCaptcha = new ReCaptcha\ReCaptcha( + $GLOBALS['cfg']['CaptchaLoginPrivateKey'], + new ReCaptcha\RequestMethod\CurlPost() + ); + } elseif (ini_get('allow_url_fopen')) { + $reCaptcha = new ReCaptcha\ReCaptcha( + $GLOBALS['cfg']['CaptchaLoginPrivateKey'], + new ReCaptcha\RequestMethod\Post() + ); + } else { + $reCaptcha = new ReCaptcha\ReCaptcha( + $GLOBALS['cfg']['CaptchaLoginPrivateKey'], + new ReCaptcha\RequestMethod\SocketPost() + ); + } + + // verify captcha status. + $resp = $reCaptcha->verify( + $_POST["g-recaptcha-response"], + Core::getIp() + ); + + // Check if the captcha entered is valid, if not stop the login. + if ($resp == null || ! $resp->isSuccess()) { + $codes = $resp->getErrorCodes(); + + if (in_array('invalid-json', $codes)) { + $conn_error = __('Failed to connect to the reCAPTCHA service!'); + } else { + $conn_error = __('Entered captcha is wrong, try again!'); + } + return false; + } + } else { + $conn_error = __('Missing reCAPTCHA verification, maybe it has been blocked by adblock?'); + return false; + } + } + + // The user just logged in + $this->user = Core::sanitizeMySQLUser($_POST['pma_username']); + $this->password = isset($_POST['pma_password']) ? $_POST['pma_password'] : ''; + if ($GLOBALS['cfg']['AllowArbitraryServer'] + && isset($_REQUEST['pma_servername']) + ) { + if ($GLOBALS['cfg']['ArbitraryServerRegexp']) { + $parts = explode(' ', $_REQUEST['pma_servername']); + if (count($parts) === 2) { + $tmp_host = $parts[0]; + } else { + $tmp_host = $_REQUEST['pma_servername']; + } + + $match = preg_match( + $GLOBALS['cfg']['ArbitraryServerRegexp'], + $tmp_host + ); + if (! $match) { + $conn_error = __( + 'You are not allowed to log in to this MySQL server!' + ); + return false; + } + } + $GLOBALS['pma_auth_server'] = Core::sanitizeMySQLHost($_REQUEST['pma_servername']); + } + /* Secure current session on login to avoid session fixation */ + Session::secure(); + return true; + } + + // At the end, try to set the $this->user + // and $this->password variables from cookies + + // check cookies + $serverCookie = $GLOBALS['PMA_Config']->getCookie('pmaUser-' . $GLOBALS['server']); + if (empty($serverCookie)) { + return false; + } + + $value = $this->cookieDecrypt( + $serverCookie, + $this->_getEncryptionSecret() + ); + + if ($value === false) { + return false; + } + + $this->user = $value; + // user was never logged in since session start + if (empty($_SESSION['browser_access_time'])) { + return false; + } + + // User inactive too long + $last_access_time = time() - $GLOBALS['cfg']['LoginCookieValidity']; + foreach ($_SESSION['browser_access_time'] as $key => $value) { + if ($value < $last_access_time) { + unset($_SESSION['browser_access_time'][$key]); + } + } + // All sessions expired + if (empty($_SESSION['browser_access_time'])) { + Util::cacheUnset('is_create_db_priv'); + Util::cacheUnset('is_reload_priv'); + Util::cacheUnset('db_to_create'); + Util::cacheUnset('dbs_where_create_table_allowed'); + Util::cacheUnset('dbs_to_test'); + Util::cacheUnset('db_priv'); + Util::cacheUnset('col_priv'); + Util::cacheUnset('table_priv'); + Util::cacheUnset('proc_priv'); + + $this->showFailure('no-activity'); + if (! defined('TESTSUITE')) { + exit; + } else { + return false; + } + } + + // check password cookie + $serverCookie = $GLOBALS['PMA_Config']->getCookie('pmaAuth-' . $GLOBALS['server']); + + if (empty($serverCookie)) { + return false; + } + $value = $this->cookieDecrypt( + $serverCookie, + $this->_getSessionEncryptionSecret() + ); + if ($value === false) { + return false; + } + + $auth_data = json_decode($value, true); + + if (! is_array($auth_data) || ! isset($auth_data['password'])) { + return false; + } + $this->password = $auth_data['password']; + if ($GLOBALS['cfg']['AllowArbitraryServer'] && ! empty($auth_data['server'])) { + $GLOBALS['pma_auth_server'] = $auth_data['server']; + } + + $GLOBALS['from_cookie'] = true; + + return true; + } + + /** + * Set the user and password after last checkings if required + * + * @return boolean always true + */ + public function storeCredentials() + { + global $cfg; + + if ($GLOBALS['cfg']['AllowArbitraryServer'] + && ! empty($GLOBALS['pma_auth_server']) + ) { + /* Allow to specify 'host port' */ + $parts = explode(' ', $GLOBALS['pma_auth_server']); + if (count($parts) === 2) { + $tmp_host = $parts[0]; + $tmp_port = $parts[1]; + } else { + $tmp_host = $GLOBALS['pma_auth_server']; + $tmp_port = ''; + } + if ($cfg['Server']['host'] != $GLOBALS['pma_auth_server']) { + $cfg['Server']['host'] = $tmp_host; + if (! empty($tmp_port)) { + $cfg['Server']['port'] = $tmp_port; + } + } + unset($tmp_host, $tmp_port, $parts); + } + + return parent::storeCredentials(); + } + + /** + * Stores user credentials after successful login. + * + * @return void|bool + */ + public function rememberCredentials() + { + // Name and password cookies need to be refreshed each time + // Duration = one month for username + + $this->storeUsernameCookie($this->user); + + // Duration = as configured + // Do not store password cookie on password change as we will + // set the cookie again after password has been changed + if (! isset($_POST['change_pw'])) { + $this->storePasswordCookie($this->password); + } + // URL where to go: + $redirect_url = './index.php'; + + // any parameters to pass? + $url_params = []; + if (strlen($GLOBALS['db']) > 0) { + $url_params['db'] = $GLOBALS['db']; + } + if (strlen($GLOBALS['table']) > 0) { + $url_params['table'] = $GLOBALS['table']; + } + // any target to pass? + if (! empty($GLOBALS['target']) + && $GLOBALS['target'] != 'index.php' + ) { + $url_params['target'] = $GLOBALS['target']; + } + + // user logged in successfully after session expiration + if (isset($_REQUEST['session_timedout'])) { + $response = Response::getInstance(); + $response->addJSON( + 'logged_in', + 1 + ); + $response->addJSON( + 'success', + 1 + ); + $response->addJSON( + 'new_token', + $_SESSION[' PMA_token '] + ); + + if (! defined('TESTSUITE')) { + exit; + } else { + return false; + } + } + // Set server cookies if required (once per session) and, in this case, + // force reload to ensure the client accepts cookies + if (! $GLOBALS['from_cookie']) { + + /** + * Clear user cache. + */ + Util::clearUserCache(); + + Response::getInstance() + ->disable(); + + Core::sendHeaderLocation( + $redirect_url . Url::getCommonRaw($url_params), + true + ); + if (! defined('TESTSUITE')) { + exit; + } else { + return false; + } + } // end if + + return true; + } + + /** + * Stores username in a cookie. + * + * @param string $username User name + * + * @return void + */ + public function storeUsernameCookie($username) + { + // Name and password cookies need to be refreshed each time + // Duration = one month for username + $GLOBALS['PMA_Config']->setCookie( + 'pmaUser-' . $GLOBALS['server'], + $this->cookieEncrypt( + $username, + $this->_getEncryptionSecret() + ) + ); + } + + /** + * Stores password in a cookie. + * + * @param string $password Password + * + * @return void + */ + public function storePasswordCookie($password) + { + $payload = ['password' => $password]; + if ($GLOBALS['cfg']['AllowArbitraryServer'] && ! empty($GLOBALS['pma_auth_server'])) { + $payload['server'] = $GLOBALS['pma_auth_server']; + } + // Duration = as configured + $GLOBALS['PMA_Config']->setCookie( + 'pmaAuth-' . $GLOBALS['server'], + $this->cookieEncrypt( + json_encode($payload), + $this->_getSessionEncryptionSecret() + ), + null, + (int) $GLOBALS['cfg']['LoginCookieStore'] + ); + } + + /** + * User is not allowed to login to MySQL -> authentication failed + * + * prepares error message and switches to showLoginForm() which display the error + * and the login form + * + * this function MUST exit/quit the application, + * currently done by call to showLoginForm() + * + * @param string $failure String describing why authentication has failed + * + * @return void + */ + public function showFailure($failure) + { + global $conn_error; + + parent::showFailure($failure); + + // Deletes password cookie and displays the login form + $GLOBALS['PMA_Config']->removeCookie('pmaAuth-' . $GLOBALS['server']); + + $conn_error = $this->getErrorMessage($failure); + + $response = Response::getInstance(); + + // needed for PHP-CGI (not need for FastCGI or mod-php) + $response->header('Cache-Control: no-store, no-cache, must-revalidate'); + $response->header('Pragma: no-cache'); + + $this->showLoginForm(); + } + + /** + * Returns blowfish secret or generates one if needed. + * + * @return string + */ + private function _getEncryptionSecret() + { + if (empty($GLOBALS['cfg']['blowfish_secret'])) { + return $this->_getSessionEncryptionSecret(); + } + + return $GLOBALS['cfg']['blowfish_secret']; + } + + /** + * Returns blowfish secret or generates one if needed. + * + * @return string + */ + private function _getSessionEncryptionSecret() + { + if (empty($_SESSION['encryption_key'])) { + if ($this->_use_openssl) { + $_SESSION['encryption_key'] = openssl_random_pseudo_bytes(32); + } else { + $_SESSION['encryption_key'] = Crypt\Random::string(32); + } + } + return $_SESSION['encryption_key']; + } + + /** + * Concatenates secret in order to make it 16 bytes log + * + * This doesn't add any security, just ensures the secret + * is long enough by copying it. + * + * @param string $secret Original secret + * + * @return string + */ + public function enlargeSecret($secret) + { + while (strlen($secret) < 16) { + $secret .= $secret; + } + return substr($secret, 0, 16); + } + + /** + * Derives MAC secret from encryption secret. + * + * @param string $secret the secret + * + * @return string the MAC secret + */ + public function getMACSecret($secret) + { + // Grab first part, up to 16 chars + // The MAC and AES secrets can overlap if original secret is short + $length = strlen($secret); + if ($length > 16) { + return substr($secret, 0, 16); + } + return $this->enlargeSecret( + $length == 1 ? $secret : substr($secret, 0, -1) + ); + } + + /** + * Derives AES secret from encryption secret. + * + * @param string $secret the secret + * + * @return string the AES secret + */ + public function getAESSecret($secret) + { + // Grab second part, up to 16 chars + // The MAC and AES secrets can overlap if original secret is short + $length = strlen($secret); + if ($length > 16) { + return substr($secret, -16); + } + return $this->enlargeSecret( + $length == 1 ? $secret : substr($secret, 1) + ); + } + + /** + * Cleans any SSL errors + * + * This can happen from corrupted cookies, by invalid encryption + * parameters used in older phpMyAdmin versions or by wrong openSSL + * configuration. + * + * In neither case the error is useful to user, but we need to clear + * the error buffer as otherwise the errors would pop up later, for + * example during MySQL SSL setup. + * + * @return void + */ + public function cleanSSLErrors() + { + if (function_exists('openssl_error_string')) { + do { + $hasSslErrors = openssl_error_string(); + } while ($hasSslErrors !== false); + } + } + + /** + * Encryption using openssl's AES or phpseclib's AES + * (phpseclib uses mcrypt when it is available) + * + * @param string $data original data + * @param string $secret the secret + * + * @return string the encrypted result + */ + public function cookieEncrypt($data, $secret) + { + $mac_secret = $this->getMACSecret($secret); + $aes_secret = $this->getAESSecret($secret); + $iv = $this->createIV(); + if ($this->_use_openssl) { + $result = openssl_encrypt( + $data, + 'AES-128-CBC', + $aes_secret, + 0, + $iv + ); + } else { + $cipher = new Crypt\AES(Crypt\Base::MODE_CBC); + $cipher->setIV($iv); + $cipher->setKey($aes_secret); + $result = base64_encode($cipher->encrypt($data)); + } + $this->cleanSSLErrors(); + $iv = base64_encode($iv); + return json_encode( + [ + 'iv' => $iv, + 'mac' => hash_hmac('sha1', $iv . $result, $mac_secret), + 'payload' => $result, + ] + ); + } + + /** + * Decryption using openssl's AES or phpseclib's AES + * (phpseclib uses mcrypt when it is available) + * + * @param string $encdata encrypted data + * @param string $secret the secret + * + * @return string|false original data, false on error + */ + public function cookieDecrypt($encdata, $secret) + { + $data = json_decode($encdata, true); + + if (! is_array($data) || ! isset($data['mac']) || ! isset($data['iv']) || ! isset($data['payload']) + || ! is_string($data['mac']) || ! is_string($data['iv']) || ! is_string($data['payload']) + ) { + return false; + } + + $mac_secret = $this->getMACSecret($secret); + $aes_secret = $this->getAESSecret($secret); + $newmac = hash_hmac('sha1', $data['iv'] . $data['payload'], $mac_secret); + + if (! hash_equals($data['mac'], $newmac)) { + return false; + } + + if ($this->_use_openssl) { + $result = openssl_decrypt( + $data['payload'], + 'AES-128-CBC', + $aes_secret, + 0, + base64_decode($data['iv']) + ); + } else { + $cipher = new Crypt\AES(Crypt\Base::MODE_CBC); + $cipher->setIV(base64_decode($data['iv'])); + $cipher->setKey($aes_secret); + $result = $cipher->decrypt(base64_decode($data['payload'])); + } + $this->cleanSSLErrors(); + return $result; + } + + /** + * Returns size of IV for encryption. + * + * @return int + */ + public function getIVSize() + { + if ($this->_use_openssl) { + return openssl_cipher_iv_length('AES-128-CBC'); + } + return (new Crypt\AES(Crypt\Base::MODE_CBC))->block_size; + } + + /** + * Initialization + * Store the initialization vector because it will be needed for + * further decryption. I don't think necessary to have one iv + * per server so I don't put the server number in the cookie name. + * + * @return string + */ + public function createIV() + { + /* Testsuite shortcut only to allow predictable IV */ + if ($this->_cookie_iv !== null) { + return $this->_cookie_iv; + } + if ($this->_use_openssl) { + return openssl_random_pseudo_bytes( + $this->getIVSize() + ); + } + + return Crypt\Random::string( + $this->getIVSize() + ); + } + + /** + * Sets encryption IV to use + * + * This is for testing only! + * + * @param string $vector The IV + * + * @return void + */ + public function setIV($vector) + { + $this->_cookie_iv = $vector; + } + + /** + * Callback when user changes password. + * + * @param string $password New password to set + * + * @return void + */ + public function handlePasswordChange($password) + { + $this->storePasswordCookie($password); + } + + /** + * Perform logout + * + * @return void + */ + public function logOut() + { + /** @var Config $PMA_Config */ + global $PMA_Config; + + // -> delete password cookie(s) + if ($GLOBALS['cfg']['LoginCookieDeleteAll']) { + foreach ($GLOBALS['cfg']['Servers'] as $key => $val) { + $PMA_Config->removeCookie('pmaAuth-' . $key); + if ($PMA_Config->issetCookie('pmaAuth-' . $key)) { + $PMA_Config->removeCookie('pmaAuth-' . $key); + } + } + } else { + $cookieName = 'pmaAuth-' . $GLOBALS['server']; + $PMA_Config->removeCookie($cookieName); + if ($PMA_Config->issetCookie($cookieName)) { + $PMA_Config->removeCookie($cookieName); + } + } + parent::logOut(); + } +} -- cgit