"Fossies" - the Fresh Open Source Software Archive

Member "phpMyAdmin-5.1.0-all-languages/libraries/classes/Plugins/Auth/AuthenticationCookie.php" (24 Feb 2021, 29369 Bytes) of package /linux/www/phpMyAdmin-5.1.0-all-languages.zip:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "AuthenticationCookie.php": 5.0.4-english_vs_5.1.0-english.

    1 <?php
    2 /**
    3  * Cookie Authentication plugin for phpMyAdmin
    4  */
    5 
    6 declare(strict_types=1);
    7 
    8 namespace PhpMyAdmin\Plugins\Auth;
    9 
   10 use PhpMyAdmin\Config;
   11 use PhpMyAdmin\Core;
   12 use PhpMyAdmin\LanguageManager;
   13 use PhpMyAdmin\Message;
   14 use PhpMyAdmin\Plugins\AuthenticationPlugin;
   15 use PhpMyAdmin\Response;
   16 use PhpMyAdmin\Server\Select;
   17 use PhpMyAdmin\Session;
   18 use PhpMyAdmin\Template;
   19 use PhpMyAdmin\Url;
   20 use PhpMyAdmin\Util;
   21 use PhpMyAdmin\Utils\SessionCache;
   22 use phpseclib\Crypt;
   23 use phpseclib\Crypt\Random;
   24 use ReCaptcha;
   25 use function base64_decode;
   26 use function base64_encode;
   27 use function class_exists;
   28 use function count;
   29 use function defined;
   30 use function explode;
   31 use function function_exists;
   32 use function hash_equals;
   33 use function hash_hmac;
   34 use function in_array;
   35 use function ini_get;
   36 use function intval;
   37 use function is_array;
   38 use function is_string;
   39 use function json_decode;
   40 use function json_encode;
   41 use function openssl_cipher_iv_length;
   42 use function openssl_decrypt;
   43 use function openssl_encrypt;
   44 use function openssl_error_string;
   45 use function openssl_random_pseudo_bytes;
   46 use function preg_match;
   47 use function session_id;
   48 use function strlen;
   49 use function substr;
   50 use function time;
   51 
   52 /**
   53  * Handles the cookie authentication method
   54  */
   55 class AuthenticationCookie extends AuthenticationPlugin
   56 {
   57     /**
   58      * IV for encryption
   59      *
   60      * @var string|null
   61      */
   62     private $cookieIv = null;
   63 
   64     /**
   65      * Whether to use OpenSSL directly
   66      *
   67      * @var bool
   68      */
   69     private $useOpenSsl;
   70 
   71     public function __construct()
   72     {
   73         parent::__construct();
   74         $this->useOpenSsl = ! class_exists(Random::class);
   75     }
   76 
   77     /**
   78      * Forces (not)using of openSSL
   79      *
   80      * @param bool $use The flag
   81      *
   82      * @return void
   83      */
   84     public function setUseOpenSSL($use)
   85     {
   86         $this->useOpenSsl = $use;
   87     }
   88 
   89     /**
   90      * Displays authentication form
   91      *
   92      * this function MUST exit/quit the application
   93      *
   94      * @return bool|void
   95      *
   96      * @global string $conn_error the last connection error
   97      */
   98     public function showLoginForm()
   99     {
  100         global $conn_error, $route;
  101 
  102         $response = Response::getInstance();
  103 
  104         /**
  105          * When sending login modal after session has expired, send the
  106          * new token explicitly with the response to update the token
  107          * in all the forms having a hidden token.
  108          */
  109         $session_expired = isset($_REQUEST['check_timeout']) || isset($_REQUEST['session_timedout']);
  110         if (! $session_expired && $response->loginPage()) {
  111             if (defined('TESTSUITE')) {
  112                 return true;
  113             }
  114 
  115             exit;
  116         }
  117 
  118         /**
  119          * When sending login modal after session has expired, send the
  120          * new token explicitly with the response to update the token
  121          * in all the forms having a hidden token.
  122          */
  123         if ($session_expired) {
  124             $response->setRequestStatus(false);
  125             $response->addJSON(
  126                 'new_token',
  127                 $_SESSION[' PMA_token ']
  128             );
  129         }
  130 
  131         /**
  132          * logged_in response parameter is used to check if the login,
  133          * using the modal was successful after session expiration.
  134          */
  135         if (isset($_REQUEST['session_timedout'])) {
  136             $response->addJSON(
  137                 'logged_in',
  138                 0
  139             );
  140         }
  141 
  142         // No recall if blowfish secret is not configured as it would produce
  143         // garbage
  144         if ($GLOBALS['cfg']['LoginCookieRecall']
  145             && ! empty($GLOBALS['cfg']['blowfish_secret'])
  146         ) {
  147             $default_user   = $this->user;
  148             $default_server = $GLOBALS['pma_auth_server'];
  149             $hasAutocomplete = true;
  150         } else {
  151             $default_user   = '';
  152             $default_server = '';
  153             $hasAutocomplete = false;
  154         }
  155 
  156         // wrap the login form in a div which overlays the whole page.
  157         if ($session_expired) {
  158             $loginHeader = $this->template->render('login/header', [
  159                 'theme' => $GLOBALS['PMA_Theme'],
  160                 'add_class' => ' modal_form',
  161                 'session_expired' => 1,
  162             ]);
  163         } else {
  164             $loginHeader = $this->template->render('login/header', [
  165                 'theme' => $GLOBALS['PMA_Theme'],
  166                 'add_class' => '',
  167                 'session_expired' => 0,
  168             ]);
  169         }
  170 
  171         $errorMessages = '';
  172         // Show error message
  173         if (! empty($conn_error)) {
  174             $errorMessages = Message::rawError((string) $conn_error)->getDisplay();
  175         } elseif (isset($_GET['session_expired'])
  176             && intval($_GET['session_expired']) == 1
  177         ) {
  178             $errorMessages = Message::rawError(
  179                 __('Your session has expired. Please log in again.')
  180             )->getDisplay();
  181         }
  182 
  183         $language_manager = LanguageManager::getInstance();
  184         $languageSelector = '';
  185         $hasLanguages = empty($GLOBALS['cfg']['Lang']) && $language_manager->hasChoice();
  186         if ($hasLanguages) {
  187             $languageSelector = $language_manager->getSelectorDisplay(new Template(), true, false);
  188         }
  189 
  190         $serversOptions = '';
  191         $hasServers = count($GLOBALS['cfg']['Servers']) > 1;
  192         if ($hasServers) {
  193             $serversOptions = Select::render(false, false);
  194         }
  195 
  196         $_form_params = [];
  197         if (isset($route)) {
  198             $_form_params['route'] = $route;
  199         }
  200         if (strlen($GLOBALS['db'])) {
  201             $_form_params['db'] = $GLOBALS['db'];
  202         }
  203         if (strlen($GLOBALS['table'])) {
  204             $_form_params['table'] = $GLOBALS['table'];
  205         }
  206 
  207         $errors = '';
  208         if ($GLOBALS['error_handler']->hasDisplayErrors()) {
  209             $errors = $GLOBALS['error_handler']->getDispErrors();
  210         }
  211 
  212         // close the wrapping div tag, if the request is after session timeout
  213         if ($session_expired) {
  214             $loginFooter = $this->template->render('login/footer', ['session_expired' => 1]);
  215         } else {
  216             $loginFooter = $this->template->render('login/footer', ['session_expired' => 0]);
  217         }
  218 
  219         $configFooter = Config::renderFooter();
  220 
  221         echo $this->template->render('login/form', [
  222             'login_header' => $loginHeader,
  223             'is_demo' => $GLOBALS['cfg']['DBG']['demo'],
  224             'error_messages' => $errorMessages,
  225             'has_languages' => $hasLanguages,
  226             'language_selector' => $languageSelector,
  227             'is_session_expired' => $session_expired,
  228             'has_autocomplete' => $hasAutocomplete,
  229             'session_id' => session_id(),
  230             'is_arbitrary_server_allowed' => $GLOBALS['cfg']['AllowArbitraryServer'],
  231             'default_server' => $default_server,
  232             'default_user' => $default_user,
  233             'has_servers' => $hasServers,
  234             'server_options' => $serversOptions,
  235             'server' => $GLOBALS['server'],
  236             'lang' => $GLOBALS['lang'],
  237             'has_captcha' => ! empty($GLOBALS['cfg']['CaptchaApi'])
  238                 && ! empty($GLOBALS['cfg']['CaptchaRequestParam'])
  239                 && ! empty($GLOBALS['cfg']['CaptchaResponseParam'])
  240                 && ! empty($GLOBALS['cfg']['CaptchaLoginPrivateKey'])
  241                 && ! empty($GLOBALS['cfg']['CaptchaLoginPublicKey']),
  242             'use_captcha_checkbox' => ($GLOBALS['cfg']['CaptchaMethod'] ?? '') === 'checkbox',
  243             'captcha_api' => $GLOBALS['cfg']['CaptchaApi'],
  244             'captcha_req' => $GLOBALS['cfg']['CaptchaRequestParam'],
  245             'captcha_resp' => $GLOBALS['cfg']['CaptchaResponseParam'],
  246             'captcha_key' => $GLOBALS['cfg']['CaptchaLoginPublicKey'],
  247             'form_params' => $_form_params,
  248             'errors' => $errors,
  249             'login_footer' => $loginFooter,
  250             'config_footer' => $configFooter,
  251         ]);
  252 
  253         if (! defined('TESTSUITE')) {
  254             exit;
  255         }
  256 
  257         return true;
  258     }
  259 
  260     /**
  261      * Gets authentication credentials
  262      *
  263      * this function DOES NOT check authentication - it just checks/provides
  264      * authentication credentials required to connect to the MySQL server
  265      * usually with $dbi->connect()
  266      *
  267      * it returns false if something is missing - which usually leads to
  268      * showLoginForm() which displays login form
  269      *
  270      * it returns true if all seems ok which usually leads to auth_set_user()
  271      *
  272      * it directly switches to showFailure() if user inactivity timeout is reached
  273      *
  274      * @return bool whether we get authentication settings or not
  275      */
  276     public function readCredentials()
  277     {
  278         global $conn_error;
  279 
  280         // Initialization
  281         /**
  282          * @global $GLOBALS['pma_auth_server'] the user provided server to
  283          * connect to
  284          */
  285         $GLOBALS['pma_auth_server'] = '';
  286 
  287         $this->user = $this->password = '';
  288         $GLOBALS['from_cookie'] = false;
  289 
  290         if (isset($_POST['pma_username']) && strlen($_POST['pma_username']) > 0) {
  291             // Verify Captcha if it is required.
  292             if (! empty($GLOBALS['cfg']['CaptchaApi'])
  293                 && ! empty($GLOBALS['cfg']['CaptchaRequestParam'])
  294                 && ! empty($GLOBALS['cfg']['CaptchaResponseParam'])
  295                 && ! empty($GLOBALS['cfg']['CaptchaLoginPrivateKey'])
  296                 && ! empty($GLOBALS['cfg']['CaptchaLoginPublicKey'])
  297             ) {
  298                 if (empty($_POST[$GLOBALS['cfg']['CaptchaResponseParam']])) {
  299                     $conn_error = __('Missing reCAPTCHA verification, maybe it has been blocked by adblock?');
  300 
  301                     return false;
  302                 }
  303 
  304                 $captchaSiteVerifyURL = $GLOBALS['cfg']['CaptchaSiteVerifyURL'] ?? '';
  305                 $captchaSiteVerifyURL = empty($captchaSiteVerifyURL) ? null : $captchaSiteVerifyURL;
  306                 if (function_exists('curl_init')) {
  307                     $reCaptcha = new ReCaptcha\ReCaptcha(
  308                         $GLOBALS['cfg']['CaptchaLoginPrivateKey'],
  309                         new ReCaptcha\RequestMethod\CurlPost(null, $captchaSiteVerifyURL)
  310                     );
  311                 } elseif (ini_get('allow_url_fopen')) {
  312                     $reCaptcha = new ReCaptcha\ReCaptcha(
  313                         $GLOBALS['cfg']['CaptchaLoginPrivateKey'],
  314                         new ReCaptcha\RequestMethod\Post($captchaSiteVerifyURL)
  315                     );
  316                 } else {
  317                     $reCaptcha = new ReCaptcha\ReCaptcha(
  318                         $GLOBALS['cfg']['CaptchaLoginPrivateKey'],
  319                         new ReCaptcha\RequestMethod\SocketPost(null, $captchaSiteVerifyURL)
  320                     );
  321                 }
  322 
  323                 // verify captcha status.
  324                 $resp = $reCaptcha->verify(
  325                     $_POST[$GLOBALS['cfg']['CaptchaResponseParam']],
  326                     Core::getIp()
  327                 );
  328 
  329                 // Check if the captcha entered is valid, if not stop the login.
  330                 if ($resp == null || ! $resp->isSuccess()) {
  331                     $codes = $resp->getErrorCodes();
  332 
  333                     if (in_array('invalid-json', $codes)) {
  334                         $conn_error = __('Failed to connect to the reCAPTCHA service!');
  335                     } else {
  336                         $conn_error = __('Entered captcha is wrong, try again!');
  337                     }
  338 
  339                     return false;
  340                 }
  341             }
  342 
  343             // The user just logged in
  344             $this->user = Core::sanitizeMySQLUser($_POST['pma_username']);
  345 
  346             $password = $_POST['pma_password'] ?? '';
  347             if (strlen($password) >= 1000) {
  348                 $conn_error = __('Your password is too long. To prevent denial-of-service attacks, ' .
  349                     'phpMyAdmin restricts passwords to less than 1000 characters.');
  350 
  351                 return false;
  352             }
  353             $this->password = $password;
  354 
  355             if ($GLOBALS['cfg']['AllowArbitraryServer']
  356                 && isset($_REQUEST['pma_servername'])
  357             ) {
  358                 if ($GLOBALS['cfg']['ArbitraryServerRegexp']) {
  359                     $parts = explode(' ', $_REQUEST['pma_servername']);
  360                     if (count($parts) === 2) {
  361                         $tmp_host = $parts[0];
  362                     } else {
  363                         $tmp_host = $_REQUEST['pma_servername'];
  364                     }
  365 
  366                     $match = preg_match(
  367                         $GLOBALS['cfg']['ArbitraryServerRegexp'],
  368                         $tmp_host
  369                     );
  370                     if (! $match) {
  371                         $conn_error = __(
  372                             'You are not allowed to log in to this MySQL server!'
  373                         );
  374 
  375                         return false;
  376                     }
  377                 }
  378                 $GLOBALS['pma_auth_server'] = Core::sanitizeMySQLHost($_REQUEST['pma_servername']);
  379             }
  380             /* Secure current session on login to avoid session fixation */
  381             Session::secure();
  382 
  383             return true;
  384         }
  385 
  386         // At the end, try to set the $this->user
  387         // and $this->password variables from cookies
  388 
  389         // check cookies
  390         $serverCookie = $GLOBALS['PMA_Config']->getCookie('pmaUser-' . $GLOBALS['server']);
  391         if (empty($serverCookie)) {
  392             return false;
  393         }
  394 
  395         $value = $this->cookieDecrypt(
  396             $serverCookie,
  397             $this->getEncryptionSecret()
  398         );
  399 
  400         if ($value === false) {
  401             return false;
  402         }
  403 
  404         $this->user = $value;
  405         // user was never logged in since session start
  406         if (empty($_SESSION['browser_access_time'])) {
  407             return false;
  408         }
  409 
  410         // User inactive too long
  411         $last_access_time = time() - $GLOBALS['cfg']['LoginCookieValidity'];
  412         foreach ($_SESSION['browser_access_time'] as $key => $value) {
  413             if ($value >= $last_access_time) {
  414                 continue;
  415             }
  416 
  417             unset($_SESSION['browser_access_time'][$key]);
  418         }
  419         // All sessions expired
  420         if (empty($_SESSION['browser_access_time'])) {
  421             SessionCache::remove('is_create_db_priv');
  422             SessionCache::remove('is_reload_priv');
  423             SessionCache::remove('db_to_create');
  424             SessionCache::remove('dbs_where_create_table_allowed');
  425             SessionCache::remove('dbs_to_test');
  426             SessionCache::remove('db_priv');
  427             SessionCache::remove('col_priv');
  428             SessionCache::remove('table_priv');
  429             SessionCache::remove('proc_priv');
  430 
  431             $this->showFailure('no-activity');
  432             if (! defined('TESTSUITE')) {
  433                 exit;
  434             }
  435 
  436             return false;
  437         }
  438 
  439         // check password cookie
  440         $serverCookie = $GLOBALS['PMA_Config']->getCookie('pmaAuth-' . $GLOBALS['server']);
  441 
  442         if (empty($serverCookie)) {
  443             return false;
  444         }
  445         $value = $this->cookieDecrypt(
  446             $serverCookie,
  447             $this->getSessionEncryptionSecret()
  448         );
  449         if ($value === false) {
  450             return false;
  451         }
  452 
  453         $auth_data = json_decode($value, true);
  454 
  455         if (! is_array($auth_data) || ! isset($auth_data['password'])) {
  456             return false;
  457         }
  458         $this->password = $auth_data['password'];
  459         if ($GLOBALS['cfg']['AllowArbitraryServer'] && ! empty($auth_data['server'])) {
  460             $GLOBALS['pma_auth_server'] = $auth_data['server'];
  461         }
  462 
  463         $GLOBALS['from_cookie'] = true;
  464 
  465         return true;
  466     }
  467 
  468     /**
  469      * Set the user and password after last checkings if required
  470      *
  471      * @return bool always true
  472      */
  473     public function storeCredentials()
  474     {
  475         global $cfg;
  476 
  477         if ($GLOBALS['cfg']['AllowArbitraryServer']
  478             && ! empty($GLOBALS['pma_auth_server'])
  479         ) {
  480             /* Allow to specify 'host port' */
  481             $parts = explode(' ', $GLOBALS['pma_auth_server']);
  482             if (count($parts) === 2) {
  483                 $tmp_host = $parts[0];
  484                 $tmp_port = $parts[1];
  485             } else {
  486                 $tmp_host = $GLOBALS['pma_auth_server'];
  487                 $tmp_port = '';
  488             }
  489             if ($cfg['Server']['host'] != $GLOBALS['pma_auth_server']) {
  490                 $cfg['Server']['host'] = $tmp_host;
  491                 if (! empty($tmp_port)) {
  492                     $cfg['Server']['port'] = $tmp_port;
  493                 }
  494             }
  495             unset($tmp_host, $tmp_port, $parts);
  496         }
  497 
  498         return parent::storeCredentials();
  499     }
  500 
  501     /**
  502      * Stores user credentials after successful login.
  503      *
  504      * @return void|bool
  505      */
  506     public function rememberCredentials()
  507     {
  508         global $route;
  509 
  510         // Name and password cookies need to be refreshed each time
  511         // Duration = one month for username
  512         $this->storeUsernameCookie($this->user);
  513 
  514         // Duration = as configured
  515         // Do not store password cookie on password change as we will
  516         // set the cookie again after password has been changed
  517         if (! isset($_POST['change_pw'])) {
  518             $this->storePasswordCookie($this->password);
  519         }
  520 
  521         // any parameters to pass?
  522         $url_params = [];
  523         if (isset($route)) {
  524             $url_params['route'] = $route;
  525         }
  526         if (strlen($GLOBALS['db']) > 0) {
  527             $url_params['db'] = $GLOBALS['db'];
  528         }
  529         if (strlen($GLOBALS['table']) > 0) {
  530             $url_params['table'] = $GLOBALS['table'];
  531         }
  532 
  533         // user logged in successfully after session expiration
  534         if (isset($_REQUEST['session_timedout'])) {
  535             $response = Response::getInstance();
  536             $response->addJSON(
  537                 'logged_in',
  538                 1
  539             );
  540             $response->addJSON(
  541                 'success',
  542                 1
  543             );
  544             $response->addJSON(
  545                 'new_token',
  546                 $_SESSION[' PMA_token ']
  547             );
  548 
  549             if (! defined('TESTSUITE')) {
  550                 exit;
  551             }
  552 
  553             return false;
  554         }
  555         // Set server cookies if required (once per session) and, in this case,
  556         // force reload to ensure the client accepts cookies
  557         if (! $GLOBALS['from_cookie']) {
  558 
  559             /**
  560              * Clear user cache.
  561              */
  562             Util::clearUserCache();
  563 
  564             Response::getInstance()
  565                 ->disable();
  566 
  567             Core::sendHeaderLocation(
  568                 './index.php?route=/' . Url::getCommonRaw($url_params, '&'),
  569                 true
  570             );
  571             if (! defined('TESTSUITE')) {
  572                 exit;
  573             }
  574 
  575             return false;
  576         }
  577 
  578         return true;
  579     }
  580 
  581     /**
  582      * Stores username in a cookie.
  583      *
  584      * @param string $username User name
  585      *
  586      * @return void
  587      */
  588     public function storeUsernameCookie($username)
  589     {
  590         // Name and password cookies need to be refreshed each time
  591         // Duration = one month for username
  592         $GLOBALS['PMA_Config']->setCookie(
  593             'pmaUser-' . $GLOBALS['server'],
  594             $this->cookieEncrypt(
  595                 $username,
  596                 $this->getEncryptionSecret()
  597             )
  598         );
  599     }
  600 
  601     /**
  602      * Stores password in a cookie.
  603      *
  604      * @param string $password Password
  605      *
  606      * @return void
  607      */
  608     public function storePasswordCookie($password)
  609     {
  610         $payload = ['password' => $password];
  611         if ($GLOBALS['cfg']['AllowArbitraryServer'] && ! empty($GLOBALS['pma_auth_server'])) {
  612             $payload['server'] = $GLOBALS['pma_auth_server'];
  613         }
  614         // Duration = as configured
  615         $GLOBALS['PMA_Config']->setCookie(
  616             'pmaAuth-' . $GLOBALS['server'],
  617             $this->cookieEncrypt(
  618                 json_encode($payload),
  619                 $this->getSessionEncryptionSecret()
  620             ),
  621             null,
  622             (int) $GLOBALS['cfg']['LoginCookieStore']
  623         );
  624     }
  625 
  626     /**
  627      * User is not allowed to login to MySQL -> authentication failed
  628      *
  629      * prepares error message and switches to showLoginForm() which display the error
  630      * and the login form
  631      *
  632      * this function MUST exit/quit the application,
  633      * currently done by call to showLoginForm()
  634      *
  635      * @param string $failure String describing why authentication has failed
  636      *
  637      * @return void
  638      */
  639     public function showFailure($failure)
  640     {
  641         global $conn_error;
  642 
  643         parent::showFailure($failure);
  644 
  645         // Deletes password cookie and displays the login form
  646         $GLOBALS['PMA_Config']->removeCookie('pmaAuth-' . $GLOBALS['server']);
  647 
  648         $conn_error = $this->getErrorMessage($failure);
  649 
  650         $response = Response::getInstance();
  651 
  652         // needed for PHP-CGI (not need for FastCGI or mod-php)
  653         $response->header('Cache-Control: no-store, no-cache, must-revalidate');
  654         $response->header('Pragma: no-cache');
  655 
  656         $this->showLoginForm();
  657     }
  658 
  659     /**
  660      * Returns blowfish secret or generates one if needed.
  661      *
  662      * @return string
  663      */
  664     private function getEncryptionSecret()
  665     {
  666         if (empty($GLOBALS['cfg']['blowfish_secret'])) {
  667             return $this->getSessionEncryptionSecret();
  668         }
  669 
  670         return $GLOBALS['cfg']['blowfish_secret'];
  671     }
  672 
  673     /**
  674      * Returns blowfish secret or generates one if needed.
  675      *
  676      * @return string
  677      */
  678     private function getSessionEncryptionSecret()
  679     {
  680         if (empty($_SESSION['encryption_key'])) {
  681             if ($this->useOpenSsl) {
  682                 $_SESSION['encryption_key'] = openssl_random_pseudo_bytes(32);
  683             } else {
  684                 $_SESSION['encryption_key'] = Crypt\Random::string(32);
  685             }
  686         }
  687 
  688         return $_SESSION['encryption_key'];
  689     }
  690 
  691     /**
  692      * Concatenates secret in order to make it 16 bytes log
  693      *
  694      * This doesn't add any security, just ensures the secret
  695      * is long enough by copying it.
  696      *
  697      * @param string $secret Original secret
  698      *
  699      * @return string
  700      */
  701     public function enlargeSecret($secret)
  702     {
  703         while (strlen($secret) < 16) {
  704             $secret .= $secret;
  705         }
  706 
  707         return substr($secret, 0, 16);
  708     }
  709 
  710     /**
  711      * Derives MAC secret from encryption secret.
  712      *
  713      * @param string $secret the secret
  714      *
  715      * @return string the MAC secret
  716      */
  717     public function getMACSecret($secret)
  718     {
  719         // Grab first part, up to 16 chars
  720         // The MAC and AES secrets can overlap if original secret is short
  721         $length = strlen($secret);
  722         if ($length > 16) {
  723             return substr($secret, 0, 16);
  724         }
  725 
  726         return $this->enlargeSecret(
  727             $length == 1 ? $secret : substr($secret, 0, -1)
  728         );
  729     }
  730 
  731     /**
  732      * Derives AES secret from encryption secret.
  733      *
  734      * @param string $secret the secret
  735      *
  736      * @return string the AES secret
  737      */
  738     public function getAESSecret($secret)
  739     {
  740         // Grab second part, up to 16 chars
  741         // The MAC and AES secrets can overlap if original secret is short
  742         $length = strlen($secret);
  743         if ($length > 16) {
  744             return substr($secret, -16);
  745         }
  746 
  747         return $this->enlargeSecret(
  748             $length == 1 ? $secret : substr($secret, 1)
  749         );
  750     }
  751 
  752     /**
  753      * Cleans any SSL errors
  754      *
  755      * This can happen from corrupted cookies, by invalid encryption
  756      * parameters used in older phpMyAdmin versions or by wrong openSSL
  757      * configuration.
  758      *
  759      * In neither case the error is useful to user, but we need to clear
  760      * the error buffer as otherwise the errors would pop up later, for
  761      * example during MySQL SSL setup.
  762      *
  763      * @return void
  764      */
  765     public function cleanSSLErrors()
  766     {
  767         if (! function_exists('openssl_error_string')) {
  768             return;
  769         }
  770 
  771         do {
  772             $hasSslErrors = openssl_error_string();
  773         } while ($hasSslErrors !== false);
  774     }
  775 
  776     /**
  777      * Encryption using openssl's AES or phpseclib's AES
  778      * (phpseclib uses another extension when it is available)
  779      *
  780      * @param string $data   original data
  781      * @param string $secret the secret
  782      *
  783      * @return string the encrypted result
  784      */
  785     public function cookieEncrypt($data, $secret)
  786     {
  787         $mac_secret = $this->getMACSecret($secret);
  788         $aes_secret = $this->getAESSecret($secret);
  789         $iv = $this->createIV();
  790         if ($this->useOpenSsl) {
  791             $result = openssl_encrypt(
  792                 $data,
  793                 'AES-128-CBC',
  794                 $aes_secret,
  795                 0,
  796                 $iv
  797             );
  798         } else {
  799             $cipher = new Crypt\AES(Crypt\Base::MODE_CBC);
  800             $cipher->setIV($iv);
  801             $cipher->setKey($aes_secret);
  802             $result = base64_encode($cipher->encrypt($data));
  803         }
  804         $this->cleanSSLErrors();
  805         $iv = base64_encode($iv);
  806 
  807         return json_encode(
  808             [
  809                 'iv' => $iv,
  810                 'mac' => hash_hmac('sha1', $iv . $result, $mac_secret),
  811                 'payload' => $result,
  812             ]
  813         );
  814     }
  815 
  816     /**
  817      * Decryption using openssl's AES or phpseclib's AES
  818      * (phpseclib uses another extension when it is available)
  819      *
  820      * @param string $encdata encrypted data
  821      * @param string $secret  the secret
  822      *
  823      * @return string|false original data, false on error
  824      */
  825     public function cookieDecrypt($encdata, $secret)
  826     {
  827         $data = json_decode($encdata, true);
  828 
  829         if (! isset($data['mac'], $data['iv'], $data['payload'])
  830             || ! is_array($data)
  831             || ! is_string($data['mac'])
  832             || ! is_string($data['iv'])
  833             || ! is_string($data['payload'])
  834         ) {
  835             return false;
  836         }
  837 
  838         $mac_secret = $this->getMACSecret($secret);
  839         $aes_secret = $this->getAESSecret($secret);
  840         $newmac = hash_hmac('sha1', $data['iv'] . $data['payload'], $mac_secret);
  841 
  842         if (! hash_equals($data['mac'], $newmac)) {
  843             return false;
  844         }
  845 
  846         if ($this->useOpenSsl) {
  847             $result = openssl_decrypt(
  848                 $data['payload'],
  849                 'AES-128-CBC',
  850                 $aes_secret,
  851                 0,
  852                 base64_decode($data['iv'])
  853             );
  854         } else {
  855             $cipher = new Crypt\AES(Crypt\Base::MODE_CBC);
  856             $cipher->setIV(base64_decode($data['iv']));
  857             $cipher->setKey($aes_secret);
  858             $result = $cipher->decrypt(base64_decode($data['payload']));
  859         }
  860         $this->cleanSSLErrors();
  861 
  862         return $result;
  863     }
  864 
  865     /**
  866      * Returns size of IV for encryption.
  867      *
  868      * @return int
  869      */
  870     public function getIVSize()
  871     {
  872         if ($this->useOpenSsl) {
  873             return openssl_cipher_iv_length('AES-128-CBC');
  874         }
  875 
  876         return (new Crypt\AES(Crypt\Base::MODE_CBC))->block_size;
  877     }
  878 
  879     /**
  880      * Initialization
  881      * Store the initialization vector because it will be needed for
  882      * further decryption. I don't think necessary to have one iv
  883      * per server so I don't put the server number in the cookie name.
  884      *
  885      * @return string
  886      */
  887     public function createIV()
  888     {
  889         /* Testsuite shortcut only to allow predictable IV */
  890         if ($this->cookieIv !== null) {
  891             return $this->cookieIv;
  892         }
  893         if ($this->useOpenSsl) {
  894             return openssl_random_pseudo_bytes(
  895                 $this->getIVSize()
  896             );
  897         }
  898 
  899         return Crypt\Random::string(
  900             $this->getIVSize()
  901         );
  902     }
  903 
  904     /**
  905      * Sets encryption IV to use
  906      *
  907      * This is for testing only!
  908      *
  909      * @param string $vector The IV
  910      *
  911      * @return void
  912      */
  913     public function setIV($vector)
  914     {
  915         $this->cookieIv = $vector;
  916     }
  917 
  918     /**
  919      * Callback when user changes password.
  920      *
  921      * @param string $password New password to set
  922      *
  923      * @return void
  924      */
  925     public function handlePasswordChange($password)
  926     {
  927         $this->storePasswordCookie($password);
  928     }
  929 
  930     /**
  931      * Perform logout
  932      *
  933      * @return void
  934      */
  935     public function logOut()
  936     {
  937         global $PMA_Config;
  938 
  939         // -> delete password cookie(s)
  940         if ($GLOBALS['cfg']['LoginCookieDeleteAll']) {
  941             foreach ($GLOBALS['cfg']['Servers'] as $key => $val) {
  942                 $PMA_Config->removeCookie('pmaAuth-' . $key);
  943                 if (! $PMA_Config->issetCookie('pmaAuth-' . $key)) {
  944                     continue;
  945                 }
  946 
  947                 $PMA_Config->removeCookie('pmaAuth-' . $key);
  948             }
  949         } else {
  950             $cookieName = 'pmaAuth-' . $GLOBALS['server'];
  951             $PMA_Config->removeCookie($cookieName);
  952             if ($PMA_Config->issetCookie($cookieName)) {
  953                 $PMA_Config->removeCookie($cookieName);
  954             }
  955         }
  956         parent::logOut();
  957     }
  958 }