1<?php
2
3/**
4 * Password Plugin for Roundcube
5 *
6 * @author Aleksander Machniak <alec@alec.pl>
7 *
8 * Copyright (C) The Roundcube Dev Team
9 *
10 * This program is free software: you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation, either version 3 of the License, or
13 * (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program. If not, see http://www.gnu.org/licenses/.
22 */
23
24define('PASSWORD_CRYPT_ERROR', 1);
25define('PASSWORD_ERROR', 2);
26define('PASSWORD_CONNECT_ERROR', 3);
27define('PASSWORD_IN_HISTORY', 4);
28define('PASSWORD_CONSTRAINT_VIOLATION', 5);
29define('PASSWORD_COMPARE_CURRENT', 6);
30define('PASSWORD_COMPARE_NEW', 7);
31define('PASSWORD_SUCCESS', 0);
32
33/**
34 * Change password plugin
35 *
36 * Plugin that adds functionality to change a users password.
37 * It provides common functionality and user interface and supports
38 * several backends to finally update the password.
39 *
40 * For installation and configuration instructions please read the README file.
41 *
42 * @author Aleksander Machniak
43 */
44class password extends rcube_plugin
45{
46    public $task    = '?(?!logout).*';
47    public $noframe = true;
48    public $noajax  = true;
49
50    private $newuser = false;
51    private $drivers = [];
52    private $rc;
53
54
55    function init()
56    {
57        $this->rc = rcmail::get_instance();
58
59        $this->load_config();
60
61        // update deprecated password_require_nonalpha option removed 20181007
62        if ($this->rc->config->get('password_minimum_score') === null && $this->rc->config->get('password_require_nonalpha')) {
63            $this->rc->config->set('password_minimum_score', 2);
64        }
65
66        if ($this->rc->task == 'settings') {
67            if (!$this->check_host_login_exceptions()) {
68                return;
69            }
70
71            $this->add_texts('localization/');
72
73            $this->add_hook('settings_actions', [$this, 'settings_actions']);
74
75            $this->register_action('plugin.password', [$this, 'password_init']);
76            $this->register_action('plugin.password-save', [$this, 'password_save']);
77        }
78
79        if ($this->rc->config->get('password_force_new_user')) {
80            if ($this->rc->config->get('newuserpassword') && $this->check_host_login_exceptions()) {
81                if (!($this->rc->task == 'settings' && strpos($this->rc->action, 'plugin.password') === 0)) {
82                    $this->rc->output->command('redirect', '?_task=settings&_action=plugin.password&_first=1', false);
83                }
84            }
85
86            $this->add_hook('user_create', [$this, 'user_create']);
87            $this->add_hook('login_after', [$this, 'login_after']);
88        }
89    }
90
91    function settings_actions($args)
92    {
93        // register as settings action
94        $args['actions'][] = [
95            'action' => 'plugin.password',
96            'class'  => 'password',
97            'label'  => 'password',
98            'title'  => 'changepasswd',
99            'domain' => 'password',
100        ];
101
102        return $args;
103    }
104
105    function password_init()
106    {
107        $this->register_handler('plugin.body', [$this, 'password_form']);
108
109        $this->rc->output->set_pagetitle($this->gettext('changepasswd'));
110
111        if (rcube_utils::get_input_value('_first', rcube_utils::INPUT_GET)) {
112            $this->rc->output->command('display_message', $this->gettext('firstloginchange'), 'notice');
113        }
114        else if (!empty($_SESSION['password_expires'])) {
115            if ($_SESSION['password_expires'] == 1) {
116                $this->rc->output->command('display_message', $this->gettext('passwdexpired'), 'error');
117            }
118            else {
119                $this->rc->output->command('display_message', $this->gettext([
120                        'name' => 'passwdexpirewarning',
121                        'vars' => ['expirationdatetime' => $_SESSION['password_expires']]
122                    ]), 'warning');
123            }
124        }
125
126        $this->rc->output->send('plugin');
127    }
128
129    function password_save()
130    {
131        $this->register_handler('plugin.body', [$this, 'password_form']);
132
133        $this->rc->output->set_pagetitle($this->gettext('changepasswd'));
134
135        $confirm         = $this->rc->config->get('password_confirm_current');
136        $required_length = (int) $this->rc->config->get('password_minimum_length', 8);
137        $force_save      = $this->rc->config->get('password_force_save');
138
139        if (($confirm && !isset($_POST['_curpasswd'])) || !isset($_POST['_newpasswd']) || !strlen($_POST['_newpasswd'])) {
140            $this->rc->output->command('display_message', $this->gettext('nopassword'), 'error');
141        }
142        else {
143            $charset    = strtoupper($this->rc->config->get('password_charset', 'UTF-8'));
144            $rc_charset = strtoupper($this->rc->output->get_charset());
145
146            $sesspwd = $this->rc->decrypt($_SESSION['password']);
147            $curpwd = $confirm ? rcube_utils::get_input_value('_curpasswd', rcube_utils::INPUT_POST, true, $charset) : $sesspwd;
148            $newpwd = rcube_utils::get_input_value('_newpasswd', rcube_utils::INPUT_POST, true);
149            $conpwd = rcube_utils::get_input_value('_confpasswd', rcube_utils::INPUT_POST, true);
150
151            // check allowed characters according to the configured 'password_charset' option
152            // by converting the password entered by the user to this charset and back to UTF-8
153            $orig_pwd = $newpwd;
154            $chk_pwd  = rcube_charset::convert($orig_pwd, $rc_charset, $charset);
155            $chk_pwd  = rcube_charset::convert($chk_pwd, $charset, $rc_charset);
156
157            // We're doing this for consistence with Roundcube core
158            $newpwd = rcube_charset::convert($newpwd, $rc_charset, $charset);
159            $conpwd = rcube_charset::convert($conpwd, $rc_charset, $charset);
160
161            if ($chk_pwd != $orig_pwd || preg_match('/[\x00-\x1F\x7F]/', $newpwd)) {
162                $this->rc->output->command('display_message', $this->gettext('passwordforbidden'), 'error');
163            }
164            // other passwords validity checks
165            else if ($conpwd != $newpwd) {
166                $this->rc->output->command('display_message', $this->gettext('passwordinconsistency'), 'error');
167            }
168            else if ($confirm && ($res = $this->_compare($sesspwd, $curpwd, PASSWORD_COMPARE_CURRENT))) {
169                $this->rc->output->command('display_message', $res, 'error');
170            }
171            else if ($required_length && strlen($newpwd) < $required_length) {
172                $this->rc->output->command('display_message', $this->gettext(
173                    ['name' => 'passwordshort', 'vars' => ['length' => $required_length]]), 'error');
174            }
175            else if ($res = $this->_check_strength($newpwd)) {
176                $this->rc->output->command('display_message', $res, 'error');
177            }
178            // password is the same as the old one, warn user, return error
179            else if (!$force_save && ($res = $this->_compare($sesspwd, $newpwd, PASSWORD_COMPARE_NEW))) {
180                $this->rc->output->command('display_message', $res, 'error');
181            }
182            // try to save the password
183            else if (!($res = $this->_save($curpwd, $newpwd))) {
184                $this->rc->output->command('display_message', $this->gettext('successfullysaved'), 'confirmation');
185
186                // allow additional actions after password change (e.g. reset some backends)
187                $plugin = $this->rc->plugins->exec_hook('password_change', [
188                        'old_pass' => $curpwd,
189                        'new_pass' => $newpwd
190                ]);
191
192                // Reset session password
193                $_SESSION['password'] = $this->rc->encrypt($plugin['new_pass']);
194
195                if ($this->rc->config->get('newuserpassword')) {
196                    $this->rc->user->save_prefs(['newuserpassword' => false]);
197                }
198
199                // Log password change
200                if ($this->rc->config->get('password_log')) {
201                    rcube::write_log('password', sprintf('Password changed for user %s (ID: %d) from %s',
202                        $this->rc->get_user_name(), $this->rc->user->ID, rcube_utils::remote_ip()));
203                }
204
205                // Remove expiration date/time
206                $this->rc->session->remove('password_expires');
207            }
208            else {
209                $this->rc->output->command('display_message', $res, 'error');
210            }
211        }
212
213        $this->rc->overwrite_action('plugin.password');
214        $this->rc->output->send('plugin');
215    }
216
217    function password_form()
218    {
219        // add some labels to client
220        $this->rc->output->add_label(
221            'password.nopassword',
222            'password.nocurpassword',
223            'password.passwordinconsistency'
224        );
225
226        $form_disabled = $this->rc->config->get('password_disabled');
227
228        $this->rc->output->set_env('product_name', $this->rc->config->get('product_name'));
229        $this->rc->output->set_env('password_disabled', !empty($form_disabled));
230
231        $table = new html_table(['cols' => 2, 'class' => 'propform']);
232
233        if ($this->rc->config->get('password_confirm_current')) {
234            // show current password selection
235            $field_id = 'curpasswd';
236            $input_curpasswd = new html_passwordfield([
237                    'name'         => '_curpasswd',
238                    'id'           => $field_id,
239                    'size'         => 20,
240                    'autocomplete' => 'off',
241            ]);
242
243            $table->add('title', html::label($field_id, rcube::Q($this->gettext('curpasswd'))));
244            $table->add(null, $input_curpasswd->show());
245        }
246
247        // show new password selection
248        $field_id = 'newpasswd';
249        $input_newpasswd = new html_passwordfield([
250                'name'         => '_newpasswd',
251                'id'           => $field_id,
252                'size'         => 20,
253                'autocomplete' => 'off',
254        ]);
255
256        $table->add('title', html::label($field_id, rcube::Q($this->gettext('newpasswd'))));
257        $table->add(null, $input_newpasswd->show());
258
259        // show confirm password selection
260        $field_id = 'confpasswd';
261        $input_confpasswd = new html_passwordfield([
262                'name'         => '_confpasswd',
263                'id'           => $field_id,
264                'size'         => 20,
265                'autocomplete' => 'off',
266        ]);
267
268        $table->add('title', html::label($field_id, rcube::Q($this->gettext('confpasswd'))));
269        $table->add(null, $input_confpasswd->show());
270
271        $rules = '';
272
273        $required_length = (int) $this->rc->config->get('password_minimum_length', 8);
274        if ($required_length > 0) {
275            $rules .= html::tag('li', ['class' => 'required-length d-block'], $this->gettext([
276                'name' => 'passwordshort',
277                'vars' => ['length' => $required_length]
278            ]));
279        }
280
281        if ($msgs = $this->_strength_rules()) {
282            foreach ($msgs as $msg) {
283                $rules .= html::tag('li', ['class' => 'strength-rule d-block'], $msg);
284            }
285        }
286
287        if (!empty($rules)) {
288            $rules = html::tag('ul', ['id' => 'ruleslist', 'class' => 'hint proplist'], $rules);
289        }
290
291        $disabled_msg = '';
292        if ($form_disabled) {
293            $disabled_msg = is_string($form_disabled) ? $form_disabled : $this->gettext('disablednotice');
294            $disabled_msg = html::div(['class' => 'boxwarning', 'id' => 'password-notice'], $disabled_msg);
295        }
296
297        $submit_button = $this->rc->output->button([
298                'command' => 'plugin.password-save',
299                'class'   => 'button mainaction submit',
300                'label'   => 'save',
301        ]);
302        $form_buttons = html::p(['class' => 'formbuttons footerleft'], $submit_button);
303
304        $this->rc->output->add_gui_object('passform', 'password-form');
305
306        $this->include_script('password.js');
307
308        $form = $this->rc->output->form_tag([
309                'id'     => 'password-form',
310                'name'   => 'password-form',
311                'method' => 'post',
312                'action' => './?_task=settings&_action=plugin.password-save',
313            ],
314            $disabled_msg . $table->show() . $rules
315        );
316
317        return html::div(['id' => 'prefs-title', 'class' => 'boxtitle'], $this->gettext('changepasswd'))
318            . html::div(['class' => 'box formcontainer scroller'],
319                html::div(['class' => 'boxcontent formcontent'], $form) . $form_buttons
320            );
321    }
322
323    private function _compare($curpwd, $newpwd, $type)
324    {
325        $driver = $this->_load_driver();
326
327        if (!$driver) {
328            $result = $this->gettext('internalerror');
329        }
330        else if (method_exists($driver, 'compare')) {
331            $result = $driver->compare($curpwd, $newpwd, $type);
332        }
333        else {
334            switch ($type) {
335            case PASSWORD_COMPARE_CURRENT:
336                $result = $curpwd != $newpwd ? $this->gettext('passwordincorrect') : null;
337                break;
338            case PASSWORD_COMPARE_NEW:
339                $result = $curpwd == $newpwd ? $this->gettext('samepasswd') : null;
340                break;
341            default:
342                $result = $this->gettext('internalerror');
343            }
344        }
345
346        return $result;
347    }
348
349    private function _strength_rules()
350    {
351        $result = null;
352
353        if (($driver = $this->_load_driver('strength')) && method_exists($driver, 'strength_rules')) {
354            $result = $driver->strength_rules();
355        }
356        else if ($this->rc->config->get('password_minimum_score') > 1) {
357            $result = $this->gettext('passwordweak');
358        }
359
360        if (!is_array($result)) {
361            $result = [$result];
362        }
363
364        return $result;
365    }
366
367    private function _check_strength($passwd)
368    {
369        $min_score = $this->rc->config->get('password_minimum_score');
370
371        if (!$min_score) {
372            return;
373        }
374
375        if (($driver = $this->_load_driver('strength')) && method_exists($driver, 'check_strength')) {
376            list($score, $reason) = $driver->check_strength($passwd);
377        }
378        else {
379            $score = (!preg_match("/[0-9]/", $passwd) || !preg_match("/[^A-Za-z0-9]/", $passwd)) ? 1 : 5;
380        }
381
382        if ($score < $min_score) {
383            return $this->gettext('passwordtooweak') . (!empty($reason) ? " $reason" : '');
384        }
385    }
386
387    private function _save($curpass, $passwd)
388    {
389        if (!($driver = $this->_load_driver())) {
390            return $this->gettext('internalerror');
391        }
392
393        $result  = $driver->save($curpass, $passwd, self::username());
394        $message = '';
395
396        if (is_array($result)) {
397            $message = $result['message'];
398            $result  = $result['code'];
399        }
400
401        switch ($result) {
402            case PASSWORD_SUCCESS:
403                return;
404            case PASSWORD_CRYPT_ERROR:
405                $reason = $this->gettext('crypterror');
406                break;
407            case PASSWORD_CONNECT_ERROR:
408                $reason = $this->gettext('connecterror');
409                break;
410            case PASSWORD_IN_HISTORY:
411                $reason = $this->gettext('passwdinhistory');
412                break;
413            case PASSWORD_CONSTRAINT_VIOLATION:
414                $reason = $this->gettext('passwdconstraintviolation');
415                break;
416            case PASSWORD_ERROR:
417            default:
418                $reason = $this->gettext('internalerror');
419        }
420
421        if ($message) {
422            $reason .= ' ' . $message;
423        }
424
425        return $reason;
426    }
427
428    private function _load_driver($type = 'password')
429    {
430        if (!($type && $driver = $this->rc->config->get('password_' . $type . '_driver'))) {
431            $driver = $this->rc->config->get('password_driver', 'sql');
432        }
433
434        if (empty($this->drivers[$type])) {
435            $class  = "rcube_{$driver}_password";
436            $file = $this->home . "/drivers/$driver.php";
437
438            if (!file_exists($file)) {
439                rcube::raise_error([
440                        'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
441                        'message' => "Password plugin: Driver file does not exist ($file)"
442                    ], true, false
443                );
444                return false;
445            }
446
447            include_once $file;
448
449            if (!class_exists($class, false) || (!method_exists($class, 'save') && !method_exists($class, 'check_strength'))) {
450                rcube::raise_error([
451                        'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
452                        'message' => "Password plugin: Broken driver $driver"
453                    ], true, false
454                );
455                return false;
456            }
457
458            $this->drivers[$type] = new $class;
459        }
460
461        return $this->drivers[$type];
462    }
463
464    function user_create($args)
465    {
466        $this->newuser = true;
467        return $args;
468    }
469
470    function login_after($args)
471    {
472        if ($this->newuser && $this->check_host_login_exceptions()) {
473            $this->rc->user->save_prefs(['newuserpassword' => true]);
474
475            $args['_task']   = 'settings';
476            $args['_action'] = 'plugin.password';
477            $args['_first']  = 'true';
478        }
479
480        return $args;
481    }
482
483    // Check if host and login is allowed to change the password, false = not allowed, true = not allowed
484    private function check_host_login_exceptions()
485    {
486        // Host exceptions
487        $hosts = $this->rc->config->get('password_hosts');
488        if (!empty($hosts) && !in_array($_SESSION['storage_host'], (array) $hosts)) {
489            return false;
490        }
491
492        // Login exceptions
493        if ($exceptions = $this->rc->config->get('password_login_exceptions')) {
494            $exceptions = array_map('trim', (array) $exceptions);
495            $exceptions = array_filter($exceptions);
496            $username   = $_SESSION['username'];
497
498            foreach ($exceptions as $ec) {
499                if ($username === $ec) {
500                    return false;
501                }
502            }
503        }
504
505        return true;
506    }
507
508    /**
509     * Hashes a password and returns the hash based on the specified method
510     *
511     * Parts of the code originally from the phpLDAPadmin development team
512     * http://phpldapadmin.sourceforge.net/
513     *
514     * @param string      Clear password
515     * @param string      Hashing method
516     * @param bool|string Prefix string or TRUE to add a default prefix
517     *
518     * @return string Hashed password
519     */
520    public static function hash_password($password, $method = '', $prefixed = true)
521    {
522        $method  = strtolower($method);
523        $rcmail  = rcmail::get_instance();
524        $prefix  = '';
525        $crypted = null;
526
527        if (empty($method) || $method == 'default') {
528            $method   = $rcmail->config->get('password_algorithm');
529            $prefixed = $rcmail->config->get('password_algorithm_prefix');
530        }
531        else if ($method == 'crypt') { // deprecated
532            if (!($method = $rcmail->config->get('password_crypt_hash'))) {
533                $method = 'md5';
534            }
535
536            if (!strpos($method, '-crypt')) {
537                $method .= '-crypt';
538            }
539        }
540
541        switch ($method) {
542        case 'des':
543        case 'des-crypt':
544            $crypted = crypt($password, rcube_utils::random_bytes(2));
545            $prefix  = '{CRYPT}';
546            break;
547
548        case 'ext_des': // for BC
549        case 'ext-des-crypt':
550            $crypted = crypt($password, '_' . rcube_utils::random_bytes(8));
551            $prefix  = '{CRYPT}';
552            break;
553
554        case 'md5crypt': // for BC
555        case 'md5-crypt':
556            $crypted = crypt($password, '$1$' . rcube_utils::random_bytes(9));
557            $prefix  = '{CRYPT}';
558            break;
559
560        case 'sha256-crypt':
561            $rounds = (int) $rcmail->config->get('password_crypt_rounds');
562            $prefix = '$5$';
563
564            if ($rounds > 1000) {
565                $prefix .= 'rounds=' . $rounds . '$';
566            }
567
568            $crypted = crypt($password, $prefix . rcube_utils::random_bytes(16));
569            $prefix  = '{CRYPT}';
570            break;
571
572        case 'sha512-crypt':
573            $rounds = (int) $rcmail->config->get('password_crypt_rounds');
574            $prefix = '$6$';
575
576            if ($rounds > 1000) {
577                $prefix .= 'rounds=' . $rounds . '$';
578            }
579
580            $crypted = crypt($password, $prefix . rcube_utils::random_bytes(16));
581            $prefix  = '{CRYPT}';
582            break;
583
584        case 'blowfish': // for BC
585        case 'blowfish-crypt':
586            $cost   = (int) $rcmail->config->get('password_blowfish_cost');
587            $cost   = $cost < 4 || $cost > 31 ? 12 : $cost;
588            $prefix = sprintf('$2a$%02d$', $cost);
589
590            $crypted = crypt($password, $prefix . rcube_utils::random_bytes(22));
591            $prefix  = '{CRYPT}';
592            break;
593
594        case 'md5':
595            $crypted = base64_encode(pack('H*', md5($password)));
596            $prefix  = '{MD5}';
597            break;
598
599        case 'sha':
600            if (function_exists('sha1')) {
601                $crypted = pack('H*', sha1($password));
602            }
603            else if (function_exists('hash')) {
604                $crypted = hash('sha1', $password, true);
605            }
606            else if (function_exists('mhash')) {
607                $crypted = mhash(MHASH_SHA1, $password);
608            }
609            else {
610                rcube::raise_error([
611                        'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
612                        'message' => "Password plugin: Your PHP install does not have the mhash()/hash() nor sha1() function"
613                    ], true, true
614                );
615            }
616
617            $crypted = base64_encode($crypted);
618            $prefix = '{SHA}';
619            break;
620
621        case 'ssha':
622            $salt = rcube_utils::random_bytes(8);
623
624            if (function_exists('mhash') && function_exists('mhash_keygen_s2k')) {
625                $salt    = mhash_keygen_s2k(MHASH_SHA1, $password, $salt, 4);
626                $crypted = mhash(MHASH_SHA1, $password . $salt);
627            }
628            else if (function_exists('sha1')) {
629                $salt    = substr(pack("H*", sha1($salt . $password)), 0, 4);
630                $crypted = sha1($password . $salt, true);
631            }
632            else if (function_exists('hash')) {
633                $salt    = substr(pack("H*", hash('sha1', $salt . $password)), 0, 4);
634                $crypted = hash('sha1', $password . $salt, true);
635            }
636            else {
637                rcube::raise_error([
638                        'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
639                        'message' => "Password plugin: Your PHP install does not have the mhash()/hash() nor sha1() function"
640                   ], true, true
641               );
642            }
643
644            $crypted = base64_encode($crypted . $salt);
645            $prefix  = '{SSHA}';
646            break;
647
648        case 'ssha512':
649            $salt = rcube_utils::random_bytes(8);
650
651            if (function_exists('mhash') && function_exists('mhash_keygen_s2k')) {
652                $salt    = mhash_keygen_s2k(MHASH_SHA512, $password, $salt, 4);
653                $crypted = mhash(MHASH_SHA512, $password . $salt);
654            }
655            else if (function_exists('hash')) {
656                $salt    = substr(pack("H*", hash('sha512', $salt . $password)), 0, 4);
657                $crypted = hash('sha512', $password . $salt, true);
658            }
659            else {
660                rcube::raise_error([
661                        'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
662                        'message' => "Password plugin: Your PHP install does not have the mhash()/hash() function"
663                    ], true, true
664                );
665            }
666
667            $crypted = base64_encode($crypted . $salt);
668            $prefix  = '{SSHA512}';
669            break;
670
671        case 'smd5':
672            $salt = rcube_utils::random_bytes(8);
673
674            if (function_exists('mhash') && function_exists('mhash_keygen_s2k')) {
675                $salt    = mhash_keygen_s2k(MHASH_MD5, $password, $salt, 4);
676                $crypted = mhash(MHASH_MD5, $password . $salt);
677            }
678            else if (function_exists('hash')) {
679                $salt    = substr(pack("H*", hash('md5', $salt . $password)), 0, 4);
680                $crypted = hash('md5', $password . $salt, true);
681            }
682            else {
683                $salt    = substr(pack("H*", md5($salt . $password)), 0, 4);
684                $crypted = md5($password . $salt, true);
685            }
686
687            $crypted = base64_encode($crypted . $salt);
688            $prefix  = '{SMD5}';
689            break;
690
691        case 'samba':
692            if (function_exists('hash')) {
693                $crypted = hash('md4', rcube_charset::convert($password, RCUBE_CHARSET, 'UTF-16LE'));
694                $crypted = strtoupper($crypted);
695            }
696            else {
697                rcube::raise_error([
698                        'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
699                        'message' => "Password plugin: Your PHP install does not have hash() function"
700                    ], true, true
701                );
702            }
703            break;
704
705        case 'ad':
706            $crypted = rcube_charset::convert('"' . $password . '"', RCUBE_CHARSET, 'UTF-16LE');
707            break;
708
709        case 'cram-md5': // deprecated
710            require_once __DIR__ . '/../helpers/dovecot_hmacmd5.php';
711            $crypted = dovecot_hmacmd5($password);
712            $prefix  = '{CRAM-MD5}';
713            break;
714
715        case 'dovecot':
716            if (!($dovecotpw = $rcmail->config->get('password_dovecotpw'))) {
717                $dovecotpw = 'dovecotpw';
718            }
719            if (!($method = $rcmail->config->get('password_dovecotpw_method'))) {
720                $method = 'CRAM-MD5';
721            }
722
723            $spec = [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['file', '/dev/null', 'a']];
724            $pipe = proc_open("$dovecotpw -s '$method'", $spec, $pipes);
725
726            if (!is_resource($pipe)) {
727                return false;
728            }
729
730            fwrite($pipes[0], $password . "\n", 1+strlen($password));
731            usleep(1000);
732            fwrite($pipes[0], $password . "\n", 1+strlen($password));
733
734            $crypted = trim(stream_get_contents($pipes[1]), "\n");
735
736            fclose($pipes[0]);
737            fclose($pipes[1]);
738            proc_close($pipe);
739
740            if (!preg_match('/^\{' . $method . '\}/', $crypted)) {
741                return false;
742            }
743
744            if (!$prefixed) {
745                $prefixed = (bool) $rcmail->config->get('password_dovecotpw_with_method');
746            }
747
748            if (!$prefixed) {
749                $crypted = trim(str_replace('{' . $method . '}', '', $crypted));
750            }
751
752            $prefixed = false;
753
754            break;
755
756        case 'hash': // deprecated
757            if (!extension_loaded('hash')) {
758                rcube::raise_error([
759                        'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
760                        'message' => "Password plugin: 'hash' extension not loaded!"
761                    ], true, true
762                );
763            }
764
765            if (!($hash_algo = strtolower($rcmail->config->get('password_hash_algorithm')))) {
766                $hash_algo = 'sha1';
767            }
768
769            $crypted = hash($hash_algo, $password);
770
771            if ($rcmail->config->get('password_hash_base64')) {
772                $crypted = base64_encode(pack('H*', $crypted));
773            }
774
775            break;
776
777        case 'clear':
778            $crypted = $password;
779            break;
780
781        default:
782            rcube::raise_error([
783                    'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
784                    'message' => "Password plugin: Hash method not supported."
785                ], true, true
786            );
787        }
788
789        if ($crypted === null || $crypted === false) {
790            rcube::raise_error([
791                    'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
792                    'message' => "Password plugin: Failed to hash password ($method). Check for configuration issues."
793                ],
794                true, true
795            );
796        }
797
798        if ($prefixed && $prefixed !== true) {
799            $prefix   = $prefixed;
800            $prefixed = true;
801        }
802
803        if ($prefixed === true && $prefix) {
804            $crypted = $prefix . $crypted;
805        }
806
807        return $crypted;
808    }
809
810    /**
811     * Returns username in a configured form appropriate for the driver
812     *
813     * @param string $format Username format
814     *
815     * @return string Username
816     */
817    public static function username($format = null)
818    {
819        $rcmail = rcmail::get_instance();
820
821        if (!$format) {
822            $format = $rcmail->config->get('password_username_format');
823        }
824
825        if (!$format) {
826            return $_SESSION['username'];
827        }
828
829        return strtr($format, [
830                '%l' => $rcmail->user->get_username('local'),
831                '%d' => $rcmail->user->get_username('domain'),
832                '%u' => $_SESSION['username'],
833        ]);
834    }
835
836    /**
837     * Returns Guzzle HTTP client instance configured for use in a password driver.
838     *
839     * @return \GuzzleHttp\Client HTTP client
840     */
841    public static function get_http_client()
842    {
843        $rcube  = rcube::get_instance();
844        $config = (array) $rcube->config->get('password_http_client');
845
846        return $rcube->get_http_client($config);
847    }
848}
849