AuthenticationCookie.php 29 KB

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