comparison plugins/password/password.php @ 0:1e000243b222

vanilla 1.3.3 distro, I hope
author Charlie Root
date Thu, 04 Jan 2018 15:50:29 -0500
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:1e000243b222
1 <?php
2
3 /**
4 * Password Plugin for Roundcube
5 *
6 * @author Aleksander Machniak <alec@alec.pl>
7 *
8 * Copyright (C) 2005-2015, 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
24 define('PASSWORD_CRYPT_ERROR', 1);
25 define('PASSWORD_ERROR', 2);
26 define('PASSWORD_CONNECT_ERROR', 3);
27 define('PASSWORD_IN_HISTORY', 4);
28 define('PASSWORD_CONSTRAINT_VIOLATION', 5);
29 define('PASSWORD_SUCCESS', 0);
30
31 /**
32 * Change password plugin
33 *
34 * Plugin that adds functionality to change a users password.
35 * It provides common functionality and user interface and supports
36 * several backends to finally update the password.
37 *
38 * For installation and configuration instructions please read the README file.
39 *
40 * @author Aleksander Machniak
41 */
42 class password extends rcube_plugin
43 {
44 public $task = 'settings|login';
45 public $noframe = true;
46 public $noajax = true;
47
48 private $newuser = false;
49
50 function init()
51 {
52 $rcmail = rcmail::get_instance();
53
54 $this->load_config();
55
56 if ($rcmail->task == 'settings') {
57 if (!$this->check_host_login_exceptions()) {
58 return;
59 }
60
61 $this->add_texts('localization/');
62
63 $this->add_hook('settings_actions', array($this, 'settings_actions'));
64
65 $this->register_action('plugin.password', array($this, 'password_init'));
66 $this->register_action('plugin.password-save', array($this, 'password_save'));
67 }
68 else if ($rcmail->config->get('password_force_new_user')) {
69 $this->add_hook('user_create', array($this, 'user_create'));
70 $this->add_hook('login_after', array($this, 'login_after'));
71 }
72 }
73
74 function settings_actions($args)
75 {
76 // register as settings action
77 $args['actions'][] = array(
78 'action' => 'plugin.password',
79 'class' => 'password',
80 'label' => 'password',
81 'title' => 'changepasswd',
82 'domain' => 'password',
83 );
84
85 return $args;
86 }
87
88 function password_init()
89 {
90 $this->register_handler('plugin.body', array($this, 'password_form'));
91
92 $rcmail = rcmail::get_instance();
93 $rcmail->output->set_pagetitle($this->gettext('changepasswd'));
94
95 if (rcube_utils::get_input_value('_first', rcube_utils::INPUT_GET)) {
96 $rcmail->output->command('display_message', $this->gettext('firstloginchange'), 'notice');
97 }
98 else if (!empty($_SESSION['password_expires'])) {
99 if ($_SESSION['password_expires'] == 1) {
100 $rcmail->output->command('display_message', $this->gettext('passwdexpired'), 'error');
101 }
102 else {
103 $rcmail->output->command('display_message', $this->gettext(array(
104 'name' => 'passwdexpirewarning',
105 'vars' => array('expirationdatetime' => $_SESSION['password_expires'])
106 )), 'warning');
107 }
108 }
109
110 $rcmail->output->send('plugin');
111 }
112
113 function password_save()
114 {
115 $this->register_handler('plugin.body', array($this, 'password_form'));
116
117 $rcmail = rcmail::get_instance();
118 $rcmail->output->set_pagetitle($this->gettext('changepasswd'));
119
120 $form_disabled = $rcmail->config->get('password_disabled');
121 $confirm = $rcmail->config->get('password_confirm_current');
122 $required_length = intval($rcmail->config->get('password_minimum_length'));
123 $check_strength = $rcmail->config->get('password_require_nonalpha');
124
125 if (($confirm && !isset($_POST['_curpasswd'])) || !isset($_POST['_newpasswd'])) {
126 $rcmail->output->command('display_message', $this->gettext('nopassword'), 'error');
127 }
128 else {
129 $charset = strtoupper($rcmail->config->get('password_charset', 'ISO-8859-1'));
130 $rc_charset = strtoupper($rcmail->output->get_charset());
131
132 $sespwd = $rcmail->decrypt($_SESSION['password']);
133 $curpwd = $confirm ? rcube_utils::get_input_value('_curpasswd', rcube_utils::INPUT_POST, true, $charset) : $sespwd;
134 $newpwd = rcube_utils::get_input_value('_newpasswd', rcube_utils::INPUT_POST, true);
135 $conpwd = rcube_utils::get_input_value('_confpasswd', rcube_utils::INPUT_POST, true);
136
137 // check allowed characters according to the configured 'password_charset' option
138 // by converting the password entered by the user to this charset and back to UTF-8
139 $orig_pwd = $newpwd;
140 $chk_pwd = rcube_charset::convert($orig_pwd, $rc_charset, $charset);
141 $chk_pwd = rcube_charset::convert($chk_pwd, $charset, $rc_charset);
142
143 // WARNING: Default password_charset is ISO-8859-1, so conversion will
144 // change national characters. This may disable possibility of using
145 // the same password in other MUA's.
146 // We're doing this for consistence with Roundcube core
147 $newpwd = rcube_charset::convert($newpwd, $rc_charset, $charset);
148 $conpwd = rcube_charset::convert($conpwd, $rc_charset, $charset);
149
150 if ($chk_pwd != $orig_pwd) {
151 $rcmail->output->command('display_message', $this->gettext('passwordforbidden'), 'error');
152 }
153 // other passwords validity checks
154 else if ($conpwd != $newpwd) {
155 $rcmail->output->command('display_message', $this->gettext('passwordinconsistency'), 'error');
156 }
157 else if ($confirm && $sespwd != $curpwd) {
158 $rcmail->output->command('display_message', $this->gettext('passwordincorrect'), 'error');
159 }
160 else if ($required_length && strlen($newpwd) < $required_length) {
161 $rcmail->output->command('display_message', $this->gettext(
162 array('name' => 'passwordshort', 'vars' => array('length' => $required_length))), 'error');
163 }
164 else if ($check_strength && (!preg_match("/[0-9]/", $newpwd) || !preg_match("/[^A-Za-z0-9]/", $newpwd))) {
165 $rcmail->output->command('display_message', $this->gettext('passwordweak'), 'error');
166 }
167 // password is the same as the old one, warn user, return error
168 else if ($sespwd == $newpwd && !$rcmail->config->get('password_force_save')) {
169 $rcmail->output->command('display_message', $this->gettext('samepasswd'), 'error');
170 }
171 // try to save the password
172 else if (!($res = $this->_save($curpwd, $newpwd))) {
173 $rcmail->output->command('display_message', $this->gettext('successfullysaved'), 'confirmation');
174
175 // allow additional actions after password change (e.g. reset some backends)
176 $plugin = $rcmail->plugins->exec_hook('password_change', array(
177 'old_pass' => $curpwd, 'new_pass' => $newpwd));
178
179 // Reset session password
180 $_SESSION['password'] = $rcmail->encrypt($plugin['new_pass']);
181
182 // Log password change
183 if ($rcmail->config->get('password_log')) {
184 rcube::write_log('password', sprintf('Password changed for user %s (ID: %d) from %s',
185 $rcmail->get_user_name(), $rcmail->user->ID, rcube_utils::remote_ip()));
186 }
187
188 // Remove expiration date/time
189 $rcmail->session->remove('password_expires');
190 }
191 else {
192 $rcmail->output->command('display_message', $res, 'error');
193 }
194 }
195
196 $rcmail->overwrite_action('plugin.password');
197 $rcmail->output->send('plugin');
198 }
199
200 function password_form()
201 {
202 $rcmail = rcmail::get_instance();
203
204 // add some labels to client
205 $rcmail->output->add_label(
206 'password.nopassword',
207 'password.nocurpassword',
208 'password.passwordinconsistency'
209 );
210
211 $form_disabled = $rcmail->config->get('password_disabled');
212
213 $rcmail->output->set_env('product_name', $rcmail->config->get('product_name'));
214 $rcmail->output->set_env('password_disabled', !empty($form_disabled));
215
216 $table = new html_table(array('cols' => 2));
217
218 if ($rcmail->config->get('password_confirm_current')) {
219 // show current password selection
220 $field_id = 'curpasswd';
221 $input_curpasswd = new html_passwordfield(array(
222 'name' => '_curpasswd',
223 'id' => $field_id,
224 'size' => 20,
225 'autocomplete' => 'off',
226 ));
227
228 $table->add('title', html::label($field_id, rcube::Q($this->gettext('curpasswd'))));
229 $table->add(null, $input_curpasswd->show());
230 }
231
232 // show new password selection
233 $field_id = 'newpasswd';
234 $input_newpasswd = new html_passwordfield(array(
235 'name' => '_newpasswd',
236 'id' => $field_id,
237 'size' => 20,
238 'autocomplete' => 'off',
239 ));
240
241 $table->add('title', html::label($field_id, rcube::Q($this->gettext('newpasswd'))));
242 $table->add(null, $input_newpasswd->show());
243
244 // show confirm password selection
245 $field_id = 'confpasswd';
246 $input_confpasswd = new html_passwordfield(array(
247 'name' => '_confpasswd',
248 'id' => $field_id,
249 'size' => 20,
250 'autocomplete' => 'off',
251 ));
252
253 $table->add('title', html::label($field_id, rcube::Q($this->gettext('confpasswd'))));
254 $table->add(null, $input_confpasswd->show());
255
256 $rules = '';
257
258 $required_length = intval($rcmail->config->get('password_minimum_length'));
259 if ($required_length > 0) {
260 $rules .= html::tag('li', array('id' => 'required-length'), $this->gettext(array(
261 'name' => 'passwordshort',
262 'vars' => array('length' => $required_length)
263 )));
264 }
265
266 if ($rcmail->config->get('password_require_nonalpha')) {
267 $rules .= html::tag('li', array('id' => 'require-nonalpha'), $this->gettext('passwordweak'));
268 }
269
270 if (!empty($rules)) {
271 $rules = html::tag('ul', array('id' => 'ruleslist'), $rules);
272 }
273
274 $disabled_msg = '';
275 if ($form_disabled) {
276 $disabled_msg = is_string($form_disabled) ? $form_disabled : $this->gettext('disablednotice');
277 $disabled_msg = html::div(array('class' => 'boxwarning', 'id' => 'password-notice'), $disabled_msg);
278 }
279
280 $submit_button = $rcmail->output->button(array(
281 'command' => 'plugin.password-save',
282 'type' => 'input',
283 'class' => 'button mainaction',
284 'label' => 'save',
285 ));
286 $form_buttons = html::p(array('class' => 'formbuttons'), $submit_button);
287
288 $out = html::div(array('class' => 'box'),
289 html::div(array('id' => 'prefs-title', 'class' => 'boxtitle'), $this->gettext('changepasswd'))
290 . html::div(array('class' => 'boxcontent'),
291 $disabled_msg . $table->show() . $rules . $form_buttons));
292
293 $rcmail->output->add_gui_object('passform', 'password-form');
294
295 $this->include_script('password.js');
296
297 return $rcmail->output->form_tag(array(
298 'id' => 'password-form',
299 'name' => 'password-form',
300 'method' => 'post',
301 'action' => './?_task=settings&_action=plugin.password-save',
302 ), $out);
303 }
304
305 private function _save($curpass, $passwd)
306 {
307 $config = rcmail::get_instance()->config;
308 $driver = $config->get('password_driver', 'sql');
309 $class = "rcube_{$driver}_password";
310 $file = $this->home . "/drivers/$driver.php";
311
312 if (!file_exists($file)) {
313 rcube::raise_error(array(
314 'code' => 600,
315 'type' => 'php',
316 'file' => __FILE__, 'line' => __LINE__,
317 'message' => "Password plugin: Unable to open driver file ($file)"
318 ), true, false);
319 return $this->gettext('internalerror');
320 }
321
322 include_once $file;
323
324 if (!class_exists($class, false) || !method_exists($class, 'save')) {
325 rcube::raise_error(array(
326 'code' => 600,
327 'type' => 'php',
328 'file' => __FILE__, 'line' => __LINE__,
329 'message' => "Password plugin: Broken driver $driver"
330 ), true, false);
331 return $this->gettext('internalerror');
332 }
333
334 $object = new $class;
335 $result = $object->save($curpass, $passwd);
336 $message = '';
337
338 if (is_array($result)) {
339 $message = $result['message'];
340 $result = $result['code'];
341 }
342
343 switch ($result) {
344 case PASSWORD_SUCCESS:
345 return;
346 case PASSWORD_CRYPT_ERROR:
347 $reason = $this->gettext('crypterror');
348 break;
349 case PASSWORD_CONNECT_ERROR:
350 $reason = $this->gettext('connecterror');
351 break;
352 case PASSWORD_IN_HISTORY:
353 $reason = $this->gettext('passwdinhistory');
354 break;
355 case PASSWORD_CONSTRAINT_VIOLATION:
356 $reason = $this->gettext('passwdconstraintviolation');
357 break;
358 case PASSWORD_ERROR:
359 default:
360 $reason = $this->gettext('internalerror');
361 }
362
363 if ($message) {
364 $reason .= ' ' . $message;
365 }
366
367 return $reason;
368 }
369
370 function user_create($args)
371 {
372 $this->newuser = true;
373 return $args;
374 }
375
376 function login_after($args)
377 {
378 if ($this->newuser && $this->check_host_login_exceptions()) {
379 $args['_task'] = 'settings';
380 $args['_action'] = 'plugin.password';
381 $args['_first'] = 'true';
382 }
383
384 return $args;
385 }
386
387 // Check if host and login is allowed to change the password, false = not allowed, true = not allowed
388 private function check_host_login_exceptions()
389 {
390 $rcmail = rcmail::get_instance();
391
392 // Host exceptions
393 $hosts = $rcmail->config->get('password_hosts');
394 if (!empty($hosts) && !in_array($_SESSION['storage_host'], (array) $hosts)) {
395 return false;
396 }
397
398 // Login exceptions
399 if ($exceptions = $rcmail->config->get('password_login_exceptions')) {
400 $exceptions = array_map('trim', (array) $exceptions);
401 $exceptions = array_filter($exceptions);
402 $username = $_SESSION['username'];
403
404 foreach ($exceptions as $ec) {
405 if ($username === $ec) {
406 return false;
407 }
408 }
409 }
410
411 return true;
412 }
413
414 /**
415 * Hashes a password and returns the hash based on the specified method
416 *
417 * Parts of the code originally from the phpLDAPadmin development team
418 * http://phpldapadmin.sourceforge.net/
419 *
420 * @param string Clear password
421 * @param string Hashing method
422 * @param bool|string Prefix string or TRUE to add a default prefix
423 *
424 * @return string Hashed password
425 */
426 static function hash_password($password, $method = '', $prefixed = true)
427 {
428 $method = strtolower($method);
429 $rcmail = rcmail::get_instance();
430 $prefix = '';
431 $crypted = '';
432 $default = false;
433
434 if (empty($method) || $method == 'default') {
435 $method = $rcmail->config->get('password_algorithm');
436 $prefixed = $rcmail->config->get('password_algorithm_prefix');
437 $default = true;
438 }
439 else if ($method == 'crypt') { // deprecated
440 if (!($method = $rcmail->config->get('password_crypt_hash'))) {
441 $method = 'md5';
442 }
443
444 if (!strpos($method, '-crypt')) {
445 $method .= '-crypt';
446 }
447 }
448
449 switch ($method) {
450 case 'des':
451 case 'des-crypt':
452 $crypted = crypt($password, rcube_utils::random_bytes(2));
453 $prefix = '{CRYPT}';
454 break;
455
456 case 'ext_des': // for BC
457 case 'ext-des-crypt':
458 $crypted = crypt($password, '_' . rcube_utils::random_bytes(8));
459 $prefix = '{CRYPT}';
460 break;
461
462 case 'md5crypt': // for BC
463 case 'md5-crypt':
464 $crypted = crypt($password, '$1$' . rcube_utils::random_bytes(9));
465 $prefix = '{CRYPT}';
466 break;
467
468 case 'sha256-crypt':
469 $rounds = (int) $rcmail->config->get('password_crypt_rounds');
470 $prefix = '$5$';
471
472 if ($rounds > 1000) {
473 $prefix .= 'rounds=' . $rounds . '$';
474 }
475
476 $crypted = crypt($password, $prefix . rcube_utils::random_bytes(16));
477 $prefix = '{CRYPT}';
478 break;
479
480 case 'sha512-crypt':
481 $rounds = (int) $rcmail->config->get('password_crypt_rounds');
482 $prefix = '$6$';
483
484 if ($rounds > 1000) {
485 $prefix .= 'rounds=' . $rounds . '$';
486 }
487
488 $crypted = crypt($password, $prefix . rcube_utils::random_bytes(16));
489 $prefix = '{CRYPT}';
490 break;
491
492 case 'blowfish': // for BC
493 case 'blowfish-crypt':
494 $cost = (int) $rcmail->config->get('password_blowfish_cost');
495 $cost = $cost < 4 || $cost > 31 ? 12 : $cost;
496 $prefix = sprintf('$2a$%02d$', $cost);
497
498 $crypted = crypt($password, $prefix . rcube_utils::random_bytes(22));
499 $prefix = '{CRYPT}';
500 break;
501
502 case 'md5':
503 $crypted = base64_encode(pack('H*', md5($password)));
504 $prefix = '{MD5}';
505 break;
506
507 case 'sha':
508 if (function_exists('sha1')) {
509 $crypted = pack('H*', sha1($password));
510 }
511 else if (function_exists('hash')) {
512 $crypted = hash('sha1', $password, true);
513 }
514 else if (function_exists('mhash')) {
515 $crypted = mhash(MHASH_SHA1, $password);
516 }
517 else {
518 rcube::raise_error(array(
519 'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
520 'message' => "Password plugin: Your PHP install does not have the mhash()/hash() nor sha1() function"
521 ), true, true);
522 }
523
524 $crypted = base64_encode($crypted);
525 $prefix = '{SHA}';
526 break;
527
528 case 'ssha':
529 $salt = rcube_utils::random_bytes(8);
530
531 if (function_exists('mhash') && function_exists('mhash_keygen_s2k')) {
532 $salt = mhash_keygen_s2k(MHASH_SHA1, $password, $salt, 4);
533 $crypted = mhash(MHASH_SHA1, $password . $salt);
534 }
535 else if (function_exists('sha1')) {
536 $salt = substr(pack("H*", sha1($salt . $password)), 0, 4);
537 $crypted = sha1($password . $salt, true);
538 }
539 else if (function_exists('hash')) {
540 $salt = substr(pack("H*", hash('sha1', $salt . $password)), 0, 4);
541 $crypted = hash('sha1', $password . $salt, true);
542 }
543 else {
544 rcube::raise_error(array(
545 'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
546 'message' => "Password plugin: Your PHP install does not have the mhash()/hash() nor sha1() function"
547 ), true, true);
548 }
549
550 $crypted = base64_encode($crypted . $salt);
551 $prefix = '{SSHA}';
552 break;
553
554 case 'smd5':
555 $salt = rcube_utils::random_bytes(8);
556
557 if (function_exists('mhash') && function_exists('mhash_keygen_s2k')) {
558 $salt = mhash_keygen_s2k(MHASH_MD5, $password, $salt, 4);
559 $crypted = mhash(MHASH_MD5, $password . $salt);
560 }
561 else if (function_exists('hash')) {
562 $salt = substr(pack("H*", hash('md5', $salt . $password)), 0, 4);
563 $crypted = hash('md5', $password . $salt, true);
564 }
565 else {
566 $salt = substr(pack("H*", md5($salt . $password)), 0, 4);
567 $crypted = md5($password . $salt, true);
568 }
569
570 $crypted = base64_encode($crypted . $salt);
571 $prefix = '{SMD5}';
572 break;
573
574 case 'samba':
575 if (function_exists('hash')) {
576 $crypted = hash('md4', rcube_charset::convert($password, RCUBE_CHARSET, 'UTF-16LE'));
577 $crypted = strtoupper($crypted);
578 }
579 else {
580 rcube::raise_error(array(
581 'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
582 'message' => "Password plugin: Your PHP install does not have hash() function"
583 ), true, true);
584 }
585 break;
586
587 case 'ad':
588 $crypted = rcube_charset::convert('"' . $password . '"', RCUBE_CHARSET, 'UTF-16LE');
589 break;
590
591 case 'cram-md5': // deprecated
592 require_once __DIR__ . '/../helpers/dovecot_hmacmd5.php';
593 $crypted = dovecot_hmacmd5($password);
594 $prefix = '{CRAM-MD5}';
595 break;
596
597 case 'dovecot':
598 if (!($dovecotpw = $rcmail->config->get('password_dovecotpw'))) {
599 $dovecotpw = 'dovecotpw';
600 }
601 if (!($method = $rcmail->config->get('password_dovecotpw_method'))) {
602 $method = 'CRAM-MD5';
603 }
604
605 $spec = array(0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('file', '/dev/null', 'a'));
606 $pipe = proc_open("$dovecotpw -s '$method'", $spec, $pipes);
607
608 if (!is_resource($pipe)) {
609 return false;
610 }
611
612 fwrite($pipes[0], $password . "\n", 1+strlen($password));
613 usleep(1000);
614 fwrite($pipes[0], $password . "\n", 1+strlen($password));
615
616 $crypted = trim(stream_get_contents($pipes[1]), "\n");
617
618 fclose($pipes[0]);
619 fclose($pipes[1]);
620 proc_close($pipe);
621
622 if (!preg_match('/^\{' . $method . '\}/', $crypted)) {
623 return false;
624 }
625
626 if (!$default) {
627 $prefixed = (bool) $rcmail->config->get('password_dovecotpw_with_method');
628 }
629
630 if (!$prefixed) {
631 $crypted = trim(str_replace('{' . $method . '}', '', $crypted));
632 }
633
634 $prefixed = false;
635
636 break;
637
638 case 'hash': // deprecated
639 if (!extension_loaded('hash')) {
640 rcube::raise_error(array(
641 'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
642 'message' => "Password plugin: 'hash' extension not loaded!"
643 ), true, true);
644 }
645
646 if (!($hash_algo = strtolower($rcmail->config->get('password_hash_algorithm')))) {
647 $hash_algo = 'sha1';
648 }
649
650 $crypted = hash($hash_algo, $password);
651
652 if ($rcmail->config->get('password_hash_base64')) {
653 $crypted = base64_encode(pack('H*', $crypted));
654 }
655
656 break;
657
658 case 'clear':
659 $crypted = $password;
660 }
661
662 if ($crypted === null || $crypted === false) {
663 return false;
664 }
665
666 if ($prefixed && $prefixed !== true) {
667 $prefix = $prefixed;
668 $prefixed = true;
669 }
670
671 if ($prefixed === true && $prefix) {
672 $crypted = $prefix . $crypted;
673 }
674
675 return $crypted;
676 }
677 }