diff --git a/lam/docs/manual-sources/images/configGeneral2.png b/lam/docs/manual-sources/images/configGeneral2.png index 1de04938..c79ac2f5 100644 Binary files a/lam/docs/manual-sources/images/configGeneral2.png and b/lam/docs/manual-sources/images/configGeneral2.png differ diff --git a/lam/help/help.inc b/lam/help/help.inc index 5eb6d127..83cb7ab0 100644 --- a/lam/help/help.inc +++ b/lam/help/help.inc @@ -159,6 +159,12 @@ $helpArray = array ( "Text" => _('Defines if the PHP error reporting setting from php.ini is used or the setting preferred by LAM ("E_ALL & ~E_NOTICE"). If you do not develop LAM modules please use the default. This will prevent displaying messages that are useful only for developers.')), "245" => array ("Headline" => _('Encrypt session'), "Text" => _('Encrypts sensitive data like passwords in your session. This requires the PHP MCrypt extension.')), + "246" => array ("Headline" => _('Number of rules that must match'), + "Text" => _('Specifies the number of above password rules that must be fulfilled.')), + "247" => array ("Headline" => _('Password must not contain user name'), + "Text" => _('Specifies if the password must not contain the user name.')), + "248" => array ("Headline" => _('Password must not contain part of user/first/last name'), + "Text" => _('Specifies if the password must not contain 3 or more characters of the user/first/last name.')), "250" => array ("Headline" => _("Filter"), "Text" => _("Here you can input simple filter expressions (e.g. 'value' or 'v*'). The filter is case-sensitive.")), "260" => array ("Headline" => _("Additional LDAP filter"), diff --git a/lam/lib/config.inc b/lam/lib/config.inc index 39db0054..ff57829e 100644 --- a/lam/lib/config.inc +++ b/lam/lib/config.inc @@ -1506,6 +1506,15 @@ class LAMCfgMain { /** minimum character classes (upper, lower, numeric, symbols) */ public $passwordMinClasses = 0; + /** number of password rules that must match (-1 = all) */ + public $checkedRulesCount = -1; + + /** password may contain the user name */ + public $passwordMustNotContainUser = 'false'; + + /** password may contain more than 2 characters of user/first/last name */ + public $passwordMustNotContain3Chars = 'false'; + /** path to config file */ private $conffile; @@ -1525,8 +1534,9 @@ class LAMCfgMain { private $settings = array("password", "default", "sessionTimeout", "logLevel", "logDestination", "allowedHosts", "passwordMinLength", "passwordMinUpper", "passwordMinLower", "passwordMinNumeric", - "passwordMinClasses", "passwordMinSymbol", "mailEOL", 'errorReporting', - 'encryptSession', 'allowedHostsSelfService'); + "passwordMinClasses", "passwordMinSymbol", 'checkedRulesCount', + 'passwordMustNotContainUser', 'passwordMustNotContain3Chars', + "mailEOL", 'errorReporting', 'encryptSession', 'allowedHostsSelfService'); /** * Loads preferences from config file @@ -1615,6 +1625,9 @@ class LAMCfgMain { if (!in_array("passwordMinNumeric", $saved)) array_push($file_array, "\n\n# Password: minimum numeric characters\n" . "passwordMinNumeric: " . $this->passwordMinNumeric); if (!in_array("passwordMinSymbol", $saved)) array_push($file_array, "\n\n# Password: minimum symbolic characters\n" . "passwordMinSymbol: " . $this->passwordMinSymbol); if (!in_array("passwordMinClasses", $saved)) array_push($file_array, "\n\n# Password: minimum character classes (0-4)\n" . "passwordMinClasses: " . $this->passwordMinClasses); + if (!in_array("checkedRulesCount", $saved)) array_push($file_array, "\n\n# Password: checked rules\n" . "checkedRulesCount: " . $this->checkedRulesCount); + if (!in_array("passwordMustNotContain3Chars", $saved)) array_push($file_array, "\n\n# Password: must not contain part of user name\n" . "passwordMustNotContain3Chars: " . $this->passwordMustNotContain3Chars); + if (!in_array("passwordMustNotContainUser", $saved)) array_push($file_array, "\n\n# Password: must not contain user name\n" . "passwordMustNotContainUser: " . $this->passwordMustNotContainUser); if (!in_array("mailEOL", $saved)) array_push($file_array, "\n\n# Email format (default/unix)\n" . "mailEOL: " . $this->mailEOL); if (!in_array("errorReporting", $saved)) array_push($file_array, "\n\n# PHP error reporting (default/system)\n" . "errorReporting: " . $this->errorReporting); $file = @fopen($this->conffile, "w"); diff --git a/lam/lib/security.inc b/lam/lib/security.inc index 904db586..57afbc64 100644 --- a/lam/lib/security.inc +++ b/lam/lib/security.inc @@ -315,10 +315,12 @@ function checkIfDeleteEntriesIsAllowed($scope) { /** * Checks if the password fulfills the password policies. * - * @param string $password password + * @param String $password password + * @param String $userName user name + * @param array $otherUserAttrs user's first/last name * @return mixed true if ok, string with error message if not valid */ -function checkPasswordStrength($password) { +function checkPasswordStrength($password, $userName, $otherUserAttrs) { if ($password == null) { $password = ""; } @@ -347,22 +349,48 @@ function checkPasswordStrength($password) { $symbols++; } } + $rulesMatched = 0; + $rulesFailed = 0; // check lower case - if ($lower < $cfg->passwordMinLower) { + if (($cfg->checkedRulesCount == -1) && ($lower < $cfg->passwordMinLower)) { return sprintf(_('The password is too weak. You have to enter at least %s lower case characters.'), $cfg->passwordMinLower); } + if ($lower < $cfg->passwordMinLower) { + $rulesFailed++; + } + else { + $rulesMatched++; + } // check upper case - if ($upper < $cfg->passwordMinUpper) { + if (($cfg->checkedRulesCount == -1) && ($upper < $cfg->passwordMinUpper)) { return sprintf(_('The password is too weak. You have to enter at least %s upper case characters.'), $cfg->passwordMinUpper); } + if ($upper < $cfg->passwordMinUpper) { + $rulesFailed++; + } + else { + $rulesMatched++; + } // check numeric - if ($numeric < $cfg->passwordMinNumeric) { + if (($cfg->checkedRulesCount == -1) && ($numeric < $cfg->passwordMinNumeric)) { return sprintf(_('The password is too weak. You have to enter at least %s numeric characters.'), $cfg->passwordMinNumeric); } + if ($numeric < $cfg->passwordMinNumeric) { + $rulesFailed++; + } + else { + $rulesMatched++; + } // check symbols - if ($symbols < $cfg->passwordMinSymbol) { + if (($cfg->checkedRulesCount == -1) && ($symbols < $cfg->passwordMinSymbol)) { return sprintf(_('The password is too weak. You have to enter at least %s symbolic characters.'), $cfg->passwordMinSymbol); } + if ($symbols < $cfg->passwordMinSymbol) { + $rulesFailed++; + } + else { + $rulesMatched++; + } // check classes $classes = 0; if ($lower > 0) { @@ -377,9 +405,51 @@ function checkPasswordStrength($password) { if ($symbols > 0) { $classes++; } - if ($classes < $cfg->passwordMinClasses) { + if (($cfg->checkedRulesCount == -1) && ($classes < $cfg->passwordMinClasses)) { return sprintf(_('The password is too weak. You have to enter at least %s different character classes (upper/lower case, numbers and symbols).'), $cfg->passwordMinClasses); } + if ($classes < $cfg->passwordMinClasses) { + $rulesFailed++; + } + else { + $rulesMatched++; + } + // check rules count + if (($cfg->checkedRulesCount != -1) && ($rulesMatched < $cfg->checkedRulesCount)) { + return sprintf(_('The password is too weak. It needs to match at least %s password complexity rules.'), $cfg->checkedRulesCount); + } + // check user name + if (($cfg->passwordMustNotContainUser == 'true') && !empty($userName)) { + $pwdLow = strtolower($password); + $userLow = strtolower($userName); + if (strpos($pwdLow, $userLow) !== false) { + return _('The password is too weak. You may not use the user name as part of the password.'); + } + } + // check part of user name and additional attributes + if (($cfg->passwordMustNotContain3Chars == 'true') && (!empty($userName) || !empty($otherUserAttrs))) { + $pwdLow = strtolower($password); + // check if contains part of user name + if (!empty($userName) && (strlen($userName) > 2)) { + $userLow = strtolower($userName); + for ($i = 0; $i < strlen($userLow) - 3; $i++) { + $part = substr($userLow, 0, 3); + if (strpos($pwdLow, $part) !== false) { + return _('The password is too weak. You may not use parts of the user name for the password.'); + } + } + } + // check other attributes + foreach ($otherUserAttrs as $other) { + $low = strtolower($other); + for ($i = 0; $i < strlen($low) - 3; $i++) { + $part = substr($low, 0, 3); + if (strpos($pwdLow, $part) !== false) { + return _('The password is too weak. You may not use parts of user attributes for the password.'); + } + } + } + } return true; } diff --git a/lam/templates/config/mainmanage.php b/lam/templates/config/mainmanage.php index a88dff6a..93a29c0e 100644 --- a/lam/templates/config/mainmanage.php +++ b/lam/templates/config/mainmanage.php @@ -153,6 +153,9 @@ if (isset($_POST['submitFormData'])) { $cfg->passwordMinNumeric = $_POST['passwordMinNumeric']; $cfg->passwordMinSymbol = $_POST['passwordMinSymbol']; $cfg->passwordMinClasses = $_POST['passwordMinClasses']; + $cfg->checkedRulesCount = $_POST['passwordRulesCount']; + $cfg->passwordMustNotContain3Chars = isset($_POST['passwordMustNotContain3Chars']) && ($_POST['passwordMustNotContain3Chars'] == 'on') ? 'true' : 'false'; + $cfg->passwordMustNotContainUser = isset($_POST['passwordMustNotContainUser']) && ($_POST['passwordMustNotContainUser'] == 'on') ? 'true' : 'false'; if (isset($_POST['sslCaCertUpload'])) { if (!isset($_FILES['sslCaCert']) || ($_FILES['sslCaCert']['size'] == 0)) { $errors[] = _('No file selected.'); @@ -381,11 +384,21 @@ $policyTable = new htmlTable(); $options20 = array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20); $options4 = array(0, 1, 2, 3, 4); $policyTable->addElement(new htmlTableExtendedSelect('passwordMinLength', $options20, array($cfg->passwordMinLength), _('Minimum password length'), '242'), true); +$policyTable->addVerticalSpace('5px'); $policyTable->addElement(new htmlTableExtendedSelect('passwordMinLower', $options20, array($cfg->passwordMinLower), _('Minimum lowercase characters'), '242'), true); $policyTable->addElement(new htmlTableExtendedSelect('passwordMinUpper', $options20, array($cfg->passwordMinUpper), _('Minimum uppercase characters'), '242'), true); $policyTable->addElement(new htmlTableExtendedSelect('passwordMinNumeric', $options20, array($cfg->passwordMinNumeric), _('Minimum numeric characters'), '242'), true); $policyTable->addElement(new htmlTableExtendedSelect('passwordMinSymbol', $options20, array($cfg->passwordMinSymbol), _('Minimum symbolic characters'), '242'), true); $policyTable->addElement(new htmlTableExtendedSelect('passwordMinClasses', $options4, array($cfg->passwordMinClasses), _('Minimum character classes'), '242'), true); +$policyTable->addVerticalSpace('5px'); +$rulesCountOptions = array(_('all') => '-1', '3' => '3', '4' => '4'); +$rulesCountSelect = new htmlTableExtendedSelect('passwordRulesCount', $rulesCountOptions, array($cfg->checkedRulesCount), _('Number of rules that must match'), '246'); +$rulesCountSelect->setHasDescriptiveElements(true); +$policyTable->addElement($rulesCountSelect, true); +$passwordMustNotContainUser = ($cfg->passwordMustNotContainUser === 'true') ? true : false; +$policyTable->addElement(new htmlTableExtendedInputCheckbox('passwordMustNotContainUser',$passwordMustNotContainUser , _('Password must not contain user name'), '247'), true); +$passwordMustNotContain3Chars = ($cfg->passwordMustNotContain3Chars === 'true') ? true : false; +$policyTable->addElement(new htmlTableExtendedInputCheckbox('passwordMustNotContain3Chars', $passwordMustNotContain3Chars, _('Password must not contain part of user/first/last name'), '248'), true); $container->addElement($policyTable, true); $container->addElement(new htmlSpacer(null, '10px'), true); diff --git a/lam/tests/lib/security.inc b/lam/tests/lib/security.inc new file mode 100644 index 00000000..3be52b04 --- /dev/null +++ b/lam/tests/lib/security.inc @@ -0,0 +1,144 @@ +/* +$Id$ + + This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/) + Copyright (C) 2014 Roland Gruber + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +*/ +cfg = &$_SESSION ['cfgMain']; + $this->resetPasswordRules(); + } + + public function testMinLength() { + $this->cfg->passwordMinLength = 5; + $this->checkPwd(array('55555', '666666'), array('1', '22', '333', '4444')); + } + + public function testMinUpper() { + $this->cfg->passwordMinUpper = 3; + $this->checkPwd(array('55A5AA55', '6BB666BB66', 'ABC'), array ('1A', '2C2C', 'AB3', '44BB')); + } + + public function testMinLower() { + $this->cfg->passwordMinLower = 3; + $this->checkPwd(array('55a5aa55', '6bb666bb66', 'abc'), array ('1a', '2c2c', 'ab3', '44bbABC')); + } + + public function testMinNumeric() { + $this->cfg->passwordMinNumeric = 3; + $this->checkPwd(array('333', '4444'), array('1', '22', '33A', '44bb')); + } + + public function testMinSymbol() { + $this->cfg->passwordMinSymbol = 3; + $this->checkPwd(array('---', '++++'), array('1.', '2.2.', '3+3+A', '44bb')); + } + + public function testMinClasses() { + $this->cfg->passwordMinClasses = 3; + $this->checkPwd(array('aB.', 'aB.1', 'aa.B99'), array('1', '2.', '3+-', '44bb')); + } + + public function testRulesCount() { + $this->cfg->passwordMinUpper = 3; + $this->cfg->passwordMinLower = 3; + $this->cfg->passwordMinNumeric = 3; + $this->cfg->passwordMinSymbol = 3; + $this->cfg->passwordMinClasses = 3; + // all rules + $this->cfg->checkedRulesCount = -1; + $this->checkPwd(array('ABC---abc123', 'ABC123xxx.-.-'), array('1', '2.', '3+-', '44bb', 'ABCabc---22')); + // at least 3 rules + $this->cfg->checkedRulesCount = 3; + $this->checkPwd(array('ABC---abc', 'ABC123.-.-', 'ABCabc-'), array('1', '2.', '3+-', '44bb', 'ABC--22')); + } + + public function testUser() { + $this->cfg->passwordMustNotContainUser = 'true'; + $this->checkPwd(array('u', 'us', 'use', 'use1r'), array('user', '2user', 'user3'), 'user'); + } + + public function testUserAttributes() { + $this->cfg->passwordMustNotContain3Chars = 'true'; + $this->checkPwd(array('u', 'us', 'us1e', 'us1er'), array('use', 'user', '2user', 'user3'), 'user'); + $this->checkPwd( + array('uf', 'usfi', 'us1ela3s', 'us1er.la#st'), + array('use', 'user', '2user', 'user3', 'las', 'last', 'fir', 'first'), + 'user', + array('first', 'last')); + } + + /** + * Resets the password rules to do no checks at all. + */ + private function resetPasswordRules() { + $this->cfg->passwordMinLength = 0; + $this->cfg->passwordMinUpper = 0; + $this->cfg->passwordMinLower = 0; + $this->cfg->passwordMinNumeric = 0; + $this->cfg->passwordMinSymbol = 0; + $this->cfg->passwordMinClasses = 0; + $this->cfg->checkedRulesCount = -1; + $this->cfg->passwordMustNotContainUser = 'false'; + $this->cfg->passwordMustNotContain3Chars = 'false'; + } + + /** + * Checks if the given passwords are correctly accepted/rejected. + * + * @param array $pwdsToAccept passwords that must be accepted + * @param array $pwdsToReject passwords that must be rejected + * @param String $userName user name + * @param array $otherUserAttrs other user attributes to check + */ + private function checkPwd($pwdsToAccept, $pwdsToReject, $userName = null, $otherUserAttrs = null) { + if ($userName == null) { + $userName = 'username'; + } + if ($otherUserAttrs == null) { + $otherUserAttrs = array (); + } + foreach ($pwdsToAccept as $pwd) { + $this->assertTrue(checkPasswordStrength($pwd, $userName, $otherUserAttrs)); + } + foreach ($pwdsToReject as $pwd) { + $this->assertNotTrue(checkPasswordStrength($pwd, $userName, $otherUserAttrs)); + } + } + +} + +?> \ No newline at end of file diff --git a/lam/tests/utils/configuration.inc b/lam/tests/utils/configuration.inc new file mode 100644 index 00000000..2e3d3242 --- /dev/null +++ b/lam/tests/utils/configuration.inc @@ -0,0 +1,42 @@ +/* +$Id$ + + This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/) + Copyright (C) 2014 Roland Gruber + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +*/ +logDestination = 'NONE'; + $_SESSION['cfgMain'] = $cfgMain; + $cfgPath = dirname(__FILE__) . '/../../config/phpunit.conf'; + if (file_exists($cfgPath)) { + unlink($cfgPath); + } + touch($cfgPath); + $config = new LAMConfig('phpunit'); + $_SESSION['config'] = $config; +} + +?> \ No newline at end of file