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