From 00946f1f570311ff0d9e53cfaf383bcc6ed6b1d7 Mon Sep 17 00:00:00 2001 From: PhilW Date: Sun, 7 Oct 2018 07:50:42 +0100 Subject: [PATCH] give password plugin drivers more power Allow password drivers more control over the entire password changeing processes not just the save action. Allow them to perform old/new password comparisons and also password strength checking. *) allow password drivers override default password comparisons (eg new is not same as current) #6473 *) allow password drivers override default strength checks (eg allow for 'not the same as last x passwords') *) allow separate password saving and strength drivers for use of strength checking services eg HaveIBeenPwned.com #5040 *) allow drivers to define password strength rules displayed to the user *) rename password_require_nonalpha config option to password_check_strength to reflect new strength checking possibilities (added backwards compatibility) --- plugins/password/README | 144 +++++++++++++++---------- plugins/password/config.inc.php.dist | 13 ++- plugins/password/password.php | 155 +++++++++++++++++++++------ 3 files changed, 219 insertions(+), 93 deletions(-) diff --git a/plugins/password/README b/plugins/password/README index 769f42959..2b2fc26ee 100644 --- a/plugins/password/README +++ b/plugins/password/README @@ -61,12 +61,15 @@ 2. Drivers ---------- + + 2.1. Password Change Drivers + ---------------------------- + Password plugin supports many password change mechanisms which are handled by included drivers. Just pass driver name in 'password_driver' option. - - 2.1. Database (sql) - ------------------- + 2.1.1. Database (sql) + --------------------- You can specify which database to connect by 'password_db_dsn' option and what SQL query to execute by 'password_query'. See config.inc.php.dist file for @@ -119,8 +122,8 @@ UPDATE users SET password=MD5(%p) WHERE username=%u AND password=MD5(%o) LIMIT 1 - 2.2. Cyrus/SASL (sasl) - ---------------------- + 2.1.2. Cyrus/SASL (sasl) + ------------------------ Cyrus SASL database authentication allows your Cyrus+Roundcube installation to host mail users without requiring a Unix Shell account! @@ -170,31 +173,31 @@ This could save you some headaches if you are the paranoid type. - 2.3. Poppassd/Courierpassd (poppassd) - ------------------------------------- + 2.1.3. Poppassd/Courierpassd (poppassd) + --------------------------------------- You can specify which host to connect to via 'password_pop_host' and what port via 'password_pop_port'. See config.inc.php.dist file for more info. - 2.4. LDAP (ldap) - ---------------- + 2.1.4. LDAP (ldap) + ------------------ See config.inc.php.dist file. Requires PEAR::Net_LDAP2 package. - 2.5. DirectAdmin Control Panel (directadmin) - -------------------------------------------- + 2.1.5. DirectAdmin Control Panel (directadmin) + ---------------------------------------------- - You can specify which host to connect to via 'password_directadmin_host' (don't + You can specify which host to connect to via 'password_directadmin_host' (don't forget to use tcp:// or ssl://) and what port via 'password_direactadmin_port'. - The password enforcement with plenty customization can be done directly by + The password enforcement with plenty customization can be done directly by DirectAdmin, please see http://www.directadmin.com/features.php?id=910 See config.inc.php.dist file for more info. - 2.6. cPanel - ----------- + 2.1.6. cPanel + ------------- cPanel offers various APIs. The `cpanel` driver is configured with and admin account. It can change user's passwords without access to the current password. @@ -204,8 +207,8 @@ an admin account. See 2.6.2. - 2.6.1. cPanel WHM (cpanel) - -------------------------- + 2.1.6.1. cPanel WHM (cpanel) + ---------------------------- Install cPanel XMLAPI Client Class into Roundcube program/lib directory or any other place in PHP include path. You can get the class from @@ -215,8 +218,8 @@ See config.inc.php.dist file for more info. - 2.6.2. cPanel Webmail (cpanel_webmail) - -------------------------------------- + 2.1.6.2. cPanel Webmail (cpanel_webmail) + ---------------------------------------- Specify the host to connect to via 'password_webmail_cpanel_host'. This driver comes with a minimal UAPI implementation and does not use the external xmlapi @@ -225,48 +228,48 @@ See config.inc.php.dist file for more info. - 2.7. XIMSS/Communigate (ximms) - ------------------------------ + 2.1.7. XIMSS/Communigate (ximms) + -------------------------------- - You can specify which host and port to connect to via 'password_ximss_host' + You can specify which host and port to connect to via 'password_ximss_host' and 'password_ximss_port'. See config.inc.php.dist file for more info. - 2.8. Virtualmin (virtualmin) - ---------------------------- + 2.1.8. Virtualmin (virtualmin) + ------------------------------ As in sasl driver this one allows to change password using shell utility called "virtualmin". See helpers/chgvirtualminpasswd.c for installation instructions. Requires virtualmin >= 4.09. - 2.9. hMailServer (hmail) - ------------------------ + 2.1.9. hMailServer (hmail) + -------------------------- Requires PHP COM (Windows only). For access to hMail server on remote host you'll need to define 'hmailserver_remote_dcom' and 'hmailserver_server'. See config.inc.php.dist file for more info. - 2.10. PAM (pam) - --------------- + 2.1.10. PAM (pam) + ----------------- This driver is for changing passwords of shell users authenticated with PAM. Requires PECL's PAM exitension to be installed (http://pecl.php.net/package/PAM). - 2.11. Chpasswd (chpasswd) - ------------------------- + 2.1.11. Chpasswd (chpasswd) + --------------------------- - Driver that adds functionality to change the systems user password via + Driver that adds functionality to change the systems user password via the 'chpasswd' command. See config.inc.php.dist file. Attached wrapper script (helpers/chpass-wrapper.py) restricts password changes to uids >= 1000 and can deny requests based on a blacklist. - 2.12. LDAP - no PEAR (ldap_simple) - ----------------------------------- + 2.1.12. LDAP - no PEAR (ldap_simple) + ------------------------------------- It's rewritten ldap driver that doesn't require the Net_LDAP2 PEAR extension. It uses directly PHP's ldap module functions instead (as Roundcube does). @@ -280,29 +283,29 @@ why the 'force replace' is always used). - 2.13. XMail (xmail) - ----------------------------------- + 2.1.13. XMail (xmail) + ---------------------- Driver for XMail (www.xmailserver.org). See config.inc.php.dist file for configuration description. - 2.14. Pw (pw_usermod) - ----------------------------------- + 2.1.14. Pw (pw_usermod) + ------------------------ Driver to change the systems user password via the 'pw usermod' command. See config.inc.php.dist file for configuration description. - 2.15. domainFACTORY (domainfactory) - ----------------------------------- + 2.1.15. domainFACTORY (domainfactory) + ------------------------------------- Driver for the hosting provider domainFACTORY (www.df.eu). No configuration options. - 2.16. DBMail (dbmail) - ----------------------------------- + 2.1.16. DBMail (dbmail) + ------------------------ Driver that adds functionality to change the users DBMail password. It only works with dbmail-users on the same host where Roundcube runs @@ -313,22 +316,22 @@ Note: DBMail users can also use sql driver. - 2.17. Expect (expect) - ----------------------------------- + 2.1.17. Expect (expect) + ------------------------ Driver to change user password via the 'expect' command. See config.inc.php.dist file for configuration description. - 2.18. Samba (smb) - ----------------------------------- + 2.1.18. Samba (smb) + -------------------- Driver to change Samba user password via the 'smbpasswd' command. See config.inc.php.dist file for configuration description. - 2.19. Vpopmail daemon (vpopmaild) - ----------------------------------- + 2.1.19. Vpopmail daemon (vpopmaild) + ------------------------------------- Driver for the daemon of vpopmail. Vpopmail is used with qmail to enable virtual users that are saved in a database and not in /etc/passwd. @@ -337,12 +340,12 @@ Set $config['password_vpopmaild_port'] to the port of vpopmaild. - Set $config['password_vpopmaild_timeout'] to the timeout used for the TCP + Set $config['password_vpopmaild_timeout'] to the timeout used for the TCP connection to vpopmaild (You may want to set it higher on busy servers). - 2.20. Plesk (Plesk RPC-API) - --------------------------- + 2.1.20. Plesk (Plesk RPC-API) + ----------------------------- Driver for changing Passwords via Plesk RPC-API. This Driver also works with Parallels Plesk Automation (PPA). @@ -356,30 +359,59 @@ Set the RPC-Path in $config['password_plesk_rpc_path']. Normally this is: enterprise/control/agent.php. - 2.21. Kpasswd - ----------------------------------- + 2.1.21. Kpasswd + --------------- Driver to change the password in Kerberos environments via the 'kpasswd' command. See config.inc.php.dist file for configuration description. - 2.22. Modoboa - ----------------------------------- + 2.1.22. Modoboa + --------------- Driver to change the password in Modoboa servers. See config.inc.php.dist file for configuration description. + 2.2. Password Strength Drivers + ------------------------------ + + Password plugin supports many password strength checking mechanisms which are + handled by included drivers. Just pass driver name in 'password_strength_driver' option. + + 3. Driver API ------------- - Driver file (.php) must define rcube__password class - with public save() method that has two arguments. First - current password, second - new password. + Driver file (.php) must define rcube__password class. Drivers should + provide one or both of a public save() or check_strength() method. + + All password changing drivers (used in config `password_driver` - the password driver) must have + a save() method. The same driver can also contain a check_strength() method or a separate driver + containing this method can be used in `password_strength_driver` (the strength driver). To enable + strength checks ensure `password_check_strength` is set to true. + + The save() method, used for changing the password has two arguments: + First - current password, second - new password. This method should return PASSWORD_SUCCESS on success or any of PASSWORD_CONNECT_ERROR, PASSWORD_CRYPT_ERROR, PASSWORD_ERROR when driver was unable to change password. Extended result (as a hash-array with 'message' and 'code' items) can be returned too. See existing drivers in drivers/ directory for examples. + Optionally a password driver can contain a compare() method which has three arguments: + First - current password, second - test password, third - compare type. + Compare type: PASSWORD_COMPARE_CURRENT - when comparing the test password with current password. + PASSWORD_COMPARE_NEW - when comparing the current password with the test password. + For PASSWORD_COMPARE_CURRENT it should return error text if user entered and real current password + DO NOT MATCH. For PASSWORD_COMPARE_NEW it should return error text if user entered and real current + password DO MATCH. Else it should return null (no error). + + 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. + + Optionally a strength driver can contain a strength_rules() method. This has no arguments as returns + a string, or array of strings explaining the password strength rules. + 4. Sudo setup ------------- diff --git a/plugins/password/config.inc.php.dist b/plugins/password/config.inc.php.dist index e6e1f736e..b106bb0a7 100644 --- a/plugins/password/config.inc.php.dist +++ b/plugins/password/config.inc.php.dist @@ -6,6 +6,11 @@ // See README file for list of supported driver names. $config['password_driver'] = 'sql'; +// A driver to use for checking password strength. Default: null. +// Set password_check_strength to true to enable +// See README file for list of supported driver names. +$config['password_strength_driver'] = null; + // Determine whether current password is required to change password. // Default: false. $config['password_confirm_current'] = true; @@ -16,7 +21,7 @@ $config['password_minimum_length'] = 0; // Require the new password to contain a letter and punctuation character // Change to false to remove this check. -$config['password_require_nonalpha'] = false; +$config['password_check_strength'] = false; // Enables logging of password changes into logs/password $config['password_log'] = false; @@ -169,7 +174,7 @@ $config['password_saslpasswd_args'] = ''; // LDAP and LDAP_SIMPLE Driver options // ----------------------------------- -// LDAP server name to connect to. +// LDAP server name to connect to. // You can provide one or several hosts in an array in which case the hosts are tried from left to right. // Exemple: array('ldap1.exemple.com', 'ldap2.exemple.com'); // Default: 'localhost' @@ -286,7 +291,7 @@ $config['password_ldap_lchattr'] = ''; // LDAP Samba password attribute, e.g. sambaNTPassword // Name of the LDAP's Samba attribute used for storing user password $config['password_ldap_samba_pwattr'] = ''; - + // LDAP Samba Password Last Change Date attribute, e.g. sambaPwdLastSet // Some places use an attribute to store the date of the last password change // The date is meassured in "seconds since epoch" (an integer value) @@ -460,7 +465,7 @@ $config['password_gearman_host'] = 'localhost'; // Plesk/PPA Driver options // -------------------- -// You need to allow RCP for IP of roundcube-server in Plesk/PPA Panel +// You need to allow RCP for IP of roundcube-server in Plesk/PPA Panel // Plesk RCP Host $config['password_plesk_host'] = '10.0.0.5'; diff --git a/plugins/password/password.php b/plugins/password/password.php index 5b9b384b8..697a1f161 100644 --- a/plugins/password/password.php +++ b/plugins/password/password.php @@ -26,6 +26,8 @@ define('PASSWORD_ERROR', 2); define('PASSWORD_CONNECT_ERROR', 3); define('PASSWORD_IN_HISTORY', 4); define('PASSWORD_CONSTRAINT_VIOLATION', 5); +define('PASSWORD_COMPARE_CURRENT', 6); +define('PASSWORD_COMPARE_NEW', 7); define('PASSWORD_SUCCESS', 0); /** @@ -46,12 +48,17 @@ class password extends rcube_plugin public $noajax = true; private $newuser = false; + private $drivers = array(); function init() { $rcmail = rcmail::get_instance(); $this->load_config(); + // update deprecated password_require_nonalpha option removed 20181007 + if ($rcmail->config->get('password_check_strength') === null) { + $rcmail->config->set('password_check_strength', $rcmail->config->get('password_require_nonalpha')); + } if ($rcmail->task == 'settings') { if (!$this->check_host_login_exceptions()) { @@ -127,7 +134,8 @@ class password extends rcube_plugin $form_disabled = $rcmail->config->get('password_disabled'); $confirm = $rcmail->config->get('password_confirm_current'); $required_length = intval($rcmail->config->get('password_minimum_length')); - $check_strength = $rcmail->config->get('password_require_nonalpha'); + $check_strength = $rcmail->config->get('password_check_strength'); + $force_save = $rcmail->config->get('password_force_save'); if (($confirm && !isset($_POST['_curpasswd'])) || !isset($_POST['_newpasswd']) || !strlen($_POST['_newpasswd'])) { $rcmail->output->command('display_message', $this->gettext('nopassword'), 'error'); @@ -161,19 +169,19 @@ class password extends rcube_plugin else if ($conpwd != $newpwd) { $rcmail->output->command('display_message', $this->gettext('passwordinconsistency'), 'error'); } - else if ($confirm && $sespwd != $curpwd) { - $rcmail->output->command('display_message', $this->gettext('passwordincorrect'), 'error'); + else if ($confirm && ($res = $this->_compare($sespwd, $curpwd, PASSWORD_COMPARE_CURRENT))) { + $rcmail->output->command('display_message', $res, 'error'); } else if ($required_length && strlen($newpwd) < $required_length) { $rcmail->output->command('display_message', $this->gettext( array('name' => 'passwordshort', 'vars' => array('length' => $required_length))), 'error'); } - else if ($check_strength && (!preg_match("/[0-9]/", $newpwd) || !preg_match("/[^A-Za-z0-9]/", $newpwd))) { - $rcmail->output->command('display_message', $this->gettext('passwordweak'), 'error'); + else if ($check_strength && ($res = $this->_check_strength($newpwd))) { + $rcmail->output->command('display_message', $res, 'error'); } // password is the same as the old one, warn user, return error - else if ($sespwd == $newpwd && !$rcmail->config->get('password_force_save')) { - $rcmail->output->command('display_message', $this->gettext('samepasswd'), 'error'); + else if (!$force_save && ($res = $this->_compare($sespwd, $newpwd, PASSWORD_COMPARE_NEW))) { + $rcmail->output->command('display_message', $res, 'error'); } // try to save the password else if (!($res = $this->_save($curpwd, $newpwd))) { @@ -268,14 +276,16 @@ class password extends rcube_plugin $required_length = intval($rcmail->config->get('password_minimum_length')); if ($required_length > 0) { - $rules .= html::tag('li', array('id' => 'required-length'), $this->gettext(array( + $rules .= html::tag('li', array('class' => 'required-length'), $this->gettext(array( 'name' => 'passwordshort', 'vars' => array('length' => $required_length) ))); } - if ($rcmail->config->get('password_require_nonalpha')) { - $rules .= html::tag('li', array('id' => 'require-nonalpha'), $this->gettext('passwordweak')); + if ($rcmail->config->get('password_check_strength') && ($msgs = $this->_strength_rules())) { + foreach ($msgs as $msg) { + $rules .= html::tag('li', array('class' => 'strength-rule'), $msg); + } } if (!empty($rules)) { @@ -312,37 +322,76 @@ class password extends rcube_plugin . $form_buttons); } - private function _save($curpass, $passwd) + private function _compare($curpwd, $newpwd, $type) { - $config = rcmail::get_instance()->config; - $driver = $config->get('password_driver', 'sql'); - $class = "rcube_{$driver}_password"; - $file = $this->home . "/drivers/$driver.php"; - - if (!file_exists($file)) { - rcube::raise_error(array( - 'code' => 600, - 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Password plugin: Unable to open driver file ($file)" - ), true, false); + $result = null; + + if (!($driver = $this->_load_driver())) return $this->gettext('internalerror'); + + if (method_exists($driver, 'compare')) { + $result = $driver->compare($curpwd, $newpwd, $type); + } + else { + switch ($type) { + case PASSWORD_COMPARE_CURRENT: + $result = $curpwd != $newpwd ? $this->gettext('passwordincorrect') : null; + break; + case PASSWORD_COMPARE_NEW: + $result = $curpwd == $newpwd ? $this->gettext('samepasswd') : null; + break; + default: + $result = $this->gettext('internalerror'); + } } - include_once $file; + return $result; + } + + private function _strength_rules() + { + $result = null; - if (!class_exists($class, false) || !method_exists($class, 'save')) { - rcube::raise_error(array( - 'code' => 600, - 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Password plugin: Broken driver $driver" - ), true, false); + if (!($driver = $this->_load_driver('strength'))) return $this->gettext('internalerror'); + + if (method_exists($driver, 'strength_rules')) { + $result = $driver->strength_rules(); + } + else { + $result = $this->gettext('passwordweak'); } - $object = new $class; - $result = $object->save($curpass, $passwd, self::username()); + if (!is_array($result)) { + $result = array($result); + } + + return $result; + } + + private function _check_strength($passwd) + { + $result = null; + + if (!($driver = $this->_load_driver('strength'))) + return $this->gettext('internalerror'); + + if (method_exists($driver, 'check_strength')) { + $result = $driver->check_strength($passwd); + } + else { + $result = (!preg_match("/[0-9]/", $passwd) || !preg_match("/[^A-Za-z0-9]/", $passwd)) ? $this->gettext('passwordweak') : null; + } + + return $result; + } + + private function _save($curpass, $passwd) + { + if (!($driver = $this->_load_driver())) + return $this->gettext('internalerror'); + + $result = $driver->save($curpass, $passwd, self::username()); $message = ''; if (is_array($result)) { @@ -377,6 +426,46 @@ class password extends rcube_plugin return $reason; } + private function _load_driver($type = 'password') + { + if (!($type && $driver = rcmail::get_instance()->config->get('password_' . $type . '_driver'))) { + $driver = rcmail::get_instance()->config->get('password_driver', 'sql'); + } + + if (!$this->drivers[$type]) { + $class = "rcube_{$driver}_password"; + $file = $this->home . "/drivers/$driver.php"; + + if (!file_exists($file)) { + rcube::raise_error(array( + 'code' => 600, + 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Password plugin: Unable to open driver file ($file)" + ), true, false); + return false; + } + + include_once $file; + + if (!class_exists($class, false) || (!method_exists($class, 'save') && !method_exists($class, 'check_strength'))) { + rcube::raise_error(array( + 'code' => 600, + 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Password plugin: Broken driver $driver" + ), true, false); + return false; + } + + $this->drivers[$type] = new $class; + return $this->drivers[$type]; + } + else { + return $this->drivers[$type]; + } + } + function user_create($args) { $this->newuser = true;