Passed
Push — master ( 6af66c...465005 )
by Isaac
08:40 queued 10s
created

classes/Plugins/Auth/AuthenticationCookie.php (1 issue)

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