AuthenticationCookie.php 30 KB

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