| +--------------------------------------------------------------------------+ | Author: Timotheus Pokorra | +--------------------------------------------------------------------------+ */ class password_policy { private static function get_password_policy() { // TODO: get the password policy from LDAP instead? $conf = Conf::get_instance(); $passwordpolicy = $conf->get('kolab', 'password_policy', Conf::AUTO); if (isset($passwordpolicy) && !isset($passwordpolicy['specialChars'])) { $passwordpolicy['specialChars'] = self::get_special_characters($passwordpolicy); } return $passwordpolicy; } /** * calculate which special characters will work in the current setup * * @return string with the allowed special characters */ private static function get_special_characters($passwordpolicy) { $special_chars = ''; // only use the characters from the 127 bit range. otherwise the user gets characters that he cannot type or does not know, eg Umlaut $maxcharacter = 127; for ($c = 33; $c < $maxcharacter; $c++) { if ($c >= ord('0') && $c <= ord('9')) { continue; } if ($c >= ord('a') && $c <= ord('z')) { continue; } if ($c >= ord('A') && $c <= ord('Z')) { continue; } if ($c == 127 || $c == 255) { continue; } # do not use some characters that are not well-known to users if (chr($c) == '`' || chr($c) == '|') { continue; } if ($c > 127) { $special_chars .= mb_convert_encoding('&#' . intval($c) . ';', 'UTF-8', 'HTML-ENTITIES'); } else { $special_chars .= chr($c); } } return $special_chars; } /** * validate if password matches the password policy * @throws an exception if password is not secure enough */ public static function validate_password($password) { $passwordpolicy = self::get_password_policy(); if (!isset($passwordpolicy)) { return true; } $password = trim($password); $errors = ''; if (strlen($password) < $passwordpolicy['minLength']) { $errors .= kolab_api_controller::translate("error").": ". kolab_api_controller::translate("user.password.tooshort", $passwordpolicy['minLength']).'
'; } $countUpperCaseChars = strlen(preg_replace('![^A-Z]+!', '', $password)); if ($countUpperCaseChars < $passwordpolicy['minUpper']) { $errors .= kolab_api_controller::translate("error").": ". kolab_api_controller::translate("user.password.moreupper", $passwordpolicy['minUpper']).'
'; } $countLowerCaseChars = strlen(preg_replace('![^a-z]+!', '', $password)); if ($countLowerCaseChars < $passwordpolicy['minLower']) { $errors .= kolab_api_controller::translate("error").": ". kolab_api_controller::translate("user.password.morelower", $passwordpolicy['minLower']).'
'; } $countDigits = strlen(preg_replace('![^0-9]+!', '', $password)); if ($countDigits < $passwordpolicy['minNumeric']) { $errors .= kolab_api_controller::translate("error").": ". kolab_api_controller::translate("user.password.moredigits", $passwordpolicy['minNumeric']).'
'; } $sourceCharacters = str_split($passwordpolicy['specialChars']); $countSpecialChars = 0; foreach($sourceCharacters as $currentCharacter) { $countSpecialChars += substr_count($password, $currentCharacter); } if ($countSpecialChars < $passwordpolicy['minSpecial']) { $errors .= kolab_api_controller::translate("error").": ". kolab_api_controller::translate("user.password.morespecial", $passwordpolicy['minSpecial'], $passwordpolicy['specialChars']).'
'; } # by default there is a 7-bit password enforcement plugin enabled on 389ds if ($passwordpolicy['7bitPasswordEnforcement'] == '0') { // do not test for characters beyond 127 } else if ($countUpperCaseChars + $countLowerCaseChars + $countDigits + $countSpecialChars < strlen($password)) { $errors .= kolab_api_controller::translate("error").": ". kolab_api_controller::translate("user.password.notallowed").'
'; } if ($errors != "") { throw new Exception($errors); } } /** * generate a password that meets the given password complexity policy * * @return password */ public static function generate_password() { $passwordpolicy = self::get_password_policy(); if (!isset($passwordpolicy)) { # default behaviour, if no policy has been configured exec("head -c 200 /dev/urandom | tr -dc _A-Z-a-z-0-9 | head -c15", $userpassword_plain); return $userpassword_plain[0]; } $password = ''; if ($passwordpolicy['minNumeric'] > 0) { unset($pwd_snippet); exec("head -c 200 /dev/urandom | tr -dc 0-9 | head -c".$passwordpolicy['minNumeric'], $pwd_snippet); $password .= $pwd_snippet[0]; } if ($passwordpolicy['minUpper'] > 0) { unset($pwd_snippet); exec("head -c 200 /dev/urandom | tr -dc 'A-Z' | head -c".$passwordpolicy['minUpper'], $pwd_snippet); $password .= $pwd_snippet[0]; } if ($passwordpolicy['minSpecial'] > 0) { # using rand instead of /dev/urandom to avoid problems with special characters on bash command line for ($countSpecialChar = 0; $countSpecialChar < $passwordpolicy['minSpecial']; $countSpecialChar++) { $password .= mb_substr($passwordpolicy['specialChars'], rand() % mb_strlen($passwordpolicy['specialChars']), 1); } } $numberLowerCase = $passwordpolicy['minLower']; if ($passwordpolicy['minLength'] > mb_strlen($password) + $passwordpolicy['minLower']) { $numberLowerCase = $passwordpolicy['minLength'] - mb_strlen($password); } unset($pwd_snippet); exec("head -c 200 /dev/urandom | tr -dc a-z | head -c".$numberLowerCase, $pwd_snippet); $password .= $pwd_snippet[0]; $len = mb_strlen($password); $pwd_chars = array(); while($len-- > 0) { $pwd_chars[] = mb_substr($password, $len, 1); } shuffle($pwd_chars); return join('', $pwd_chars); } }