/* * Password Management Servlets (PWM) * http://www.pwm-project.org * * Copyright (c) 2006-2009 Novell, Inc. * Copyright (c) 2009-2017 The PWM Project * * 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 */ package password.pwm.util; import com.google.gson.reflect.TypeToken; import com.novell.ldapchai.ChaiUser; import com.novell.ldapchai.exception.ChaiError; import com.novell.ldapchai.exception.ChaiPasswordPolicyException; import com.novell.ldapchai.exception.ChaiUnavailableException; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import password.pwm.AppProperty; import password.pwm.PwmApplication; import password.pwm.PwmConstants; import password.pwm.bean.SessionLabel; import password.pwm.bean.UserInfoBean; import password.pwm.bean.pub.PublicUserInfoBean; import password.pwm.config.Configuration; import password.pwm.config.PwmSetting; import password.pwm.config.option.ADPolicyComplexity; import password.pwm.config.profile.PwmPasswordPolicy; import password.pwm.config.profile.PwmPasswordPolicy.RuleHelper; import password.pwm.config.profile.PwmPasswordRule; import password.pwm.error.ErrorInformation; import password.pwm.error.PwmDataValidationException; import password.pwm.error.PwmError; import password.pwm.error.PwmOperationalException; import password.pwm.error.PwmUnrecoverableException; import password.pwm.svc.PwmService; import password.pwm.svc.stats.Statistic; import password.pwm.util.java.JavaHelper; import password.pwm.util.java.JsonUtil; import password.pwm.util.java.StringUtil; import password.pwm.util.logging.PwmLogger; import password.pwm.util.macro.MacroMachine; import password.pwm.util.operations.PasswordUtility; import password.pwm.ws.client.rest.RestClientHelper; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; public class PwmPasswordRuleValidator { private static final PwmLogger LOGGER = PwmLogger.forClass(PwmPasswordRuleValidator.class); private final PwmApplication pwmApplication; private final PwmPasswordPolicy policy; private final Locale locale; public enum Flag { FailFast, } public PwmPasswordRuleValidator(final PwmApplication pwmApplication, final PwmPasswordPolicy policy) { this.pwmApplication = pwmApplication; this.policy = policy; this.locale = PwmConstants.DEFAULT_LOCALE; } public PwmPasswordRuleValidator( final PwmApplication pwmApplication, final PwmPasswordPolicy policy, final Locale locale ) { this.pwmApplication = pwmApplication; this.policy = policy; this.locale = locale; } public boolean testPassword( final PasswordData password, final PasswordData oldPassword, final UserInfoBean userInfoBean, final ChaiUser user ) throws PwmDataValidationException, ChaiUnavailableException, PwmUnrecoverableException { final List<ErrorInformation> errorResults = validate(password, oldPassword, userInfoBean); if (!errorResults.isEmpty()) { throw new PwmDataValidationException(errorResults.iterator().next()); } if (user != null) { try { LOGGER.trace("calling chai directory password validation checker"); user.testPasswordPolicy(password.getStringValue()); } catch (UnsupportedOperationException e) { LOGGER.trace("Unsupported operation was thrown while validating password: " + e.toString()); } catch (ChaiUnavailableException e) { pwmApplication.getStatisticsManager().incrementValue(Statistic.LDAP_UNAVAILABLE_COUNT); LOGGER.warn("ChaiUnavailableException was thrown while validating password: " + e.toString()); throw e; } catch (ChaiPasswordPolicyException e) { final ChaiError passwordError = e.getErrorCode(); final PwmError pwmError = PwmError.forChaiError(passwordError); final ErrorInformation info = new ErrorInformation(pwmError == null ? PwmError.PASSWORD_UNKNOWN_VALIDATION : pwmError); LOGGER.trace("ChaiPasswordPolicyException was thrown while validating password: " + e.toString()); errorResults.add(info); } } if (!errorResults.isEmpty()) { throw new PwmDataValidationException(errorResults.iterator().next()); } return true; } /** * Validates a password against the configured rules of PWM. No directory operations * are performed here. * * @param password desired new password * @return true if the password is okay, never returns false. */ private List<ErrorInformation> validate( final PasswordData password, final PasswordData oldPassword, final UserInfoBean uiBean ) throws PwmUnrecoverableException { final List<ErrorInformation> internalResults = internalPwmPolicyValidator(password, oldPassword, uiBean); if (pwmApplication != null) { final List<ErrorInformation> externalResults = invokeExternalRuleMethods(pwmApplication.getConfig(), policy, password, uiBean); internalResults.addAll(externalResults); } return internalResults; } public List<ErrorInformation> internalPwmPolicyValidator( final PasswordData password, final PasswordData oldPassword, final UserInfoBean uiBean, final Flag... flags ) throws PwmUnrecoverableException { final String passwordString = password == null ? "" : password.getStringValue(); final String oldPasswordString = oldPassword == null ? null : oldPassword.getStringValue(); return internalPwmPolicyValidator(passwordString, oldPasswordString, uiBean, flags); } public List<ErrorInformation> internalPwmPolicyValidator( final String passwordString, final String oldPasswordString, final UserInfoBean uiBean, final Flag... flags ) throws PwmUnrecoverableException { final boolean failFast = flags != null && Arrays.asList(flags).contains(Flag.FailFast); // null check if (passwordString == null) { return Collections.singletonList(new ErrorInformation(PwmError.ERROR_UNKNOWN, "empty (null) new password")); } final List<ErrorInformation> errorList = new ArrayList<>(); final PwmPasswordPolicy.RuleHelper ruleHelper = policy.getRuleHelper(); final MacroMachine macroMachine = uiBean == null || uiBean.getUserIdentity() == null ? MacroMachine.forNonUserSpecific(pwmApplication, SessionLabel.SYSTEM_LABEL) : MacroMachine.forUser(pwmApplication, PwmConstants.DEFAULT_LOCALE, SessionLabel.SYSTEM_LABEL, uiBean.getUserIdentity()); //check against old password if (oldPasswordString != null && oldPasswordString.length() > 0 && ruleHelper.readBooleanValue(PwmPasswordRule.DisallowCurrent)) { if (oldPasswordString.length() > 0) { if (oldPasswordString.equalsIgnoreCase(passwordString)) { errorList.add(new ErrorInformation(PwmError.PASSWORD_SAMEASOLD)); } } //check chars from old password final int maxOldAllowed = ruleHelper.readIntValue(PwmPasswordRule.MaximumOldChars); if (maxOldAllowed > 0) { if (oldPasswordString.length() > 0) { final String lPassword = passwordString.toLowerCase(); final Set<Character> dupeChars = new HashSet<>(); //add all dupes to the set. for (final char loopChar : oldPasswordString.toLowerCase().toCharArray()) { if (lPassword.indexOf(loopChar) != -1) { dupeChars.add(loopChar); } } //count the number of (unique) set elements. if (dupeChars.size() >= maxOldAllowed) { errorList.add(new ErrorInformation(PwmError.PASSWORD_TOO_MANY_OLD_CHARS)); } } } } if (failFast && errorList.size() > 1) { return errorList; } errorList.addAll(basicSyntaxRuleChecks(passwordString,policy,uiBean)); if (failFast && errorList.size() > 1) { return errorList; } // check against disallowed values; if (!ruleHelper.getDisallowedValues().isEmpty()) { final String lcasePwd = passwordString.toLowerCase(); final Set<String> paramValues = new HashSet<>(ruleHelper.getDisallowedValues()); for (final String loopValue : paramValues) { if (loopValue != null && loopValue.length() > 0) { final String expandedValue = macroMachine.expandMacros(loopValue); if (StringUtils.isNotBlank(expandedValue)) { final String loweredLoop = expandedValue.toLowerCase(); if (lcasePwd.contains(loweredLoop)) { errorList.add(new ErrorInformation(PwmError.PASSWORD_USING_DISALLOWED)); } } } } } if (failFast && errorList.size() > 1) { return errorList; } // check disallowed attributes. if (!policy.getRuleHelper().getDisallowedAttributes().isEmpty()) { final List<String> paramConfigs = policy.getRuleHelper().getDisallowedAttributes(RuleHelper.Flag.KeepThresholds); if (uiBean != null) { final Map<String,String> userValues = uiBean.getCachedPasswordRuleAttributes(); for (final String paramConfig : paramConfigs) { final String[] parts = paramConfig.split(":"); final String attrName = parts[0]; final String disallowedValue = StringUtils.defaultString(userValues.get(attrName)); final int threshold = parts.length > 1 ? NumberUtils.toInt(parts[1]) : 0; if (containsDisallowedValue(passwordString, disallowedValue, threshold)) { LOGGER.trace("password rejected, same as user attr " + attrName); errorList.add(new ErrorInformation(PwmError.PASSWORD_SAMEASATTR)); } } } } if (failFast && errorList.size() > 1) { return errorList; } { // check password strength final int requiredPasswordStrength = ruleHelper.readIntValue(PwmPasswordRule.MinimumStrength); if (requiredPasswordStrength > 0) { if (pwmApplication != null) { final int passwordStrength = PasswordUtility.judgePasswordStrength(passwordString); if (passwordStrength < requiredPasswordStrength) { errorList.add(new ErrorInformation(PwmError.PASSWORD_TOO_WEAK)); //LOGGER.trace(pwmSession, "password rejected, password strength of " + passwordStrength + " is lower than policy requirement of " + requiredPasswordStrength); } } } } if (failFast && errorList.size() > 1) { return errorList; } // check regex matches. for (final Pattern pattern : ruleHelper.getRegExMatch(macroMachine)) { if (!pattern.matcher(passwordString).matches()) { errorList.add(new ErrorInformation(PwmError.PASSWORD_INVALID_CHAR)); //LOGGER.trace(pwmSession, "password rejected, does not match configured regex pattern: " + pattern.toString()); } } if (failFast && errorList.size() > 1) { return errorList; } // check no-regex matches. for (final Pattern pattern : ruleHelper.getRegExNoMatch(macroMachine)) { if (pattern.matcher(passwordString).matches()) { errorList.add(new ErrorInformation(PwmError.PASSWORD_INVALID_CHAR)); //LOGGER.trace(pwmSession, "password rejected, matches configured no-regex pattern: " + pattern.toString()); } } if (failFast && errorList.size() > 1) { return errorList; } // check char group matches if (ruleHelper.readIntValue(PwmPasswordRule.CharGroupsMinMatch) > 0) { final List<Pattern> ruleGroups = ruleHelper.getCharGroupValues(); if (ruleGroups != null && !ruleGroups.isEmpty()) { final int requiredMatches = ruleHelper.readIntValue(PwmPasswordRule.CharGroupsMinMatch); int matches = 0; for (final Pattern pattern : ruleGroups) { if (pattern.matcher(passwordString).find()) { matches++; } } if (matches < requiredMatches) { errorList.add(new ErrorInformation(PwmError.PASSWORD_NOT_ENOUGH_GROUPS)); } } if (failFast && errorList.size() > 1) { return errorList; } } if (failFast && errorList.size() > 1) { return errorList; } // check if the password is in the dictionary. if (ruleHelper.readBooleanValue(PwmPasswordRule.EnableWordlist)) { if (pwmApplication != null) { if (pwmApplication.getWordlistManager() != null && pwmApplication.getWordlistManager().status() == PwmService.STATUS.OPEN) { final boolean found = pwmApplication.getWordlistManager().containsWord(passwordString); if (found) { //LOGGER.trace(pwmSession, "password rejected, in wordlist file"); errorList.add(new ErrorInformation(PwmError.PASSWORD_INWORDLIST)); } } else { /* noop */ //LOGGER.warn(pwmSession, "password wordlist checking enabled, but wordlist is not available, skipping wordlist check"); } } if (failFast && errorList.size() > 1) { return errorList; } } if (failFast && errorList.size() > 1) { return errorList; } // check for shared (global) password history if (pwmApplication != null) { if (pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.PASSWORD_SHAREDHISTORY_ENABLE) && pwmApplication.getSharedHistoryManager().status() == PwmService.STATUS.OPEN) { final boolean found = pwmApplication.getSharedHistoryManager().containsWord(passwordString); if (found) { //LOGGER.trace(pwmSession, "password rejected, in global shared history"); errorList.add(new ErrorInformation(PwmError.PASSWORD_INWORDLIST)); } } if (failFast && errorList.size() > 1) { return errorList; } } return errorList; } static boolean containsDisallowedValue(final String password, final String disallowedValue, final int threshold) { if (StringUtils.isNotBlank(disallowedValue)) { if (threshold > 0) { if (disallowedValue.length() >= threshold) { final String[] disallowedValueChunks = StringUtil.createStringChunks(disallowedValue, threshold); for (final String chunk : disallowedValueChunks) { if (StringUtils.containsIgnoreCase(password, chunk)) { return true; } } } } else { // No threshold? Then the password can't contain the whole disallowed value return StringUtils.containsIgnoreCase(password, disallowedValue); } } return false; } /** * Check a supplied password for it's validity according to AD complexity rules. * - Not contain the user's account name or parts of the user's full name that exceed two consecutive characters * - Be at least six characters in length * - Contain characters from three of the following five categories: * - English uppercase characters (A through Z) * - English lowercase characters (a through z) * - Base 10 digits (0 through 9) * - Non-alphabetic characters (for example, !, $, #, %) * - Any character categorized as an alphabetic but is not uppercase or lowercase. * <p/> * See this article: http://technet.microsoft.com/en-us/library/cc786468%28WS.10%29.aspx * * @param userInfoBean userInfoBean * @param password password to test * @param charCounter associated charCounter for the password. * @return list of errors if the password does not meet requirements, or an empty list if the password complies * with AD requirements */ private static List<ErrorInformation> checkPasswordForADComplexity( final ADPolicyComplexity complexityLevel, final UserInfoBean userInfoBean, final String password, final PasswordCharCounter charCounter, final int maxGroupViolationCount ) { final List<ErrorInformation> errorList = new ArrayList<>(); if (password == null || password.length() < 6) { errorList.add(new ErrorInformation(PwmError.PASSWORD_TOO_SHORT)); return errorList; } final int maxLength = complexityLevel == ADPolicyComplexity.AD2003 ? 128 : 512; if (password.length() > maxLength) { errorList.add(new ErrorInformation(PwmError.PASSWORD_TOO_LONG)); return errorList; } if (userInfoBean != null && userInfoBean.getCachedPasswordRuleAttributes() != null) { final Map<String,String> userAttrs = userInfoBean.getCachedPasswordRuleAttributes(); final String samAccountName = userAttrs.get("sAMAccountName"); if (samAccountName != null && samAccountName.length() > 2 && samAccountName.length() >= password.length()) { if (password.toLowerCase().contains(samAccountName.toLowerCase())) { errorList.add(new ErrorInformation(PwmError.PASSWORD_INWORDLIST)); LOGGER.trace("Password violation due to ADComplexity check: Password contains sAMAccountName"); } } final String displayName = userAttrs.get("displayName"); if (displayName != null && displayName.length() > 2) { if (checkContainsTokens(password, displayName)) { errorList.add(new ErrorInformation(PwmError.PASSWORD_INWORDLIST)); LOGGER.trace("Password violation due to ADComplexity check: Tokens from displayName used in password"); } } } int complexityPoints = 0; if (charCounter.getUpperCharCount() > 0) { complexityPoints++; } if (charCounter.getLowerCharCount() > 0) { complexityPoints++; } if (charCounter.getNumericCharCount() > 0) { complexityPoints++; } switch (complexityLevel) { case AD2003: if (charCounter.getSpecialCharsCount() > 0 || charCounter.getOtherLetterCharCount() > 0) { complexityPoints++; } break; case AD2008: if (charCounter.getSpecialCharsCount() > 0) { complexityPoints++; } if (charCounter.getOtherLetterCharCount() > 0) { complexityPoints++; } break; default: JavaHelper.unhandledSwitchStatement(complexityLevel); } switch (complexityLevel) { case AD2008: final int totalGroups = 5; final int violations = totalGroups - complexityPoints; if (violations <= maxGroupViolationCount) { return errorList; } break; case AD2003: if (complexityPoints >= 3) { return errorList; } break; default: JavaHelper.unhandledSwitchStatement(complexityLevel); } if (charCounter.getUpperCharCount() < 1) { errorList.add(new ErrorInformation(PwmError.PASSWORD_NOT_ENOUGH_UPPER)); } if (charCounter.getLowerCharCount() < 1) { errorList.add(new ErrorInformation(PwmError.PASSWORD_NOT_ENOUGH_LOWER)); } if (charCounter.getNumericCharCount() < 1) { errorList.add(new ErrorInformation(PwmError.PASSWORD_NOT_ENOUGH_NUM)); } if (charCounter.getSpecialCharsCount() < 1) { errorList.add(new ErrorInformation(PwmError.PASSWORD_NOT_ENOUGH_SPECIAL)); } if (charCounter.getOtherLetterCharCount() < 1) { errorList.add(new ErrorInformation(PwmError.PASSWORD_UNKNOWN_VALIDATION)); } return errorList; } // escape characters permitted because they match the exact AD specification @SuppressWarnings("checkstyle:avoidescapedunicodecharacters") private static boolean checkContainsTokens(final String baseValue, final String checkPattern) { if (baseValue == null || baseValue.length() == 0) { return false; } if (checkPattern == null || checkPattern.length() == 0) { return false; } final String baseValueLower = baseValue.toLowerCase(); final String[] tokens = checkPattern.toLowerCase().split("[,\\.\\-\u2013\u2014_ \u00a3\\t]+"); if (tokens != null && tokens.length > 0) { for (final String token : tokens) { if (token.length() > 2) { if (baseValueLower.contains(token)) { return true; } } } } return false; } private static final String REST_RESPONSE_KEY_ERROR = "error"; private static final String REST_RESPONSE_KEY_ERROR_MSG = "errorMessage"; public List<ErrorInformation> invokeExternalRuleMethods( final Configuration config, final PwmPasswordPolicy pwmPasswordPolicy, final PasswordData password, final UserInfoBean uiBean ) throws PwmUnrecoverableException { final List<ErrorInformation> returnedErrors = new ArrayList<>(); final String restURL = config.readSettingAsString(PwmSetting.EXTERNAL_PWCHECK_REST_URLS); final boolean haltOnError = Boolean.parseBoolean(config.readAppProperty(AppProperty.WS_REST_CLIENT_PWRULE_HALTONERROR)); final Map<String,Object> sendData = new LinkedHashMap<>(); if (restURL == null || restURL.isEmpty()) { return Collections.emptyList(); } { final String passwordStr = password == null ? "" : password.getStringValue(); sendData.put("password", passwordStr); } if (pwmPasswordPolicy != null) { final LinkedHashMap<String,Object> policyData = new LinkedHashMap<>(); for (final PwmPasswordRule rule : PwmPasswordRule.values()) { policyData.put(rule.name(),pwmPasswordPolicy.getValue(rule)); } sendData.put("policy",policyData); } if (uiBean != null) { final MacroMachine macroMachine = MacroMachine.forUser(pwmApplication, PwmConstants.DEFAULT_LOCALE, SessionLabel.SYSTEM_LABEL, uiBean.getUserIdentity()); final PublicUserInfoBean publicUserInfoBean = PublicUserInfoBean.fromUserInfoBean(uiBean, pwmApplication.getConfig(), locale, macroMachine); sendData.put("userInfo", publicUserInfoBean); } final String jsonRequestBody = JsonUtil.serializeMap(sendData); try { final String responseBody = RestClientHelper.makeOutboundRestWSCall(pwmApplication, locale, restURL, jsonRequestBody); final Map<String,Object> responseMap = JsonUtil.deserialize(responseBody, new TypeToken<Map<String, Object>>() {} ); if (responseMap.containsKey(REST_RESPONSE_KEY_ERROR) && Boolean.parseBoolean(responseMap.get( REST_RESPONSE_KEY_ERROR).toString())) { if (responseMap.containsKey(REST_RESPONSE_KEY_ERROR_MSG)) { final String errorMessage = responseMap.get(REST_RESPONSE_KEY_ERROR_MSG).toString(); LOGGER.trace("external web service reported error: " + errorMessage); returnedErrors.add(new ErrorInformation(PwmError.PASSWORD_CUSTOM_ERROR,errorMessage,errorMessage,null)); } else { LOGGER.trace("external web service reported error without specifying an errorMessage"); returnedErrors.add(new ErrorInformation(PwmError.PASSWORD_CUSTOM_ERROR)); } } else { LOGGER.trace("external web service did not report an error"); } } catch (PwmOperationalException e) { final String errorMsg = "error executing external rule REST call: " + e.getMessage(); LOGGER.error(errorMsg); if (haltOnError) { throw new PwmUnrecoverableException(e.getErrorInformation(),e); } throw new IllegalStateException("http response error code: " + e.getMessage()); } return returnedErrors; } private static List<ErrorInformation> basicSyntaxRuleChecks( final String password, final PwmPasswordPolicy policy, final UserInfoBean uiBean ) { final List<ErrorInformation> errorList = new ArrayList<>(); final PwmPasswordPolicy.RuleHelper ruleHelper = policy.getRuleHelper(); final PasswordCharCounter charCounter = new PasswordCharCounter(password); final int passwordLength = password.length(); //Check minimum length if (passwordLength < ruleHelper.readIntValue(PwmPasswordRule.MinimumLength)) { errorList.add(new ErrorInformation(PwmError.PASSWORD_TOO_SHORT)); } //Check maximum length { final int passwordMaximumLength = ruleHelper.readIntValue(PwmPasswordRule.MaximumLength); if (passwordMaximumLength > 0 && passwordLength > passwordMaximumLength) { errorList.add(new ErrorInformation(PwmError.PASSWORD_TOO_LONG)); } } //check number of numeric characters { final int numberOfNumericChars = charCounter.getNumericCharCount(); if (ruleHelper.readBooleanValue(PwmPasswordRule.AllowNumeric)) { if (numberOfNumericChars < ruleHelper.readIntValue(PwmPasswordRule.MinimumNumeric)) { errorList.add(new ErrorInformation(PwmError.PASSWORD_NOT_ENOUGH_NUM)); } final int maxNumeric = ruleHelper.readIntValue(PwmPasswordRule.MaximumNumeric); if (maxNumeric > 0 && numberOfNumericChars > maxNumeric) { errorList.add(new ErrorInformation(PwmError.PASSWORD_TOO_MANY_NUMERIC)); } if (!ruleHelper.readBooleanValue( PwmPasswordRule.AllowFirstCharNumeric) && charCounter.isFirstNumeric()) { errorList.add(new ErrorInformation(PwmError.PASSWORD_FIRST_IS_NUMERIC)); } if (!ruleHelper.readBooleanValue( PwmPasswordRule.AllowLastCharNumeric) && charCounter.isLastNumeric()) { errorList.add(new ErrorInformation(PwmError.PASSWORD_LAST_IS_NUMERIC)); } } else { if (numberOfNumericChars > 0) { errorList.add(new ErrorInformation(PwmError.PASSWORD_TOO_MANY_NUMERIC)); } } } //check number of upper characters { final int numberOfUpperChars = charCounter.getUpperCharCount(); if (numberOfUpperChars < ruleHelper.readIntValue(PwmPasswordRule.MinimumUpperCase)) { errorList.add(new ErrorInformation(PwmError.PASSWORD_NOT_ENOUGH_UPPER)); } final int maxUpper = ruleHelper.readIntValue(PwmPasswordRule.MaximumUpperCase); if (maxUpper > 0 && numberOfUpperChars > maxUpper) { errorList.add(new ErrorInformation(PwmError.PASSWORD_TOO_MANY_UPPER)); } } //check number of alpha characters { final int numberOfAlphaChars = charCounter.getAlphaCharCount(); if (numberOfAlphaChars < ruleHelper.readIntValue(PwmPasswordRule.MinimumAlpha)) { errorList.add(new ErrorInformation(PwmError.PASSWORD_NOT_ENOUGH_ALPHA)); } final int maxAlpha = ruleHelper.readIntValue(PwmPasswordRule.MaximumAlpha); if (maxAlpha > 0 && numberOfAlphaChars > maxAlpha) { errorList.add(new ErrorInformation(PwmError.PASSWORD_TOO_MANY_ALPHA)); } } //check number of non-alpha characters { final int numberOfNonAlphaChars = charCounter.getNonAlphaCharCount(); if (numberOfNonAlphaChars < ruleHelper.readIntValue(PwmPasswordRule.MinimumNonAlpha)) { errorList.add(new ErrorInformation(PwmError.PASSWORD_NOT_ENOUGH_NONALPHA)); } final int maxNonAlpha = ruleHelper.readIntValue(PwmPasswordRule.MaximumNonAlpha); if (maxNonAlpha > 0 && numberOfNonAlphaChars > maxNonAlpha) { errorList.add(new ErrorInformation(PwmError.PASSWORD_TOO_MANY_NONALPHA)); } } //check number of lower characters { final int numberOfLowerChars = charCounter.getLowerCharCount(); if (numberOfLowerChars < ruleHelper.readIntValue(PwmPasswordRule.MinimumLowerCase)) { errorList.add(new ErrorInformation(PwmError.PASSWORD_NOT_ENOUGH_LOWER)); } final int maxLower = ruleHelper.readIntValue(PwmPasswordRule.MaximumLowerCase); if (maxLower > 0 && numberOfLowerChars > maxLower) { errorList.add(new ErrorInformation(PwmError.PASSWORD_TOO_MANY_UPPER)); } } //check number of special characters { final int numberOfSpecialChars = charCounter.getSpecialCharsCount(); if (ruleHelper.readBooleanValue(PwmPasswordRule.AllowSpecial)) { if (numberOfSpecialChars < ruleHelper.readIntValue(PwmPasswordRule.MinimumSpecial)) { errorList.add(new ErrorInformation(PwmError.PASSWORD_NOT_ENOUGH_SPECIAL)); } final int maxSpecial = ruleHelper.readIntValue(PwmPasswordRule.MaximumSpecial); if (maxSpecial > 0 && numberOfSpecialChars > maxSpecial) { errorList.add(new ErrorInformation(PwmError.PASSWORD_TOO_MANY_SPECIAL)); } if (!ruleHelper.readBooleanValue( PwmPasswordRule.AllowFirstCharSpecial) && charCounter.isFirstSpecial()) { errorList.add(new ErrorInformation(PwmError.PASSWORD_FIRST_IS_SPECIAL)); } if (!ruleHelper.readBooleanValue( PwmPasswordRule.AllowLastCharSpecial) && charCounter.isLastSpecial()) { errorList.add(new ErrorInformation(PwmError.PASSWORD_LAST_IS_SPECIAL)); } } else { if (numberOfSpecialChars > 0) { errorList.add(new ErrorInformation(PwmError.PASSWORD_TOO_MANY_SPECIAL)); } } } //Check maximum character repeats (sequential) { final int maxSequentialRepeat = ruleHelper.readIntValue(PwmPasswordRule.MaximumSequentialRepeat); if (maxSequentialRepeat > 0 && charCounter.getSequentialRepeatedChars() > maxSequentialRepeat) { errorList.add(new ErrorInformation(PwmError.PASSWORD_TOO_MANY_REPEAT)); } //Check maximum character repeats (overall) final int maxRepeat = ruleHelper.readIntValue(PwmPasswordRule.MaximumRepeat); if (maxRepeat > 0 && charCounter.getRepeatedChars() > maxRepeat) { errorList.add(new ErrorInformation(PwmError.PASSWORD_TOO_MANY_REPEAT)); } } //Check minimum unique character { final int minUnique = ruleHelper.readIntValue(PwmPasswordRule.MinimumUnique); if (minUnique > 0 && charCounter.getUniqueChars() < minUnique) { errorList.add(new ErrorInformation(PwmError.PASSWORD_NOT_ENOUGH_UNIQUE)); } } // check ad-complexity { final ADPolicyComplexity complexityLevel = ruleHelper.getADComplexityLevel(); if (complexityLevel == ADPolicyComplexity.AD2003 || complexityLevel == ADPolicyComplexity.AD2008) { final int maxGroupViolations = ruleHelper.readIntValue(PwmPasswordRule.ADComplexityMaxViolations); errorList.addAll(checkPasswordForADComplexity(complexityLevel, uiBean, password, charCounter, maxGroupViolations)); } } // check consecutive characters { final int maximumConsecutive = ruleHelper.readIntValue(PwmPasswordRule.MaximumConsecutive); if (tooManyConsecutiveChars(password, maximumConsecutive)) { errorList.add(new ErrorInformation(PwmError.PASSWORD_TOO_MANY_CONSECUTIVE)); } } return errorList; } public static boolean tooManyConsecutiveChars(final String str, final int maximumConsecutive) { if (str != null && maximumConsecutive > 1 && str.length() >= maximumConsecutive) { final int[] codePoints = StringUtil.toCodePointArray(str.toLowerCase()); int lastCodePoint = -1; int consecutiveCharCount = 1; for (int i=0; i<codePoints.length; i++) { if (codePoints[i] == lastCodePoint+1) { consecutiveCharCount++; } else { consecutiveCharCount = 1; } lastCodePoint = codePoints[i]; if (consecutiveCharCount == maximumConsecutive) { return true; } } } return false; } }