Password: Change API for password checking

... for simpler implementation of strength indicator in future.

Also simplified configuration by removing password_check_strength and
adding password_minimum_score.
pull/6528/head
Aleksander Machniak 6 years ago
parent 9c4e2c9abe
commit 9babe138af

@ -421,10 +421,12 @@
password DO MATCH. Else it should return null (no error). password DO MATCH. Else it should return null (no error).
The check_strength() method, used for checking password strength has one argument: new password. The check_strength() method, used for checking password strength has one argument: new password.
This method should return null on success or error message text on failure. This method should return an array with tho elements:
- Score: integer from 1 (week) to 5 (strong)
- Reason for the score (optional)
Optionally a strength driver can contain a strength_rules() method. This has no arguments as returns Optionally a strength driver can contain a strength_rules() method. This has no arguments
a string, or array of strings explaining the password strength rules. and returns a string, or array of strings explaining the password strength rules.
4. Sudo setup 4. Sudo setup

@ -19,9 +19,9 @@ $config['password_confirm_current'] = true;
// set to blank to allow passwords of any length // set to blank to allow passwords of any length
$config['password_minimum_length'] = 0; $config['password_minimum_length'] = 0;
// Require the new password to contain a letter and punctuation character // Require the new password to have at least the specified strength score.
// Change to false to remove this check. // Note: Password strength is scored from 1 (week) to 5 (strong).
$config['password_check_strength'] = false; $config['password_minimum_score'] = 0;
// Enables logging of password changes into logs/password // Enables logging of password changes into logs/password
$config['password_log'] = false; $config['password_log'] = false;
@ -493,9 +493,3 @@ $config['password_kpasswd_cmd'] = '/usr/bin/kpasswd';
// --------------------- // ---------------------
// put token number from Modoboa server // put token number from Modoboa server
$config['password_modoboa_api_token'] = ''; $config['password_modoboa_api_token'] = '';
// Zxcvbn Strength Driver options
// ------------------------------
// minimum Zxcvbn score required for new passwords (0 = weak, 4 = very strong, 3 = default)
$config['password_zxcvbn_min_score'] = 3;

@ -38,6 +38,13 @@ class rcube_zxcvbn_password
return $rules; return $rules;
} }
/**
* Password strength check
*
* @param string $passwd Password
*
* @return array Score (1 to 5) and Reason
*/
function check_strength($passwd) function check_strength($passwd)
{ {
if (!class_exists('ZxcvbnPhp\Zxcvbn')) { if (!class_exists('ZxcvbnPhp\Zxcvbn')) {
@ -52,13 +59,7 @@ class rcube_zxcvbn_password
$rcmail = rcmail::get_instance(); $rcmail = rcmail::get_instance();
$zxcvbn = new ZxcvbnPhp\Zxcvbn(); $zxcvbn = new ZxcvbnPhp\Zxcvbn();
$strength = $zxcvbn->passwordStrength($passwd); $strength = $zxcvbn->passwordStrength($passwd);
$result = null;
if ($strength['score'] < $rcmail->config->get('password_zxcvbn_min_score', 3)) { return array($strength['score'] + 1, $strength['feedback']['warning']);
$reason = $strength['feedback']['warning'];
$result = $rcmail->gettext(array('name' => 'password.passwordweakreason', 'vars' => array('reason' => $reason)));
}
return $result;
} }
} }

@ -32,7 +32,7 @@ $messages['connecterror'] = 'Could not save new password. Connection error.';
$messages['internalerror'] = 'Could not save new password.'; $messages['internalerror'] = 'Could not save new password.';
$messages['passwordshort'] = 'Password must be at least $length characters long.'; $messages['passwordshort'] = 'Password must be at least $length characters long.';
$messages['passwordweak'] = 'Password must include at least one number and one punctuation character.'; $messages['passwordweak'] = 'Password must include at least one number and one punctuation character.';
$messages['passwordweakreason'] = 'Password too weak. $reason'; $messages['passwordtooweak'] = 'Password too weak.';
$messages['passwordnoseq'] = 'Password should not be a sequence like 123456 or QWERTY.'; $messages['passwordnoseq'] = 'Password should not be a sequence like 123456 or QWERTY.';
$messages['passwordnocommon'] = 'Password should not be a common word or name.'; $messages['passwordnocommon'] = 'Password should not be a common word or name.';
$messages['passwordforbidden'] = 'Password contains forbidden characters.'; $messages['passwordforbidden'] = 'Password contains forbidden characters.';

@ -49,18 +49,21 @@ class password extends rcube_plugin
private $newuser = false; private $newuser = false;
private $drivers = array(); private $drivers = array();
private $rc;
function init() function init()
{ {
$rcmail = rcmail::get_instance(); $this->rc = rcmail::get_instance();
$this->load_config(); $this->load_config();
// update deprecated password_require_nonalpha option removed 20181007 // update deprecated password_require_nonalpha option removed 20181007
if ($rcmail->config->get('password_check_strength') === null) { if ($this->rc->config->get('password_minimum_score') === null && $this->rc->config->get('password_require_nonalpha')) {
$rcmail->config->set('password_check_strength', $rcmail->config->get('password_require_nonalpha')); $this->rc->config->set('password_minimum_score', 2);
} }
if ($rcmail->task == 'settings') { if ($this->rc->task == 'settings') {
if (!$this->check_host_login_exceptions()) { if (!$this->check_host_login_exceptions()) {
return; return;
} }
@ -73,10 +76,10 @@ class password extends rcube_plugin
$this->register_action('plugin.password-save', array($this, 'password_save')); $this->register_action('plugin.password-save', array($this, 'password_save'));
} }
if ($rcmail->config->get('password_force_new_user')) { if ($this->rc->config->get('password_force_new_user')) {
if ($rcmail->config->get('newuserpassword') && $this->check_host_login_exceptions()) { if ($this->rc->config->get('newuserpassword') && $this->check_host_login_exceptions()) {
if (!($rcmail->task == 'settings' && strpos($rcmail->action, 'plugin.password') === 0)) { if (!($this->rc->task == 'settings' && strpos($this->rc->action, 'plugin.password') === 0)) {
$rcmail->output->command('redirect', '?_task=settings&_action=plugin.password&_first=1', false); $this->rc->output->command('redirect', '?_task=settings&_action=plugin.password&_first=1', false);
} }
} }
@ -103,48 +106,45 @@ class password extends rcube_plugin
{ {
$this->register_handler('plugin.body', array($this, 'password_form')); $this->register_handler('plugin.body', array($this, 'password_form'));
$rcmail = rcmail::get_instance(); $this->rc->output->set_pagetitle($this->gettext('changepasswd'));
$rcmail->output->set_pagetitle($this->gettext('changepasswd'));
if (rcube_utils::get_input_value('_first', rcube_utils::INPUT_GET)) { if (rcube_utils::get_input_value('_first', rcube_utils::INPUT_GET)) {
$rcmail->output->command('display_message', $this->gettext('firstloginchange'), 'notice'); $this->rc->output->command('display_message', $this->gettext('firstloginchange'), 'notice');
} }
else if (!empty($_SESSION['password_expires'])) { else if (!empty($_SESSION['password_expires'])) {
if ($_SESSION['password_expires'] == 1) { if ($_SESSION['password_expires'] == 1) {
$rcmail->output->command('display_message', $this->gettext('passwdexpired'), 'error'); $this->rc->output->command('display_message', $this->gettext('passwdexpired'), 'error');
} }
else { else {
$rcmail->output->command('display_message', $this->gettext(array( $this->rc->output->command('display_message', $this->gettext(array(
'name' => 'passwdexpirewarning', 'name' => 'passwdexpirewarning',
'vars' => array('expirationdatetime' => $_SESSION['password_expires']) 'vars' => array('expirationdatetime' => $_SESSION['password_expires'])
)), 'warning'); )), 'warning');
} }
} }
$rcmail->output->send('plugin'); $this->rc->output->send('plugin');
} }
function password_save() function password_save()
{ {
$this->register_handler('plugin.body', array($this, 'password_form')); $this->register_handler('plugin.body', array($this, 'password_form'));
$rcmail = rcmail::get_instance(); $this->rc->output->set_pagetitle($this->gettext('changepasswd'));
$rcmail->output->set_pagetitle($this->gettext('changepasswd'));
$form_disabled = $rcmail->config->get('password_disabled'); $form_disabled = $this->rc->config->get('password_disabled');
$confirm = $rcmail->config->get('password_confirm_current'); $confirm = $this->rc->config->get('password_confirm_current');
$required_length = intval($rcmail->config->get('password_minimum_length')); $required_length = intval($this->rc->config->get('password_minimum_length'));
$check_strength = $rcmail->config->get('password_check_strength'); $force_save = $this->rc->config->get('password_force_save');
$force_save = $rcmail->config->get('password_force_save');
if (($confirm && !isset($_POST['_curpasswd'])) || !isset($_POST['_newpasswd']) || !strlen($_POST['_newpasswd'])) { if (($confirm && !isset($_POST['_curpasswd'])) || !isset($_POST['_newpasswd']) || !strlen($_POST['_newpasswd'])) {
$rcmail->output->command('display_message', $this->gettext('nopassword'), 'error'); $this->rc->output->command('display_message', $this->gettext('nopassword'), 'error');
} }
else { else {
$charset = strtoupper($rcmail->config->get('password_charset', 'ISO-8859-1')); $charset = strtoupper($this->rc->config->get('password_charset', 'ISO-8859-1'));
$rc_charset = strtoupper($rcmail->output->get_charset()); $rc_charset = strtoupper($this->rc->output->get_charset());
$sespwd = $rcmail->decrypt($_SESSION['password']); $sespwd = $this->rc->decrypt($_SESSION['password']);
$curpwd = $confirm ? rcube_utils::get_input_value('_curpasswd', rcube_utils::INPUT_POST, true, $charset) : $sespwd; $curpwd = $confirm ? rcube_utils::get_input_value('_curpasswd', rcube_utils::INPUT_POST, true, $charset) : $sespwd;
$newpwd = rcube_utils::get_input_value('_newpasswd', rcube_utils::INPUT_POST, true); $newpwd = rcube_utils::get_input_value('_newpasswd', rcube_utils::INPUT_POST, true);
$conpwd = rcube_utils::get_input_value('_confpasswd', rcube_utils::INPUT_POST, true); $conpwd = rcube_utils::get_input_value('_confpasswd', rcube_utils::INPUT_POST, true);
@ -163,78 +163,76 @@ class password extends rcube_plugin
$conpwd = rcube_charset::convert($conpwd, $rc_charset, $charset); $conpwd = rcube_charset::convert($conpwd, $rc_charset, $charset);
if ($chk_pwd != $orig_pwd) { if ($chk_pwd != $orig_pwd) {
$rcmail->output->command('display_message', $this->gettext('passwordforbidden'), 'error'); $this->rc->output->command('display_message', $this->gettext('passwordforbidden'), 'error');
} }
// other passwords validity checks // other passwords validity checks
else if ($conpwd != $newpwd) { else if ($conpwd != $newpwd) {
$rcmail->output->command('display_message', $this->gettext('passwordinconsistency'), 'error'); $this->rc->output->command('display_message', $this->gettext('passwordinconsistency'), 'error');
} }
else if ($confirm && ($res = $this->_compare($sespwd, $curpwd, PASSWORD_COMPARE_CURRENT))) { else if ($confirm && ($res = $this->_compare($sespwd, $curpwd, PASSWORD_COMPARE_CURRENT))) {
$rcmail->output->command('display_message', $res, 'error'); $this->rc->output->command('display_message', $res, 'error');
} }
else if ($required_length && strlen($newpwd) < $required_length) { else if ($required_length && strlen($newpwd) < $required_length) {
$rcmail->output->command('display_message', $this->gettext( $this->rc->output->command('display_message', $this->gettext(
array('name' => 'passwordshort', 'vars' => array('length' => $required_length))), 'error'); array('name' => 'passwordshort', 'vars' => array('length' => $required_length))), 'error');
} }
else if ($check_strength && ($res = $this->_check_strength($newpwd))) { else if ($res = $this->_check_strength($newpwd)) {
$rcmail->output->command('display_message', $res, 'error'); $this->rc->output->command('display_message', $res, 'error');
} }
// password is the same as the old one, warn user, return error // password is the same as the old one, warn user, return error
else if (!$force_save && ($res = $this->_compare($sespwd, $newpwd, PASSWORD_COMPARE_NEW))) { else if (!$force_save && ($res = $this->_compare($sespwd, $newpwd, PASSWORD_COMPARE_NEW))) {
$rcmail->output->command('display_message', $res, 'error'); $this->rc->output->command('display_message', $res, 'error');
} }
// try to save the password // try to save the password
else if (!($res = $this->_save($curpwd, $newpwd))) { else if (!($res = $this->_save($curpwd, $newpwd))) {
$rcmail->output->command('display_message', $this->gettext('successfullysaved'), 'confirmation'); $this->rc->output->command('display_message', $this->gettext('successfullysaved'), 'confirmation');
// allow additional actions after password change (e.g. reset some backends) // allow additional actions after password change (e.g. reset some backends)
$plugin = $rcmail->plugins->exec_hook('password_change', array( $plugin = $this->rc->plugins->exec_hook('password_change', array(
'old_pass' => $curpwd, 'new_pass' => $newpwd)); 'old_pass' => $curpwd, 'new_pass' => $newpwd));
// Reset session password // Reset session password
$_SESSION['password'] = $rcmail->encrypt($plugin['new_pass']); $_SESSION['password'] = $this->rc->encrypt($plugin['new_pass']);
if ($rcmail->config->get('newuserpassword')) { if ($this->rc->config->get('newuserpassword')) {
$rcmail->user->save_prefs(array('newuserpassword' => false)); $this->rc->user->save_prefs(array('newuserpassword' => false));
} }
// Log password change // Log password change
if ($rcmail->config->get('password_log')) { if ($this->rc->config->get('password_log')) {
rcube::write_log('password', sprintf('Password changed for user %s (ID: %d) from %s', rcube::write_log('password', sprintf('Password changed for user %s (ID: %d) from %s',
$rcmail->get_user_name(), $rcmail->user->ID, rcube_utils::remote_ip())); $this->rc->get_user_name(), $this->rc->user->ID, rcube_utils::remote_ip()));
} }
// Remove expiration date/time // Remove expiration date/time
$rcmail->session->remove('password_expires'); $this->rc->session->remove('password_expires');
} }
else { else {
$rcmail->output->command('display_message', $res, 'error'); $this->rc->output->command('display_message', $res, 'error');
} }
} }
$rcmail->overwrite_action('plugin.password'); $this->rc->overwrite_action('plugin.password');
$rcmail->output->send('plugin'); $this->rc->output->send('plugin');
} }
function password_form() function password_form()
{ {
$rcmail = rcmail::get_instance();
// add some labels to client // add some labels to client
$rcmail->output->add_label( $this->rc->output->add_label(
'password.nopassword', 'password.nopassword',
'password.nocurpassword', 'password.nocurpassword',
'password.passwordinconsistency' 'password.passwordinconsistency'
); );
$form_disabled = $rcmail->config->get('password_disabled'); $form_disabled = $this->rc->config->get('password_disabled');
$rcmail->output->set_env('product_name', $rcmail->config->get('product_name')); $this->rc->output->set_env('product_name', $this->rc->config->get('product_name'));
$rcmail->output->set_env('password_disabled', !empty($form_disabled)); $this->rc->output->set_env('password_disabled', !empty($form_disabled));
$table = new html_table(array('cols' => 2, 'class' => 'propform')); $table = new html_table(array('cols' => 2, 'class' => 'propform'));
if ($rcmail->config->get('password_confirm_current')) { if ($this->rc->config->get('password_confirm_current')) {
// show current password selection // show current password selection
$field_id = 'curpasswd'; $field_id = 'curpasswd';
$input_curpasswd = new html_passwordfield(array( $input_curpasswd = new html_passwordfield(array(
@ -274,7 +272,7 @@ class password extends rcube_plugin
$rules = ''; $rules = '';
$required_length = intval($rcmail->config->get('password_minimum_length')); $required_length = intval($this->rc->config->get('password_minimum_length'));
if ($required_length > 0) { if ($required_length > 0) {
$rules .= html::tag('li', array('class' => 'required-length'), $this->gettext(array( $rules .= html::tag('li', array('class' => 'required-length'), $this->gettext(array(
'name' => 'passwordshort', 'name' => 'passwordshort',
@ -282,7 +280,7 @@ class password extends rcube_plugin
))); )));
} }
if ($rcmail->config->get('password_check_strength') && ($msgs = $this->_strength_rules())) { if ($msgs = $this->_strength_rules()) {
foreach ($msgs as $msg) { foreach ($msgs as $msg) {
$rules .= html::tag('li', array('class' => 'strength-rule'), $msg); $rules .= html::tag('li', array('class' => 'strength-rule'), $msg);
} }
@ -298,18 +296,18 @@ class password extends rcube_plugin
$disabled_msg = html::div(array('class' => 'boxwarning', 'id' => 'password-notice'), $disabled_msg); $disabled_msg = html::div(array('class' => 'boxwarning', 'id' => 'password-notice'), $disabled_msg);
} }
$submit_button = $rcmail->output->button(array( $submit_button = $this->rc->output->button(array(
'command' => 'plugin.password-save', 'command' => 'plugin.password-save',
'class' => 'button mainaction submit', 'class' => 'button mainaction submit',
'label' => 'save', 'label' => 'save',
)); ));
$form_buttons = html::p(array('class' => 'formbuttons footerleft'), $submit_button); $form_buttons = html::p(array('class' => 'formbuttons footerleft'), $submit_button);
$rcmail->output->add_gui_object('passform', 'password-form'); $this->rc->output->add_gui_object('passform', 'password-form');
$this->include_script('password.js'); $this->include_script('password.js');
$form = $rcmail->output->form_tag(array( $form = $this->rc->output->form_tag(array(
'id' => 'password-form', 'id' => 'password-form',
'name' => 'password-form', 'name' => 'password-form',
'method' => 'post', 'method' => 'post',
@ -350,15 +348,10 @@ class password extends rcube_plugin
private function _strength_rules() private function _strength_rules()
{ {
$driver = $this->_load_driver('strength'); if (($driver = $this->_load_driver('strength')) && method_exists($driver, 'strength_rules')) {
if (!$driver) {
$result = null;
}
else if (method_exists($driver, 'strength_rules')) {
$result = $driver->strength_rules(); $result = $driver->strength_rules();
} }
else { else if ($this->rc->config->get('password_minimum_score') > 1) {
$result = $this->gettext('passwordweak'); $result = $this->gettext('passwordweak');
} }
@ -371,17 +364,22 @@ class password extends rcube_plugin
private function _check_strength($passwd) private function _check_strength($passwd)
{ {
$driver = $this->_load_driver('strength'); $min_score = $this->rc->config->get('password_minimum_score');
if (!$driver) { if (!$min_score) {
return $this->gettext('internalerror'); return;
} }
if (method_exists($driver, 'check_strength')) { if (($driver = $this->_load_driver('strength')) && method_exists($driver, 'check_strength')) {
return $driver->check_strength($passwd); list($score, $reason) = $driver->check_strength($passwd);
}
else {
$score = (!preg_match("/[0-9]/", $passwd) || !preg_match("/[^A-Za-z0-9]/", $passwd)) ? 1 : 5;
} }
return (!preg_match("/[0-9]/", $passwd) || !preg_match("/[^A-Za-z0-9]/", $passwd)) ? $this->gettext('passwordweak') : null; if ($score < $min_score) {
return $this->gettext('passwordtooweak') . (!empty($reason) ? " $reason" : '');
}
} }
private function _save($curpass, $passwd) private function _save($curpass, $passwd)
@ -427,8 +425,8 @@ class password extends rcube_plugin
private function _load_driver($type = 'password') private function _load_driver($type = 'password')
{ {
if (!($type && $driver = rcmail::get_instance()->config->get('password_' . $type . '_driver'))) { if (!($type && $driver = $this->rc->config->get('password_' . $type . '_driver'))) {
$driver = rcmail::get_instance()->config->get('password_driver', 'sql'); $driver = $this->rc->config->get('password_driver', 'sql');
} }
if (!$this->drivers[$type]) { if (!$this->drivers[$type]) {
@ -472,8 +470,7 @@ class password extends rcube_plugin
function login_after($args) function login_after($args)
{ {
if ($this->newuser && $this->check_host_login_exceptions()) { if ($this->newuser && $this->check_host_login_exceptions()) {
$rcmail = rcmail::get_instance(); $this->rc->user->save_prefs(array('newuserpassword' => true));
$rcmail->user->save_prefs(array('newuserpassword' => true));
$args['_task'] = 'settings'; $args['_task'] = 'settings';
$args['_action'] = 'plugin.password'; $args['_action'] = 'plugin.password';
@ -486,16 +483,14 @@ class password extends rcube_plugin
// Check if host and login is allowed to change the password, false = not allowed, true = not allowed // Check if host and login is allowed to change the password, false = not allowed, true = not allowed
private function check_host_login_exceptions() private function check_host_login_exceptions()
{ {
$rcmail = rcmail::get_instance();
// Host exceptions // Host exceptions
$hosts = $rcmail->config->get('password_hosts'); $hosts = $this->rc->config->get('password_hosts');
if (!empty($hosts) && !in_array($_SESSION['storage_host'], (array) $hosts)) { if (!empty($hosts) && !in_array($_SESSION['storage_host'], (array) $hosts)) {
return false; return false;
} }
// Login exceptions // Login exceptions
if ($exceptions = $rcmail->config->get('password_login_exceptions')) { if ($exceptions = $this->rc->config->get('password_login_exceptions')) {
$exceptions = array_map('trim', (array) $exceptions); $exceptions = array_map('trim', (array) $exceptions);
$exceptions = array_filter($exceptions); $exceptions = array_filter($exceptions);
$username = $_SESSION['username']; $username = $_SESSION['username'];
@ -524,9 +519,9 @@ class password extends rcube_plugin
*/ */
static function hash_password($password, $method = '', $prefixed = true) static function hash_password($password, $method = '', $prefixed = true)
{ {
$method = strtolower($method); $method = strtolower($method);
$rcmail = rcmail::get_instance(); $rcmail = rcmail::get_instance();
$prefix = ''; $prefix = '';
$crypted = ''; $crypted = '';
if (empty($method) || $method == 'default') { if (empty($method) || $method == 'default') {

Loading…
Cancel
Save