diff --git a/server/src/main/java/password/pwm/AppProperty.java b/server/src/main/java/password/pwm/AppProperty.java index 8a6440881..371568ee7 100644 --- a/server/src/main/java/password/pwm/AppProperty.java +++ b/server/src/main/java/password/pwm/AppProperty.java @@ -290,7 +290,6 @@ public enum AppProperty PASSWORD_RANDOMGEN_MAX_LENGTH ( "password.randomGenerator.maxLength" ), PASSWORD_RANDOMGEN_MIN_LENGTH ( "password.randomGenerator.minLength" ), PASSWORD_RANDOMGEN_DEFAULT_STRENGTH ( "password.randomGenerator.defaultStrength" ), - PASSWORD_RANDOMGEN_JITTER_COUNT ( "password.randomGenerator.jitter.count" ), /* Strength thresholds, introduced by the addition of the zxcvbn strength meter library (since it has 5 levels) */ PASSWORD_STRENGTH_THRESHOLD_VERY_STRONG ( "password.strength.threshold.veryStrong" ), diff --git a/server/src/main/java/password/pwm/health/LDAPHealthChecker.java b/server/src/main/java/password/pwm/health/LDAPHealthChecker.java index 0dd1d7cea..6476542e4 100644 --- a/server/src/main/java/password/pwm/health/LDAPHealthChecker.java +++ b/server/src/main/java/password/pwm/health/LDAPHealthChecker.java @@ -66,7 +66,6 @@ import password.pwm.util.logging.PwmLogger; import password.pwm.util.macro.MacroRequest; import password.pwm.util.password.PasswordUtility; -import password.pwm.util.password.RandomPasswordGenerator; import password.pwm.ws.server.rest.bean.PublicHealthData; import java.net.InetAddress; @@ -357,7 +356,7 @@ public List doLdapTestUserCheck( } if ( doPasswordChange ) { - final PasswordData newPassword = RandomPasswordGenerator.createRandomPassword( null, passwordPolicy, pwmDomain ); + final PasswordData newPassword = PasswordUtility.generateRandom( sessionLabel, passwordPolicy, pwmDomain ); try { theUser.setPassword( newPassword.getStringValue() ); diff --git a/server/src/main/java/password/pwm/http/servlet/GuestRegistrationServlet.java b/server/src/main/java/password/pwm/http/servlet/GuestRegistrationServlet.java index 0632fb706..019bd9c35 100644 --- a/server/src/main/java/password/pwm/http/servlet/GuestRegistrationServlet.java +++ b/server/src/main/java/password/pwm/http/servlet/GuestRegistrationServlet.java @@ -48,22 +48,21 @@ import password.pwm.http.bean.GuestRegistrationBean; import password.pwm.i18n.Message; import password.pwm.ldap.LdapOperationsHelper; -import password.pwm.user.UserInfo; import password.pwm.ldap.UserInfoFactory; import password.pwm.ldap.search.SearchConfiguration; import password.pwm.ldap.search.UserSearchService; import password.pwm.svc.stats.Statistic; import password.pwm.svc.stats.StatisticsClient; +import password.pwm.user.UserInfo; import password.pwm.util.FormMap; import password.pwm.util.PasswordData; import password.pwm.util.form.FormUtility; -import password.pwm.util.java.PwmUtil; import password.pwm.util.java.PwmDateFormat; +import password.pwm.util.java.PwmUtil; import password.pwm.util.logging.PwmLogger; import password.pwm.util.macro.MacroRequest; import password.pwm.util.operations.ActionExecutor; import password.pwm.util.password.PasswordUtility; -import password.pwm.util.password.RandomPasswordGenerator; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; @@ -478,7 +477,7 @@ public ProcessStatus handleCreateRequest( userIdentity, theUser ); - final PasswordData newPassword = RandomPasswordGenerator.createRandomPassword( pwmRequest.getLabel(), passwordPolicy, pwmDomain ); + final PasswordData newPassword = PasswordUtility.generateRandom( pwmRequest.getLabel(), passwordPolicy, pwmDomain ); theUser.setPassword( newPassword.getStringValue() ); diff --git a/server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java b/server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java index f924953b8..89bdd43e7 100644 --- a/server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java +++ b/server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java @@ -64,7 +64,6 @@ import password.pwm.util.macro.MacroRequest; import password.pwm.util.password.PasswordUtility; import password.pwm.util.password.PwmPasswordRuleValidator; -import password.pwm.util.password.RandomPasswordGenerator; import password.pwm.ws.server.RestResultBean; import password.pwm.ws.server.rest.RestCheckPasswordServer; import password.pwm.ws.server.rest.RestRandomPasswordServer; @@ -458,7 +457,7 @@ public ProcessStatus processCheckPasswordAction( final PwmRequest pwmRequest ) @ActionHandler( action = "randomPassword" ) public ProcessStatus processRandomPasswordAction( final PwmRequest pwmRequest ) throws IOException, PwmUnrecoverableException, ChaiUnavailableException { - final PasswordData passwordData = RandomPasswordGenerator.createRandomPassword( + final PasswordData passwordData = PasswordUtility.generateRandom( pwmRequest.getLabel(), pwmRequest.getPwmSession().getUserInfo().getPasswordPolicy(), pwmRequest.getPwmDomain() ); diff --git a/server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java b/server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java index 0c6c07d17..8da92eea0 100644 --- a/server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java +++ b/server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java @@ -62,12 +62,12 @@ import password.pwm.http.servlet.AbstractPwmServlet; import password.pwm.http.servlet.ControlledPwmServlet; import password.pwm.http.servlet.PwmServletDefinition; +import password.pwm.http.servlet.admin.system.ConfigManagerServlet; import password.pwm.http.servlet.configeditor.data.NavTreeDataMaker; import password.pwm.http.servlet.configeditor.data.NavTreeItem; import password.pwm.http.servlet.configeditor.data.NavTreeSettings; import password.pwm.http.servlet.configeditor.data.SettingData; import password.pwm.http.servlet.configeditor.data.SettingDataMaker; -import password.pwm.http.servlet.admin.system.ConfigManagerServlet; import password.pwm.i18n.Config; import password.pwm.i18n.Message; import password.pwm.i18n.PwmLocaleBundle; @@ -85,8 +85,8 @@ import password.pwm.util.json.JsonFactory; import password.pwm.util.logging.PwmLogger; import password.pwm.util.macro.MacroRequest; +import password.pwm.util.password.PasswordUtility; import password.pwm.util.password.RandomGeneratorConfig; -import password.pwm.util.password.RandomPasswordGenerator; import password.pwm.ws.server.RestResultBean; import password.pwm.ws.server.rest.RestRandomPasswordServer; import password.pwm.ws.server.rest.bean.PublicHealthData; @@ -934,7 +934,10 @@ public ProcessStatus restRandomPassword( final PwmRequest pwmRequest ) { final RestRandomPasswordServer.JsonInput jsonInput = pwmRequest.readBodyAsJsonObject( RestRandomPasswordServer.JsonInput.class ); final RandomGeneratorConfig randomConfig = RestRandomPasswordServer.jsonInputToRandomConfig( jsonInput, pwmRequest.getPwmDomain(), PwmPasswordPolicy.defaultPolicy() ); - final PasswordData randomPassword = RandomPasswordGenerator.createRandomPassword( pwmRequest.getLabel(), randomConfig, pwmRequest.getPwmDomain() ); + final PasswordData randomPassword = PasswordUtility.generateRandom( + pwmRequest.getLabel(), + randomConfig, + pwmRequest.getPwmDomain() ); final RestRandomPasswordServer.JsonOutput outputMap = new RestRandomPasswordServer.JsonOutput(); outputMap.setPassword( randomPassword.getStringValue() ); diff --git a/server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java b/server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java index 29dbebd22..f830b3d51 100644 --- a/server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java +++ b/server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java @@ -76,7 +76,6 @@ import password.pwm.util.logging.PwmLogger; import password.pwm.util.macro.MacroRequest; import password.pwm.util.password.PasswordUtility; -import password.pwm.util.password.RandomPasswordGenerator; import javax.servlet.ServletException; import java.io.IOException; @@ -465,11 +464,10 @@ static void doActionSendNewPassword( final PwmRequest pwmRequest ) + theUser.getEntryDN() ); // create new password - final PasswordData newPassword = RandomPasswordGenerator.createRandomPassword( + final PasswordData newPassword = PasswordUtility.generateRandom( pwmRequest.getLabel(), userInfo.getPasswordPolicy(), - pwmDomain - ); + pwmDomain ); LOGGER.trace( pwmRequest, () -> "generated random password value based on password policy for " + userIdentity.toDisplayString() ); diff --git a/server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java b/server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java index 3d191f009..e74c728d7 100644 --- a/server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java +++ b/server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java @@ -92,7 +92,6 @@ import password.pwm.util.operations.ActionExecutor; import password.pwm.util.password.PasswordUtility; import password.pwm.util.password.RandomGeneratorConfig; -import password.pwm.util.password.RandomPasswordGenerator; import password.pwm.ws.server.RestResultBean; import password.pwm.ws.server.rest.RestCheckPasswordServer; import password.pwm.ws.server.rest.RestRandomPasswordServer; @@ -1276,7 +1275,7 @@ public ProcessStatus processSetPasswordAction( final PwmRequest pwmRequest ) thr pwmRequest.getLabel(), userIdentity, chaiUser ); - newPassword = RandomPasswordGenerator.createRandomPassword( + newPassword = PasswordUtility.generateRandom( pwmRequest.getLabel(), passwordPolicy, pwmRequest.getPwmDomain() @@ -1336,7 +1335,7 @@ public ProcessStatus processRandomPasswordAction( final PwmRequest pwmRequest ) ); final RandomGeneratorConfig randomConfig = RandomGeneratorConfig.make( pwmRequest.getPwmDomain(), userInfo.getPasswordPolicy() ); - final PasswordData randomPassword = RandomPasswordGenerator.createRandomPassword( pwmRequest.getLabel(), randomConfig, pwmRequest.getPwmDomain() ); + final PasswordData randomPassword = PasswordUtility.generateRandom( pwmRequest.getLabel(), randomConfig, pwmRequest.getPwmDomain() ); final RestRandomPasswordServer.JsonOutput jsonOutput = new RestRandomPasswordServer.JsonOutput(); jsonOutput.setPassword( randomPassword.getStringValue() ); diff --git a/server/src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java b/server/src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java index 50af86321..359f50179 100644 --- a/server/src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java +++ b/server/src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java @@ -55,8 +55,6 @@ import password.pwm.http.PwmSession; import password.pwm.http.bean.NewUserBean; import password.pwm.http.servlet.forgottenpw.RemoteVerificationMethod; -import password.pwm.user.UserInfo; -import password.pwm.user.UserInfoBean; import password.pwm.ldap.auth.PwmAuthenticationSource; import password.pwm.ldap.auth.SessionAuthenticator; import password.pwm.ldap.search.SearchConfiguration; @@ -67,20 +65,21 @@ import password.pwm.svc.stats.StatisticsClient; import password.pwm.svc.token.TokenType; import password.pwm.svc.token.TokenUtil; +import password.pwm.user.UserInfo; +import password.pwm.user.UserInfoBean; import password.pwm.util.PasswordData; import password.pwm.util.form.FormUtility; import password.pwm.util.java.CollectionUtil; import password.pwm.util.java.PwmUtil; -import password.pwm.util.json.JsonFactory; import password.pwm.util.java.StringUtil; import password.pwm.util.java.TimeDuration; +import password.pwm.util.json.JsonFactory; import password.pwm.util.logging.PwmLogger; import password.pwm.util.macro.MacroReplacer; import password.pwm.util.macro.MacroRequest; import password.pwm.util.operations.ActionExecutor; import password.pwm.util.password.PasswordUtility; import password.pwm.util.password.RandomGeneratorConfig; -import password.pwm.util.password.RandomPasswordGenerator; import password.pwm.ws.client.rest.form.FormDataRequestBean; import password.pwm.ws.client.rest.form.FormDataResponseBean; import password.pwm.ws.client.rest.form.RestFormDataClient; @@ -161,7 +160,7 @@ static void createUser( else { final PwmPasswordPolicy pwmPasswordPolicy = newUserProfile.getNewUserPasswordPolicy( pwmRequest.getPwmRequestContext() ); - userPassword = RandomPasswordGenerator.createRandomPassword( pwmRequest.getLabel(), pwmPasswordPolicy, pwmRequest.getPwmDomain() ); + userPassword = PasswordUtility.generateRandom( pwmRequest.getLabel(), pwmPasswordPolicy, pwmRequest.getPwmDomain() ); } // set up the user creation attributes @@ -216,7 +215,7 @@ static void createUser( final RandomGeneratorConfig randomGeneratorConfig = RandomGeneratorConfig.make( pwmRequest.getPwmDomain(), newUserProfile.getNewUserPasswordPolicy( pwmRequest.getPwmRequestContext() ) ); - temporaryPassword = RandomPasswordGenerator.createRandomPassword( pwmRequest.getLabel(), randomGeneratorConfig, pwmDomain ); + temporaryPassword = PasswordUtility.generateRandom( pwmRequest.getLabel(), randomGeneratorConfig, pwmDomain ); } final ChaiUser proxiedUser = chaiProvider.getEntryFactory().newChaiUser( newUserDN ); try diff --git a/server/src/main/java/password/pwm/ldap/LdapDebugDataGenerator.java b/server/src/main/java/password/pwm/ldap/LdapDebugDataGenerator.java index df6fc8240..32866a5b7 100644 --- a/server/src/main/java/password/pwm/ldap/LdapDebugDataGenerator.java +++ b/server/src/main/java/password/pwm/ldap/LdapDebugDataGenerator.java @@ -66,7 +66,10 @@ public static List makeLdapDebugInfos( try { - final ChaiConfiguration profileChaiConf = LdapOperationsHelper.createChaiConfiguration( domainConfig, ldapProfile ); + final DomainConfig nonObfuscatedDomainConf = pwmDomain.getConfig(); + final ChaiConfiguration profileChaiConf = LdapOperationsHelper.createChaiConfiguration( + nonObfuscatedDomainConf, + ldapProfile ); final Collection chaiConfigurations = ChaiUtility.splitConfigurationPerReplica( profileChaiConf, null ); for ( final ChaiConfiguration chaiConfiguration : chaiConfigurations ) @@ -120,6 +123,7 @@ private static LdapDebugDataGenerator.LdapDebugServerInfo makeLdapDebugServerInf final LdapDebugServerInfo.LdapDebugServerInfoBuilder builder = LdapDebugServerInfo.builder(); builder.ldapServerlUrl( chaiConfiguration.getSetting( ChaiSetting.BIND_URLS ) ); + builder.vendorName( chaiProvider.getDirectoryVendor().name() ); final ChaiProvider loopProvider = chaiProvider.getProviderFactory().newProvider( chaiConfiguration ); { @@ -188,6 +192,7 @@ public static class LdapDebugInfo public static class LdapDebugServerInfo { private String ldapServerlUrl; + private String vendorName; private String testUserDN; private Map> testUserAttributes; private String proxyDN; diff --git a/server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java b/server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java index 3b14070c1..8e4a51f4c 100644 --- a/server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java +++ b/server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java @@ -60,7 +60,6 @@ import password.pwm.util.logging.PwmLogger; import password.pwm.util.password.PasswordUtility; import password.pwm.util.password.RandomGeneratorConfig; -import password.pwm.util.password.RandomPasswordGenerator; import java.time.Instant; import java.util.Collections; @@ -485,7 +484,7 @@ private Optional setTempUserPassword( // create random password for user final RandomGeneratorConfig randomGeneratorConfig = RandomGeneratorConfig.make( pwmDomain, passwordPolicy ); - final PasswordData currentPass = RandomPasswordGenerator.createRandomPassword( sessionLabel, randomGeneratorConfig, pwmDomain ); + final PasswordData currentPass = PasswordUtility.generateRandom( sessionLabel, randomGeneratorConfig, pwmDomain ); try { diff --git a/server/src/main/java/password/pwm/util/password/MutablePassword.java b/server/src/main/java/password/pwm/util/password/MutablePassword.java new file mode 100644 index 000000000..3c9b15998 --- /dev/null +++ b/server/src/main/java/password/pwm/util/password/MutablePassword.java @@ -0,0 +1,220 @@ +/* + * Password Management Servlets (PWM) + * http://www.pwm-project.org + * + * Copyright (c) 2006-2009 Novell, Inc. + * Copyright (c) 2009-2021 The PWM Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package password.pwm.util.password; + +import com.novell.ldapchai.exception.ImpossiblePasswordPolicyException; +import password.pwm.util.secure.PwmRandom; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +class MutablePassword +{ + private final RandomGeneratorRequest request; + private final SeedMachine seedMachine; + private final PwmRandom pwmRandom; + + private final StringBuilder password = new StringBuilder(); + + private PasswordCharCounter passwordCharCounter; + + MutablePassword( + final RandomGeneratorRequest request, + final SeedMachine seedMachine, + final PwmRandom pwmRandom, + final CharSequence password + ) + { + this.request = request; + this.seedMachine = seedMachine; + this.pwmRandom = pwmRandom; + this.reset( password ); + } + + String value() + { + return password.toString(); + } + + void reset( final CharSequence value ) + { + password.delete( 0, password.length() ); + password.append( value == null ? "" : value ); + } + + public PwmRandom getPwmRandom() + { + return pwmRandom; + } + + PasswordCharCounter getPasswordCharCounter() + { + final String passwordString = password.toString(); + if ( passwordCharCounter != null + && Objects.equals( passwordCharCounter.getPassword(), passwordString ) ) + { + return passwordCharCounter; + } + + passwordCharCounter = new PasswordCharCounter( passwordString ); + return passwordCharCounter; + } + + void randomizeCasing() + { + for ( int i = 0; i < password.length(); i++ ) + { + final int randspot = pwmRandom.nextInt( password.length() ); + final char oldChar = password.charAt( randspot ); + if ( Character.isLetter( oldChar ) ) + { + final char newChar = Character.isUpperCase( oldChar ) + ? Character.toLowerCase( oldChar ) + : Character.toUpperCase( oldChar ); + password.deleteCharAt( randspot ); + password.insert( randspot, newChar ); + return; + } + } + } + + public void addRandChar() + throws ImpossiblePasswordPolicyException + { + final List possibleCharTypes = + new ArrayList<>( request.maxCharsPerType().keySet() ); + final PasswordCharType charType = possibleCharTypes.get( pwmRandom.nextInt( possibleCharTypes.size() ) ); + addRandCharImpl( charType ); + } + + public void addRandCharExceptType( + final PasswordCharType notType + ) + throws ImpossiblePasswordPolicyException + { + final List possibleCharTypes = + new ArrayList<>( request.maxCharsPerType().keySet() ); + possibleCharTypes.remove( notType ); + final PasswordCharType charType = possibleCharTypes.get( pwmRandom.nextInt( possibleCharTypes.size() ) ); + addRandCharImpl( charType ); + } + + public void addRandChar( final PasswordCharType charType ) + { + addRandCharImpl( charType ); + } + + private void addRandCharImpl( final PasswordCharType charType ) + { + final int insertPosition = password.length() < 1 ? 0 : pwmRandom.nextInt( password.length() ); + final String possibleCharsToAdd = seedMachine.charsOfType( charType ); + final char charToAdd = possibleCharsToAdd.charAt( pwmRandom.nextInt( possibleCharsToAdd.length() ) ); + password.insert( insertPosition, charToAdd ); + } + + void deleteRandChar() + { + if ( password.length() == 0 ) + { + return; + } + password.deleteCharAt( pwmRandom.nextInt( password.length() - 1 ) ); + } + + void deleteFirstChar() + { + password.deleteCharAt( 0 ); + } + + void deleteLastChar() + { + password.deleteCharAt( password.length() ); + } + + + public void deleteRandCharExceptType( + final PasswordCharType notType + ) + throws ImpossiblePasswordPolicyException + { + final List possibleCharTypes = new ArrayList<>(); + for ( final PasswordCharType charType : PasswordCharType.uniqueTypes() ) + { + if ( charType != notType && getPasswordCharCounter().hasCharsOfType( charType ) ) + { + possibleCharTypes.add( charType ); + } + } + + if ( possibleCharTypes.isEmpty() ) + { + deleteRandChar(); + } + else + { + final PasswordCharType charType = possibleCharTypes.get( pwmRandom.nextInt( possibleCharTypes.size() ) ); + deleteRandChar( charType ); + } + } + + void deleteRandChar( + final PasswordCharType passwordCharType + ) + throws ImpossiblePasswordPolicyException + { + // no need to iterate the entire pw for large values. + final int maxDiscoverCount = 25; + + final String charsToRemove = getPasswordCharCounter().charsOfType( passwordCharType ); + final List removePossibilities = new ArrayList<>(); + for ( int i = 0; i < password.length() && removePossibilities.size() < maxDiscoverCount; i++ ) + { + final char loopChar = password.charAt( i ); + final int index = charsToRemove.indexOf( loopChar ); + if ( index != -1 ) + { + removePossibilities.add( i ); + } + } + if ( removePossibilities.isEmpty() ) + { + throw new ImpossiblePasswordPolicyException( ImpossiblePasswordPolicyException.ErrorEnum.UNEXPECTED_ERROR ); + } + final Integer charToDelete = removePossibilities.get( pwmRandom.nextInt( removePossibilities.size() ) ); + password.deleteCharAt( charToDelete ); + } + + public void randomPasswordCharModifier( + ) + { + switch ( pwmRandom.nextInt( 10 ) ) + { + case 0 -> addRandChar( PasswordCharType.SPECIAL ); + case 1 -> addRandChar( PasswordCharType.NUMBER ); + case 2 -> addRandChar( PasswordCharType.UPPERCASE ); + case 3 -> addRandChar( PasswordCharType.LOWERCASE ); + case 4, 5, 6, 7 -> addRandChar( PasswordCharType.LETTER ); + default -> randomizeCasing(); + } + } + +} diff --git a/server/src/main/java/password/pwm/util/password/PasswordCharCounter.java b/server/src/main/java/password/pwm/util/password/PasswordCharCounter.java index 1cabf09e2..a0e59e5df 100644 --- a/server/src/main/java/password/pwm/util/password/PasswordCharCounter.java +++ b/server/src/main/java/password/pwm/util/password/PasswordCharCounter.java @@ -20,75 +20,34 @@ package password.pwm.util.password; -public class PasswordCharCounter +import java.util.EnumMap; +import java.util.Map; + +class PasswordCharCounter { private final String password; private final int passwordLength; + private final Map cache = new EnumMap<>( PasswordCharType.class ); - public PasswordCharCounter( final String password ) + PasswordCharCounter( final String password ) { this.password = password; this.passwordLength = password.length(); } - public int getNumericCharCount( ) - { - return getNumericChars().length(); - } - - public String getNumericChars( ) + public int charTypeCount( final PasswordCharType passwordCharType ) { - return returnCharsOfType( password, CharType.NUMBER ); + return charsOfType( passwordCharType ).length(); } - public int getUpperCharCount( ) + public boolean hasCharsOfType( final PasswordCharType passwordCharType ) { - return getUpperChars().length(); + return charTypeCount( passwordCharType ) > 0; } - public String getUpperChars( ) + public String charsOfType( final PasswordCharType passwordCharType ) { - return returnCharsOfType( password, CharType.UPPERCASE ); - } - - public int getAlphaCharCount( ) - { - return getAlphaChars().length(); - } - - public String getAlphaChars( ) - { - return returnCharsOfType( password, CharType.LETTER ); - } - - public int getNonAlphaCharCount( ) - { - return getNonAlphaChars().length(); - } - - public String getNonAlphaChars( ) - { - return returnCharsOfType( password, CharType.NON_LETTER ); - } - - public int getLowerCharCount( ) - { - return getLowerChars().length(); - } - - public String getLowerChars( ) - { - return returnCharsOfType( password, CharType.LOWERCASE ); - } - - public int getSpecialCharsCount( ) - { - return getSpecialChars().length(); - } - - public String getSpecialChars( ) - { - return returnCharsOfType( password, CharType.SPECIAL ); + return cache.computeIfAbsent( passwordCharType, type -> PasswordCharType.charsOfType( password, type ) ); } public int getRepeatedChars( ) @@ -145,7 +104,7 @@ public int getSequentialRepeatedChars( ) return numberOfRepeats; } - public int getSequentialNumericChars( ) + public int sequentialCharCountOfType( final PasswordCharType passwordCharType ) { int numberOfRepeats = 0; @@ -154,7 +113,7 @@ public int getSequentialNumericChars( ) int loopRepeats = 0; for ( int j = i; j < passwordLength; j++ ) { - if ( Character.isDigit( password.charAt( j ) ) ) + if ( passwordCharType.isCharType( password.charAt( j ) ) ) { loopRepeats++; } @@ -168,36 +127,11 @@ public int getSequentialNumericChars( ) numberOfRepeats = loopRepeats; } } - return numberOfRepeats; - } - - public int getSequentialAlphaChars( ) - { - int numberOfRepeats = 0; - for ( int i = 0; i < passwordLength - 1; i++ ) - { - int loopRepeats = 0; - for ( int j = i; j < passwordLength; j++ ) - { - if ( Character.isLetter( password.charAt( j ) ) ) - { - loopRepeats++; - } - else - { - break; - } - } - if ( loopRepeats > numberOfRepeats ) - { - numberOfRepeats = loopRepeats; - } - } return numberOfRepeats; } - public int getUniqueChars( ) + public int uniqueCharCount( ) { final StringBuilder sb = new StringBuilder(); final String passwordL = password.toLowerCase(); @@ -212,76 +146,18 @@ public int getUniqueChars( ) return sb.length(); } - public int getOtherLetterCharCount( ) + public boolean isFirstCharType( final PasswordCharType passwordCharType ) { - return getOtherLetterChars().length(); + return password.length() > 0 && passwordCharType.isCharType( password.charAt( 0 ) ); } - public String getOtherLetterChars( ) + public boolean isLastCharType( final PasswordCharType passwordCharType ) { - return returnCharsOfType( password, CharType.OTHER_LETTER ); - } - - public boolean isFirstNumeric( ) - { - return password.length() > 0 && Character.isDigit( password.charAt( 0 ) ); - } - - public boolean isLastNumeric( ) - { - return password.length() > 0 && Character.isDigit( password.charAt( password.length() - 1 ) ); - } - - public boolean isFirstSpecial( ) - { - return password.length() > 0 && !Character.isLetterOrDigit( password.charAt( 0 ) ); - } - - public boolean isLastSpecial( ) - { - return password.length() > 0 && !Character.isLetterOrDigit( password.charAt( password.length() - 1 ) ); - } - - private static String returnCharsOfType( final String input, final CharType charType ) - { - final int passwordLength = input.length(); - final StringBuilder sb = new StringBuilder(); - for ( int i = 0; i < passwordLength; i++ ) - { - final char nextChar = input.charAt( i ); - if ( charType.getCharTester().isType( nextChar ) ) - { - sb.append( nextChar ); - } - } - return sb.toString(); - } - - private enum CharType - { - UPPERCASE( Character::isUpperCase ), - LOWERCASE( Character::isLowerCase ), - SPECIAL( character -> !Character.isLetterOrDigit( character ) ), - NUMBER( Character::isDigit ), - LETTER( Character::isLetter ), - NON_LETTER( character -> !Character.isLetter( character ) ), - OTHER_LETTER( character -> Character.getType( character ) == Character.OTHER_LETTER ),; - - private final transient CharTester charTester; - - CharType( final CharTester charClassType ) - { - this.charTester = charClassType; - } - - public CharTester getCharTester( ) - { - return charTester; - } + return password.length() > 0 && passwordCharType.isCharType( password.charAt( password.length() - 1 ) ); } - private interface CharTester + String getPassword() { - boolean isType( char character ); + return password; } } diff --git a/server/src/main/java/password/pwm/util/password/PasswordCharType.java b/server/src/main/java/password/pwm/util/password/PasswordCharType.java new file mode 100644 index 000000000..33ccf1833 --- /dev/null +++ b/server/src/main/java/password/pwm/util/password/PasswordCharType.java @@ -0,0 +1,190 @@ +/* + * Password Management Servlets (PWM) + * http://www.pwm-project.org + * + * Copyright (c) 2006-2009 Novell, Inc. + * Copyright (c) 2009-2021 The PWM Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package password.pwm.util.password; + +import password.pwm.config.profile.PwmPasswordPolicy; +import password.pwm.config.profile.PwmPasswordRule; +import password.pwm.error.PwmError; + +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +public enum PasswordCharType +{ + UPPERCASE( + Character::isUpperCase, + PwmError.PASSWORD_TOO_MANY_UPPER, + PwmError.PASSWORD_NOT_ENOUGH_UPPER ), + + LOWERCASE( + Character::isLowerCase, + PwmError.PASSWORD_TOO_MANY_LOWER, + PwmError.PASSWORD_NOT_ENOUGH_LOWER ), + SPECIAL( + character -> !Character.isLetterOrDigit( character ), + PwmError.PASSWORD_TOO_MANY_SPECIAL, + PwmError.PASSWORD_NOT_ENOUGH_SPECIAL ), + NUMBER( + Character::isDigit, + PwmError.PASSWORD_TOO_MANY_NUMERIC, + PwmError.PASSWORD_NOT_ENOUGH_NUM ), + LETTER( + Character::isLetter, + PwmError.PASSWORD_TOO_MANY_ALPHA, + PwmError.PASSWORD_NOT_ENOUGH_ALPHA ), + NON_LETTER( + character -> !Character.isLetter( character ), + PwmError.PASSWORD_TOO_MANY_NONALPHA, + PwmError.PASSWORD_NOT_ENOUGH_NONALPHA ), + OTHER_LETTER( + character -> Character.getType( character ) == Character.OTHER_LETTER, + null, + null ); + + private final transient CharTester charTester; + private final PwmError tooManyError; + private final PwmError tooFewError; + + private static final Set UNIQUE_TYPES = Set.of( UPPERCASE, LOWERCASE, SPECIAL, NUMBER ); + + PasswordCharType( + final CharTester charClassType, + final PwmError tooManyError, + final PwmError tooFewError + ) + { + this.charTester = charClassType; + this.tooFewError = tooFewError; + this.tooManyError = tooManyError; + } + + boolean isCharType( final char character ) + { + return charTester.isType( character ); + } + + public Optional getTooManyError() + { + return Optional.ofNullable( tooManyError ); + } + + public Optional getTooFewError() + { + return Optional.ofNullable( tooFewError ); + } + + public static String charsOfType( final String input, final PasswordCharType charType ) + { + final CharTester charTester = charType.getCharTester(); + return charsOfTester( input, charTester ); + } + + public static String charsExceptOfType( final String input, final PasswordCharType charType ) + { + final CharTester charTester = charType.getCharTester(); + final CharTester inverseTester = character -> !charTester.isType( character ); + return charsOfTester( input, inverseTester ); + } + + private static String charsOfTester( final String input, final CharTester charTester ) + { + Objects.requireNonNull( input ); + Objects.requireNonNull( charTester ); + + final int passwordLength = input.length(); + final StringBuilder sb = new StringBuilder(); + for ( int i = 0; i < passwordLength; i++ ) + { + final char nextChar = input.charAt( i ); + if ( charTester.isType( nextChar ) ) + { + sb.append( nextChar ); + } + } + return sb.toString(); + } + + private interface CharTester + { + boolean isType( char character ); + } + + private CharTester getCharTester() + { + return charTester; + } + + public static Set uniqueTypes() + { + return UNIQUE_TYPES; + } + + public static Map maxCharPerPolicy( + final RandomGeneratorConfig randomGeneratorConfig, + final PwmPasswordPolicy pwmPasswordPolicy + ) + { + final Map returnMap = new EnumMap<>( PasswordCharType.class ); + final PasswordRuleReaderHelper ruleHelper = pwmPasswordPolicy.ruleHelper(); + + for ( final CharTypeRuleAssociations charTypeRuleAssociations : CHAR_TYPE_RULE_ASSOCIATIONS_LIST ) + { + returnMap.put( charTypeRuleAssociations.passwordCharType(), 0 ); + if ( charTypeRuleAssociations.allowRule() == null || ruleHelper.readBooleanValue( charTypeRuleAssociations.allowRule() ) ) + { + final int maxOfType = ruleHelper.readIntValue( charTypeRuleAssociations.maxRule() ); + final int suggestedCount; + if ( maxOfType > 0 ) + { + suggestedCount = Math.min( maxOfType, randomGeneratorConfig.maximumLength() ); + } + else + { + + suggestedCount = randomGeneratorConfig.minimumLength(); + } + returnMap.put( charTypeRuleAssociations.passwordCharType(), suggestedCount ); + } + } + + return Map.copyOf( returnMap ); + } + + private static final List CHAR_TYPE_RULE_ASSOCIATIONS_LIST = List.of( + new CharTypeRuleAssociations( PasswordCharType.UPPERCASE, null, PwmPasswordRule.MinimumUpperCase, PwmPasswordRule.MaximumUpperCase ), + new CharTypeRuleAssociations( PasswordCharType.LOWERCASE, null, PwmPasswordRule.MinimumLowerCase, PwmPasswordRule.MaximumLowerCase ), + new CharTypeRuleAssociations( PasswordCharType.NUMBER, PwmPasswordRule.AllowNumeric, PwmPasswordRule.MinimumNumeric, PwmPasswordRule.MaximumNumeric ), + new CharTypeRuleAssociations( PasswordCharType.SPECIAL, PwmPasswordRule.AllowSpecial, PwmPasswordRule.MinimumSpecial, PwmPasswordRule.MaximumSpecial ) ); + + private record CharTypeRuleAssociations( + PasswordCharType passwordCharType, + PwmPasswordRule allowRule, + PwmPasswordRule minRule, + PwmPasswordRule maxRule + ) + { + } + +} diff --git a/server/src/main/java/password/pwm/util/password/PasswordRuleChecks.java b/server/src/main/java/password/pwm/util/password/PasswordRuleChecks.java index e146228cf..3e2c1c2b9 100644 --- a/server/src/main/java/password/pwm/util/password/PasswordRuleChecks.java +++ b/server/src/main/java/password/pwm/util/password/PasswordRuleChecks.java @@ -245,7 +245,7 @@ public List test( final String password, final String oldPassw final PasswordRuleReaderHelper ruleHelper = ruleCheckData.getRuleHelper(); final PasswordCharCounter charCounter = ruleCheckData.getCharCounter(); { - final int numberOfNumericChars = charCounter.getNumericCharCount(); + final int numberOfNumericChars = charCounter.charTypeCount( PasswordCharType.NUMBER ); if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowNumeric ) ) { if ( numberOfNumericChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumNumeric ) ) @@ -260,13 +260,13 @@ public List test( final String password, final String oldPassw } if ( !ruleHelper.readBooleanValue( - PwmPasswordRule.AllowFirstCharNumeric ) && charCounter.isFirstNumeric() ) + PwmPasswordRule.AllowFirstCharNumeric ) && charCounter.isFirstCharType( PasswordCharType.NUMBER ) ) { errorList.add( new ErrorInformation( PwmError.PASSWORD_FIRST_IS_NUMERIC ) ); } if ( !ruleHelper.readBooleanValue( - PwmPasswordRule.AllowLastCharNumeric ) && charCounter.isLastNumeric() ) + PwmPasswordRule.AllowLastCharNumeric ) && charCounter.isLastCharType( PasswordCharType.NUMBER ) ) { errorList.add( new ErrorInformation( PwmError.PASSWORD_LAST_IS_NUMERIC ) ); } @@ -295,7 +295,7 @@ public List test( final String password, final String oldPassw //check number of upper characters { - final int numberOfUpperChars = charCounter.getUpperCharCount(); + final int numberOfUpperChars = charCounter.charTypeCount( PasswordCharType.UPPERCASE ); if ( numberOfUpperChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumUpperCase ) ) { errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_UPPER ) ); @@ -310,7 +310,7 @@ public List test( final String password, final String oldPassw //check number of lower characters { - final int numberOfLowerChars = charCounter.getLowerCharCount(); + final int numberOfLowerChars = charCounter.charTypeCount( PasswordCharType.LOWERCASE ); if ( numberOfLowerChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumLowerCase ) ) { errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_LOWER ) ); @@ -338,7 +338,7 @@ public List test( final String password, final String oldPassw //check number of alpha characters { - final int numberOfAlphaChars = charCounter.getAlphaCharCount(); + final int numberOfAlphaChars = charCounter.charTypeCount( PasswordCharType.LETTER ); if ( numberOfAlphaChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumAlpha ) ) { errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_ALPHA ) ); @@ -353,7 +353,7 @@ public List test( final String password, final String oldPassw //check number of non-alpha characters { - final int numberOfNonAlphaChars = charCounter.getNonAlphaCharCount(); + final int numberOfNonAlphaChars = charCounter.charTypeCount( PasswordCharType.NON_LETTER ); if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowNonAlpha ) ) { @@ -392,7 +392,7 @@ public List test( final String password, final String oldPassw //check number of special characters { - final int numberOfSpecialChars = charCounter.getSpecialCharsCount(); + final int numberOfSpecialChars = charCounter.charTypeCount( PasswordCharType.SPECIAL ); if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowSpecial ) ) { if ( numberOfSpecialChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumSpecial ) ) @@ -407,13 +407,13 @@ public List test( final String password, final String oldPassw } if ( !ruleHelper.readBooleanValue( - PwmPasswordRule.AllowFirstCharSpecial ) && charCounter.isFirstSpecial() ) + PwmPasswordRule.AllowFirstCharSpecial ) && charCounter.isFirstCharType( PasswordCharType.SPECIAL ) ) { errorList.add( new ErrorInformation( PwmError.PASSWORD_FIRST_IS_SPECIAL ) ); } if ( !ruleHelper.readBooleanValue( - PwmPasswordRule.AllowLastCharSpecial ) && charCounter.isLastSpecial() ) + PwmPasswordRule.AllowLastCharSpecial ) && charCounter.isLastCharType( PasswordCharType.SPECIAL ) ) { errorList.add( new ErrorInformation( PwmError.PASSWORD_LAST_IS_SPECIAL ) ); } @@ -483,7 +483,7 @@ public List test( final String password, final String oldPassw //Check minimum unique character { final int minUnique = ruleHelper.readIntValue( PwmPasswordRule.MinimumUnique ); - if ( minUnique > 0 && charCounter.getUniqueChars() < minUnique ) + if ( minUnique > 0 && charCounter.uniqueCharCount() < minUnique ) { errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_UNIQUE ) ); } diff --git a/server/src/main/java/password/pwm/util/password/PasswordUtility.java b/server/src/main/java/password/pwm/util/password/PasswordUtility.java index f83283ad6..424720ec4 100644 --- a/server/src/main/java/password/pwm/util/password/PasswordUtility.java +++ b/server/src/main/java/password/pwm/util/password/PasswordUtility.java @@ -26,6 +26,7 @@ import com.novell.ldapchai.exception.ChaiOperationException; import com.novell.ldapchai.exception.ChaiPasswordPolicyException; import com.novell.ldapchai.exception.ChaiUnavailableException; +import com.novell.ldapchai.exception.ImpossiblePasswordPolicyException; import com.novell.ldapchai.impl.oracleds.entry.OracleDSEntries; import com.novell.ldapchai.provider.ChaiConfiguration; import com.novell.ldapchai.provider.ChaiProvider; @@ -217,6 +218,41 @@ private static ErrorInformation sendNewPasswordEmail( return null; } + /** + *

Creates a new password that satisfies the password rules. All rules are checked for. If for some + * reason the pwmRandom algorithm can not generate a valid password, null will be returned.

+ * + *

If there is an identifiable reason the password can not be created (such as mis-configured rules) then + * an {@link com.novell.ldapchai.exception.ImpossiblePasswordPolicyException} will be thrown.

+ * + * @param sessionLabel A valid pwmSession + * @param randomGeneratorConfig Policy to be used during generation + * @param pwmDomain Used to read configuration, seedmanager and other services. + * @return A randomly generated password value that meets the requirements of this {@code PasswordPolicy} + * @throws ImpossiblePasswordPolicyException If there is no way to create a password using the configured rules and + * default seed phrase + * @throws PwmUnrecoverableException if the operation can not be completed + */ + public static PasswordData generateRandom( + final SessionLabel sessionLabel, + final RandomGeneratorConfig randomGeneratorConfig, + final PwmDomain pwmDomain + ) + throws PwmUnrecoverableException + { + return RandomPasswordGenerator.generate( RandomGeneratorRequest.create( sessionLabel, randomGeneratorConfig, pwmDomain ) ); + } + + public static PasswordData generateRandom( + final SessionLabel sessionLabel, + final PwmPasswordPolicy passwordPolicy, + final PwmDomain pwmDomain + ) + throws PwmUnrecoverableException + { + return RandomPasswordGenerator.generate( RandomGeneratorRequest.create( sessionLabel, passwordPolicy, pwmDomain ) ); + } + enum PasswordPolicySource { @@ -853,19 +889,14 @@ public static int judgePasswordStrengthUsingZxcvbnAlgorithm( final int zxcvbnScore = strength.getScore(); // zxcvbn returns a score of 0-4 (see: https://github.com/nulab/zxcvbn4j) - switch ( zxcvbnScore ) - { - case 4: - return Integer.parseInt( domainConfig.readAppProperty( AppProperty.PASSWORD_STRENGTH_THRESHOLD_VERY_STRONG ) ); - case 3: - return Integer.parseInt( domainConfig.readAppProperty( AppProperty.PASSWORD_STRENGTH_THRESHOLD_STRONG ) ); - case 2: - return Integer.parseInt( domainConfig.readAppProperty( AppProperty.PASSWORD_STRENGTH_THRESHOLD_GOOD ) ); - case 1: - return Integer.parseInt( domainConfig.readAppProperty( AppProperty.PASSWORD_STRENGTH_THRESHOLD_WEAK ) ); - default: - return Integer.parseInt( domainConfig.readAppProperty( AppProperty.PASSWORD_STRENGTH_THRESHOLD_VERY_WEAK ) ); - } + return switch ( zxcvbnScore ) + { + case 4 -> Integer.parseInt( domainConfig.readAppProperty( AppProperty.PASSWORD_STRENGTH_THRESHOLD_VERY_STRONG ) ); + case 3 -> Integer.parseInt( domainConfig.readAppProperty( AppProperty.PASSWORD_STRENGTH_THRESHOLD_STRONG ) ); + case 2 -> Integer.parseInt( domainConfig.readAppProperty( AppProperty.PASSWORD_STRENGTH_THRESHOLD_GOOD ) ); + case 1 -> Integer.parseInt( domainConfig.readAppProperty( AppProperty.PASSWORD_STRENGTH_THRESHOLD_WEAK ) ); + default -> Integer.parseInt( domainConfig.readAppProperty( AppProperty.PASSWORD_STRENGTH_THRESHOLD_VERY_WEAK ) ); + }; } public static int judgePasswordStrengthUsingTraditionalAlgorithm( @@ -882,29 +913,29 @@ public static int judgePasswordStrengthUsingTraditionalAlgorithm( // -- Additions -- // amount of unique chars - if ( charCounter.getUniqueChars() > 7 ) + if ( charCounter.uniqueCharCount() > 7 ) { score = score + 10; } - score = score + ( ( charCounter.getUniqueChars() ) * 3 ); + score = score + ( ( charCounter.uniqueCharCount() ) * 3 ); // Numbers - if ( charCounter.getNumericCharCount() > 0 ) + if ( charCounter.hasCharsOfType( PasswordCharType.NUMBER ) ) { score = score + 8; - score = score + ( charCounter.getNumericCharCount() ) * 4; + score = score + ( charCounter.charTypeCount( PasswordCharType.NUMBER ) ) * 4; } // specials - if ( charCounter.getSpecialCharsCount() > 0 ) + if ( charCounter.hasCharsOfType( PasswordCharType.SPECIAL ) ) { score = score + 14; - score = score + ( charCounter.getSpecialCharsCount() ) * 5; + score = score + ( charCounter.charTypeCount( PasswordCharType.SPECIAL ) ) * 5; } // mixed case - if ( ( charCounter.getAlphaChars().length() != charCounter.getUpperChars().length() ) - && ( charCounter.getAlphaChars().length() != charCounter.getLowerChars().length() ) ) + if ( ( charCounter.charTypeCount( PasswordCharType.LETTER ) != charCounter.charTypeCount( PasswordCharType.UPPERCASE ) ) + && ( charCounter.charTypeCount( PasswordCharType.LETTER ) != charCounter.charTypeCount( PasswordCharType.LOWERCASE ) ) ) { score = score + 10; } @@ -912,9 +943,9 @@ public static int judgePasswordStrengthUsingTraditionalAlgorithm( // -- Deductions -- // sequential numbers - if ( charCounter.getSequentialNumericChars() > 2 ) + if ( charCounter.sequentialCharCountOfType( PasswordCharType.NUMBER ) > 2 ) { - score = score - ( charCounter.getSequentialNumericChars() - 1 ) * 4; + score = score - ( charCounter.sequentialCharCountOfType( PasswordCharType.NUMBER ) - 1 ) * 4; } // sequential chars @@ -923,7 +954,7 @@ public static int judgePasswordStrengthUsingTraditionalAlgorithm( score = score - ( charCounter.getSequentialRepeatedChars() ) * 5; } - return score > 100 ? 100 : score < 0 ? 0 : score; + return score > 100 ? 100 : Math.max( score, 0 ); } diff --git a/server/src/main/java/password/pwm/util/password/PwmPasswordAdRuleUtil.java b/server/src/main/java/password/pwm/util/password/PwmPasswordAdRuleUtil.java index f12522536..a5a28718d 100644 --- a/server/src/main/java/password/pwm/util/password/PwmPasswordAdRuleUtil.java +++ b/server/src/main/java/password/pwm/util/password/PwmPasswordAdRuleUtil.java @@ -128,23 +128,23 @@ private static List makeComplexityViolationErrors( final List errorList = new ArrayList<>(); // add errors complexity violations - if ( charCounter.getUpperCharCount() < 1 ) + if ( !charCounter.hasCharsOfType( PasswordCharType.UPPERCASE ) ) { errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_UPPER ) ); } - if ( charCounter.getLowerCharCount() < 1 ) + if ( !charCounter.hasCharsOfType( PasswordCharType.LOWERCASE ) ) { errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_LOWER ) ); } - if ( charCounter.getNumericCharCount() < 1 ) + if ( !charCounter.hasCharsOfType( PasswordCharType.NUMBER ) ) { errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_NUM ) ); } - if ( charCounter.getSpecialCharsCount() < 1 ) + if ( !charCounter.hasCharsOfType( PasswordCharType.SPECIAL ) ) { errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_SPECIAL ) ); } - if ( charCounter.getOtherLetterCharCount() < 1 ) + if ( !charCounter.hasCharsOfType( PasswordCharType.OTHER_LETTER ) ) { errorList.add( new ErrorInformation( PwmError.PASSWORD_UNKNOWN_VALIDATION ) ); } @@ -194,33 +194,34 @@ private static int calculateComplexity( ) { int complexityPoints = 0; - if ( charCounter.getUpperCharCount() > 0 ) + if ( charCounter.hasCharsOfType( PasswordCharType.UPPERCASE ) ) { complexityPoints++; } - if ( charCounter.getLowerCharCount() > 0 ) + if ( charCounter.hasCharsOfType( PasswordCharType.LOWERCASE ) ) { complexityPoints++; } - if ( charCounter.getNumericCharCount() > 0 ) + if ( charCounter.hasCharsOfType( PasswordCharType.NUMBER ) ) { complexityPoints++; } switch ( complexityLevel ) { case AD2003: - if ( charCounter.getSpecialCharsCount() > 0 || charCounter.getOtherLetterCharCount() > 0 ) + if ( charCounter.hasCharsOfType( PasswordCharType.SPECIAL ) + || charCounter.hasCharsOfType( PasswordCharType.OTHER_LETTER ) ) { complexityPoints++; } break; case AD2008: - if ( charCounter.getSpecialCharsCount() > 0 ) + if ( charCounter.hasCharsOfType( PasswordCharType.SPECIAL ) ) { complexityPoints++; } - if ( charCounter.getOtherLetterCharCount() > 0 ) + if ( charCounter.hasCharsOfType( PasswordCharType.OTHER_LETTER ) ) { complexityPoints++; } diff --git a/server/src/main/java/password/pwm/util/password/RandomGeneratorConfig.java b/server/src/main/java/password/pwm/util/password/RandomGeneratorConfig.java index 7dc407a1b..a5f4868de 100644 --- a/server/src/main/java/password/pwm/util/password/RandomGeneratorConfig.java +++ b/server/src/main/java/password/pwm/util/password/RandomGeneratorConfig.java @@ -20,9 +20,6 @@ package password.pwm.util.password; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Value; import password.pwm.AppProperty; import password.pwm.PwmDomain; import password.pwm.config.profile.PwmPasswordPolicy; @@ -30,42 +27,54 @@ import password.pwm.error.ErrorInformation; import password.pwm.error.PwmError; import password.pwm.error.PwmUnrecoverableException; -import password.pwm.util.java.CollectionUtil; -import java.util.Collection; -import java.util.Collections; - -@Value -@Builder( toBuilder = true, access = AccessLevel.PRIVATE ) -public class RandomGeneratorConfig +public record RandomGeneratorConfig( + SeedMachine seedMachine, + int minimumLength, + int maximumLength, + int minimumStrength, + int maximumAttempts +) { + private static final int MINIMUM_LENGTH = 0; + private static final int MAXIMUM_LENGTH = 1_000_000; private static final int MINIMUM_STRENGTH = 0; private static final int MAXIMUM_STRENGTH = 100; - /** - * A set of phrases (Strings) used to generate the pwmRandom passwords. There must be enough - * values in the phrases to build a random password that meets rule requirements - */ - @Builder.Default - private Collection seedlistPhrases = Collections.emptySet(); - - /** - * The minimum length desired for the password. The algorithm will attempt to make - * the returned value at least this long, but it is not guaranteed. - */ - private int minimumLength; + public RandomGeneratorConfig( + final SeedMachine seedMachine, + final int minimumLength, + final int maximumLength, + final int minimumStrength, + final int maximumAttempts + ) + { + this.seedMachine = seedMachine; + this.minimumLength = minimumLength; + this.maximumLength = maximumLength; + this.minimumStrength = minimumStrength; + this.maximumAttempts = maximumAttempts; - private int maximumLength; + if ( minimumLength < MINIMUM_LENGTH ) + { + throw new IllegalArgumentException( "minimumLength too low" ); + } - /** - * The minimum length desired strength. The algorithm will attempt to make - * the returned value at least this strong, but it is not guaranteed. - */ - private int minimumStrength; + if ( maximumLength > MAXIMUM_LENGTH ) + { + throw new IllegalArgumentException( "maximumLength too large" ); + } - private int jitter; + if ( minimumStrength < MINIMUM_STRENGTH ) + { + throw new IllegalArgumentException( "minimumStrength too low" ); + } - private int maximumAttempts; + if ( minimumStrength > MAXIMUM_STRENGTH ) + { + throw new IllegalArgumentException( "minimumStrength too large" ); + } + } public static RandomGeneratorConfig make( final PwmDomain pwmDomain, @@ -73,7 +82,6 @@ public static RandomGeneratorConfig make( ) throws PwmUnrecoverableException { - return make( pwmDomain, pwmPasswordPolicy, RandomGeneratorConfigRequest.builder().build() ); } @@ -84,66 +92,102 @@ public static RandomGeneratorConfig make( ) throws PwmUnrecoverableException { - final RandomGeneratorConfig config = RandomGeneratorConfig.builder() - .maximumAttempts( Integer.parseInt( pwmDomain.getConfig().readAppProperty( AppProperty.PASSWORD_RANDOMGEN_MAX_ATTEMPTS ) ) ) - .jitter( Integer.parseInt( pwmDomain.getConfig().readAppProperty( AppProperty.PASSWORD_RANDOMGEN_JITTER_COUNT ) ) ) - .maximumLength( figureMaximumLength( pwmDomain, pwmPasswordPolicy, request.getMaximumLength() ) ) - .minimumLength( figureMinimumLength( pwmDomain, pwmPasswordPolicy, request.getMinimumLength() ) ) - .minimumStrength( figureMinimumStrength( pwmDomain, pwmPasswordPolicy, request.getMinimumStrength() ) ) - .seedlistPhrases( CollectionUtil.isEmpty( request.getSeedlistPhrases() ) - ? RandomPasswordGenerator.DEFAULT_SEED_PHRASES : request.getSeedlistPhrases() ) - .build(); + final RandomGeneratorConfig config = new RandomGeneratorConfig( + SeedMachine.create( pwmDomain.getSecureService().pwmRandom(), request.getSeedlistPhrases() ), + figureMinimumLength( pwmDomain, pwmPasswordPolicy, request ), + figureMaximumLength( pwmDomain, pwmPasswordPolicy, request ), + figureMinimumStrength( pwmDomain, pwmPasswordPolicy, request.getMinimumStrength() ), + Integer.parseInt( pwmDomain.getConfig().readAppProperty( AppProperty.PASSWORD_RANDOMGEN_MAX_ATTEMPTS ) ) ); config.validateSettings( pwmDomain ); return config; } - private static int figureMaximumLength( final PwmDomain pwmDomain, final PwmPasswordPolicy pwmPasswordPolicy, final int requestedValue ) + private static int figureMaximumLength( + final PwmDomain pwmDomain, + final PwmPasswordPolicy pwmPasswordPolicy, + final RandomGeneratorConfigRequest request + ) { - int policyMax = requestedValue; - if ( requestedValue <= 0 ) + final int requestedValue = request.getMaximumLength(); + final int maxRandomGenLength = Integer.parseInt( pwmDomain.getConfig().readAppProperty( AppProperty.PASSWORD_RANDOMGEN_MAX_LENGTH ) ); + + int policyValue = -1; + if ( pwmPasswordPolicy != null ) { - policyMax = Integer.parseInt( pwmDomain.getConfig().readAppProperty( AppProperty.PASSWORD_RANDOMGEN_MAX_LENGTH ) ); + policyValue = pwmPasswordPolicy.ruleHelper().readIntValue( PwmPasswordRule.MaximumLength ); } - if ( pwmPasswordPolicy != null ) + + if ( requestedValue > 0 && requestedValue < policyValue ) { - policyMax = Math.min( policyMax, pwmPasswordPolicy.ruleHelper().readIntValue( PwmPasswordRule.MaximumLength ) ); + return Math.min( maxRandomGenLength, requestedValue ); } - return policyMax; + + if ( policyValue > 0 ) + { + return Math.min( maxRandomGenLength, policyValue ); + } + + return 50; } - private static int figureMinimumLength( final PwmDomain pwmDomain, final PwmPasswordPolicy pwmPasswordPolicy, final int requestedValue ) + private static int figureMinimumLength( + final PwmDomain pwmDomain, + final PwmPasswordPolicy pwmPasswordPolicy, + final RandomGeneratorConfigRequest request + ) { + final int requestedValue = request.getMinimumLength(); int returnVal = requestedValue; + if ( requestedValue <= 0 ) { returnVal = Integer.parseInt( pwmDomain.getConfig().readAppProperty( AppProperty.PASSWORD_RANDOMGEN_MIN_LENGTH ) ); } + if ( pwmPasswordPolicy != null ) { - final int policyMin = pwmPasswordPolicy.ruleHelper().readIntValue( PwmPasswordRule.MinimumLength ); + final PasswordRuleReaderHelper ruleHelper = pwmPasswordPolicy.ruleHelper(); + final int policyMin = ruleHelper.readIntValue( PwmPasswordRule.MinimumLength ); if ( policyMin > 0 ) { - returnVal = Math.min( returnVal, pwmPasswordPolicy.ruleHelper().readIntValue( PwmPasswordRule.MinimumLength ) ); + returnVal = Math.max( returnVal, ruleHelper.readIntValue( PwmPasswordRule.MinimumLength ) ); + } + + final int policyMaxLength = ruleHelper.readIntValue( PwmPasswordRule.MaximumLength ); + if ( policyMaxLength > 0 && returnVal > policyMaxLength ) + { + returnVal = policyMaxLength; } } + + final int requestMaxLength = request.getMaximumLength(); + if ( requestMaxLength > 0 && returnVal > requestMaxLength ) + { + returnVal = requestMaxLength; + } + return returnVal; } private static int figureMinimumStrength( final PwmDomain pwmDomain, final PwmPasswordPolicy pwmPasswordPolicy, final int requestedValue ) { - int policyMin = requestedValue; + int returnValue = requestedValue; if ( requestedValue <= 0 ) { - policyMin = Integer.parseInt( pwmDomain.getConfig().readAppProperty( AppProperty.PASSWORD_RANDOMGEN_DEFAULT_STRENGTH ) ); + returnValue = Integer.parseInt( pwmDomain.getConfig().readAppProperty( AppProperty.PASSWORD_RANDOMGEN_DEFAULT_STRENGTH ) ); } if ( pwmPasswordPolicy != null ) { - policyMin = Math.max( policyMin, pwmPasswordPolicy.ruleHelper().readIntValue( PwmPasswordRule.MinimumStrength ) ); + final int policyValue = pwmPasswordPolicy.ruleHelper().readIntValue( PwmPasswordRule.MinimumStrength ); + if ( policyValue > 0 ) + { + returnValue = Math.max( MAXIMUM_STRENGTH, policyValue ); + } } - return policyMin; + return returnValue; } void validateSettings( final PwmDomain pwmDomain ) @@ -151,7 +195,7 @@ void validateSettings( final PwmDomain pwmDomain ) { final int maxLength = Integer.parseInt( pwmDomain.getConfig().readAppProperty( AppProperty.PASSWORD_RANDOMGEN_MAX_LENGTH ) ); - if ( this.getMinimumLength() > maxLength ) + if ( this.minimumLength() > maxLength ) { throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, @@ -159,7 +203,15 @@ void validateSettings( final PwmDomain pwmDomain ) ) ); } - if ( this.getMaximumLength() > maxLength ) + if ( this.minimumLength() > this.maximumLength() ) + { + throw new PwmUnrecoverableException( new ErrorInformation( + PwmError.ERROR_INTERNAL, + "random generated password minimum length exceeds maximum length value" + ) ); + } + + if ( this.maximumLength() > maxLength ) { throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, @@ -167,7 +219,7 @@ void validateSettings( final PwmDomain pwmDomain ) ) ); } - if ( this.getMinimumStrength() > RandomGeneratorConfig.MAXIMUM_STRENGTH ) + if ( this.minimumStrength() > RandomGeneratorConfig.MAXIMUM_STRENGTH ) { throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, @@ -175,4 +227,5 @@ void validateSettings( final PwmDomain pwmDomain ) ) ); } } + } diff --git a/server/src/main/java/password/pwm/util/password/RandomGeneratorConfigRequest.java b/server/src/main/java/password/pwm/util/password/RandomGeneratorConfigRequest.java index 612299b4b..4577db3f6 100644 --- a/server/src/main/java/password/pwm/util/password/RandomGeneratorConfigRequest.java +++ b/server/src/main/java/password/pwm/util/password/RandomGeneratorConfigRequest.java @@ -23,8 +23,7 @@ import lombok.Builder; import lombok.Value; -import java.util.Collection; -import java.util.Collections; +import java.util.List; @Builder @Value @@ -35,7 +34,7 @@ public class RandomGeneratorConfigRequest * values in the phrases to build a random password that meets rule requirements */ @Builder.Default - private Collection seedlistPhrases = Collections.emptySet(); + private List seedlistPhrases = List.of(); /** * The minimum length desired for the password. The algorithm will attempt to make @@ -53,4 +52,5 @@ public class RandomGeneratorConfigRequest */ @Builder.Default private int minimumStrength = -1; + } diff --git a/server/src/main/java/password/pwm/util/password/RandomGeneratorRequest.java b/server/src/main/java/password/pwm/util/password/RandomGeneratorRequest.java new file mode 100644 index 000000000..b1876baff --- /dev/null +++ b/server/src/main/java/password/pwm/util/password/RandomGeneratorRequest.java @@ -0,0 +1,143 @@ +/* + * Password Management Servlets (PWM) + * http://www.pwm-project.org + * + * Copyright (c) 2006-2009 Novell, Inc. + * Copyright (c) 2009-2021 The PWM Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package password.pwm.util.password; + +import com.novell.ldapchai.exception.ImpossiblePasswordPolicyException; +import password.pwm.PwmDomain; +import password.pwm.bean.SessionLabel; +import password.pwm.config.profile.PwmPasswordPolicy; +import password.pwm.config.profile.PwmPasswordRule; +import password.pwm.error.PwmUnrecoverableException; +import password.pwm.util.java.CollectionUtil; +import password.pwm.util.secure.PwmRandom; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +record RandomGeneratorRequest( + SessionLabel sessionLabel, + PwmPasswordPolicy randomGenPolicy, + RandomGeneratorConfig randomGeneratorConfig, + Map maxCharsPerType, + PwmRandom pwmRandom, + PwmDomain pwmDomain +) +{ + RandomGeneratorRequest( + final SessionLabel sessionLabel, + final PwmPasswordPolicy randomGenPolicy, + final RandomGeneratorConfig randomGeneratorConfig, + final Map maxCharsPerType, + final PwmRandom pwmRandom, + final PwmDomain pwmDomain + ) + { + this.sessionLabel = Objects.requireNonNull( sessionLabel ); + this.randomGenPolicy = Objects.requireNonNull( randomGenPolicy ); + this.randomGeneratorConfig = Objects.requireNonNull( randomGeneratorConfig ); + this.maxCharsPerType = CollectionUtil.stripNulls( maxCharsPerType ); + this.pwmRandom = Objects.requireNonNull( pwmRandom ); + this.pwmDomain = Objects.requireNonNull( pwmDomain ); + } + + public static RandomGeneratorRequest create( + final SessionLabel sessionLabel, + final PwmPasswordPolicy passwordPolicy, + final PwmDomain pwmDomain + ) + throws PwmUnrecoverableException + { + final RandomGeneratorConfig randomGeneratorConfig = RandomGeneratorConfig.make( pwmDomain, passwordPolicy ); + + final Map maxCharsPerType = PasswordCharType.maxCharPerPolicy( randomGeneratorConfig, passwordPolicy ); + + return new RandomGeneratorRequest( + sessionLabel, + passwordPolicy, + randomGeneratorConfig, + maxCharsPerType, + pwmDomain.getSecureService().pwmRandom(), + pwmDomain + ); + } + + /** + *

Creates a new password that satisfies the password rules. All rules are checked for. If for some + * reason the pwmRandom algorithm can not generate a valid password, null will be returned.

+ * + *

If there is an identifiable reason the password can not be created (such as mis-configured rules) then + * an {@link com.novell.ldapchai.exception.ImpossiblePasswordPolicyException} will be thrown.

+ * + * @param sessionLabel A valid pwmSession + * @param randomGeneratorConfig Policy to be used during generation + * @param pwmDomain Used to read configuration, seedmanager and other services. + * @return A randomly generated password value that meets the requirements of this {@code PasswordPolicy} + * @throws ImpossiblePasswordPolicyException If there is no way to create a password using the configured rules and + * default seed phrase + * @throws PwmUnrecoverableException if the operation can not be completed + */ + public static RandomGeneratorRequest create( + final SessionLabel sessionLabel, + final RandomGeneratorConfig randomGeneratorConfig, + final PwmDomain pwmDomain + ) + throws PwmUnrecoverableException + { + // determine the password policy to use for random generation + final PwmPasswordPolicy randomGenPolicy = makeRandomGenPwdPolicy( randomGeneratorConfig, pwmDomain ); + + final Map maxCharsPerType = PasswordCharType.maxCharPerPolicy( randomGeneratorConfig, randomGenPolicy ); + + return new RandomGeneratorRequest( + sessionLabel, + randomGenPolicy, + randomGeneratorConfig, + maxCharsPerType, + pwmDomain.getSecureService().pwmRandom(), + pwmDomain + ); + } + + static PwmPasswordPolicy makeRandomGenPwdPolicy( + final RandomGeneratorConfig randomGeneratorConfig, + final PwmDomain pwmDomain ) + { + final PwmPasswordPolicy defaultPolicy = PwmPasswordPolicy.defaultPolicy(); + final Map newPolicyMap = new HashMap<>( defaultPolicy.getPolicyMap() ); + + newPolicyMap.put( PwmPasswordRule.MaximumLength.getKey(), String.valueOf( randomGeneratorConfig.maximumLength() ) ); + if ( randomGeneratorConfig.minimumLength() > defaultPolicy.ruleHelper().readIntValue( PwmPasswordRule.MinimumLength ) ) + { + newPolicyMap.put( PwmPasswordRule.MinimumLength.getKey(), String.valueOf( randomGeneratorConfig.minimumLength() ) ); + } + if ( randomGeneratorConfig.maximumLength() < defaultPolicy.ruleHelper().readIntValue( PwmPasswordRule.MaximumLength ) ) + { + newPolicyMap.put( PwmPasswordRule.MaximumLength.getKey(), String.valueOf( randomGeneratorConfig.maximumLength() ) ); + } + if ( randomGeneratorConfig.minimumStrength() > defaultPolicy.ruleHelper().readIntValue( PwmPasswordRule.MinimumStrength ) ) + { + newPolicyMap.put( PwmPasswordRule.MinimumStrength.getKey(), String.valueOf( randomGeneratorConfig.minimumStrength() ) ); + } + return PwmPasswordPolicy.createPwmPasswordPolicy( pwmDomain.getDomainID(), newPolicyMap ); + } + +} diff --git a/server/src/main/java/password/pwm/util/password/RandomPasswordGenerator.java b/server/src/main/java/password/pwm/util/password/RandomPasswordGenerator.java index 47aac208a..e95bb8c74 100644 --- a/server/src/main/java/password/pwm/util/password/RandomPasswordGenerator.java +++ b/server/src/main/java/password/pwm/util/password/RandomPasswordGenerator.java @@ -20,14 +20,12 @@ package password.pwm.util.password; -import com.novell.ldapchai.exception.ImpossiblePasswordPolicyException; -import lombok.Value; +import org.apache.commons.lang3.mutable.MutableInt; import password.pwm.PwmDomain; import password.pwm.bean.SessionLabel; import password.pwm.config.DomainConfig; import password.pwm.config.PwmSetting; import password.pwm.config.profile.PwmPasswordPolicy; -import password.pwm.config.profile.PwmPasswordRule; import password.pwm.error.ErrorInformation; import password.pwm.error.PwmError; import password.pwm.error.PwmUnrecoverableException; @@ -41,405 +39,242 @@ import java.time.Instant; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; /** * Random password generator. * * @author Jason D. Rivard */ -public class RandomPasswordGenerator +final class RandomPasswordGenerator { - /** - * Default seed phrases. Most basic ASCII chars, except those that are visually ambiguous are - * represented here. No multi-character phrases are included. - */ - public static final Set DEFAULT_SEED_PHRASES = Collections.unmodifiableSet( new HashSet<>( Arrays.asList( - "a", "b", "c", "d", "e", "f", "g", "h", "j", "k", "m", "n", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", - "a", "b", "c", "d", "e", "f", "g", "h", "j", "k", "m", "n", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", - "a", "b", "c", "d", "e", "f", "g", "h", "j", "k", "m", "n", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", - "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", - "2", "3", "4", "5", "6", "7", "8", "9", - "@", "&", "!", "?", "%", "$", "#", "^", ")", "(", "+", "-", "=", ".", ",", "/", "\\" - ) ) ); - private static final PwmLogger LOGGER = PwmLogger.forClass( RandomPasswordGenerator.class ); - public static PasswordData createRandomPassword( - final SessionLabel sessionLabel, - final PwmPasswordPolicy passwordPolicy, - final PwmDomain pwmDomain + private record MutatorResult( + String password, + boolean validPassword, + int rounds ) - throws PwmUnrecoverableException { - final RandomGeneratorConfig randomGeneratorConfig = RandomGeneratorConfig.make( pwmDomain, passwordPolicy ); - - return createRandomPassword( - sessionLabel, - randomGeneratorConfig, - pwmDomain - ); } + private RandomPasswordGenerator( ) + { + } - /** - *

Creates a new password that satisfies the password rules. All rules are checked for. If for some - * reason the pwmRandom algorithm can not generate a valid password, null will be returned.

- * - *

If there is an identifiable reason the password can not be created (such as mis-configured rules) then - * an {@link com.novell.ldapchai.exception.ImpossiblePasswordPolicyException} will be thrown.

- * - * @param sessionLabel A valid pwmSession - * @param randomGeneratorConfig Policy to be used during generation - * @param pwmDomain Used to read configuration, seedmanager and other services. - * @return A randomly generated password value that meets the requirements of this {@code PasswordPolicy} - * @throws ImpossiblePasswordPolicyException If there is no way to create a password using the configured rules and - * default seed phrase - * @throws PwmUnrecoverableException if the operation can not be completed - */ - public static PasswordData createRandomPassword( - final SessionLabel sessionLabel, - final RandomGeneratorConfig randomGeneratorConfig, - final PwmDomain pwmDomain + public static PasswordData generate( + final RandomGeneratorRequest request ) throws PwmUnrecoverableException { + final SessionLabel sessionLabel = request.sessionLabel(); + final PwmPasswordPolicy randomGenPolicy = request.randomGenPolicy(); + final RandomGeneratorConfig randomGeneratorConfig = request.randomGeneratorConfig(); + final PwmDomain pwmDomain = request.pwmDomain(); + final Instant startTime = Instant.now(); randomGeneratorConfig.validateSettings( pwmDomain ); - final PwmRandom pwmRandom = pwmDomain.getSecureService().pwmRandom(); - final SeedMachine seedMachine = new SeedMachine( pwmRandom, normalizeSeeds( randomGeneratorConfig.getSeedlistPhrases() ) ); - - // determine the password policy to use for random generation - final PwmPasswordPolicy randomGenPolicy = makeRandomGenPwdPolicy( randomGeneratorConfig, pwmDomain ); - // read a rule validator // modify until it passes all the rules - final MutatorResult mutatorResult = passwordMutator( sessionLabel, pwmDomain, seedMachine, randomGeneratorConfig, randomGenPolicy ); + final MutatorResult mutatorResult = mutatePassword( request ); // report outcome - - if ( mutatorResult.isValidPassword() ) + if ( mutatorResult.validPassword() ) { - LOGGER.trace( sessionLabel, () -> "finished random password generation after " + mutatorResult.getRounds() - + " rounds.", TimeDuration.fromCurrent( startTime ) ); + final Supplier logMsg = () -> "finished random password generation after " + + mutatorResult.rounds() + " rounds."; + LOGGER.trace( sessionLabel, logMsg, TimeDuration.fromCurrent( startTime ) ); + //System.out.println( logMsg.get() ); } else { if ( LOGGER.isInterestingLevel( PwmLogLevel.ERROR ) ) { final PwmPasswordRuleValidator pwmPasswordRuleValidator = PwmPasswordRuleValidator.create( sessionLabel, pwmDomain, randomGenPolicy ); - final int errors = pwmPasswordRuleValidator.internalPwmPolicyValidator( mutatorResult.getPassword(), null, null ).size(); - final int judgeLevel = PasswordUtility.judgePasswordStrength( pwmDomain.getConfig(), mutatorResult.getPassword() ); - LOGGER.error( sessionLabel, () -> "failed random password generation after " - + mutatorResult.getRounds() + " rounds. " + "(errors=" + errors + ", judgeLevel=" + judgeLevel, - TimeDuration.fromCurrent( startTime ) ); + final int errors = pwmPasswordRuleValidator.internalPwmPolicyValidator( mutatorResult.password(), null, null ).size(); + final int judgeLevel = PasswordUtility.judgePasswordStrength( pwmDomain.getConfig(), mutatorResult.password() ); + final Supplier logMsg = () -> "failed random password generation after " + + mutatorResult.rounds() + " rounds. " + "(errors=" + errors + ", judgeLevel=" + judgeLevel; + LOGGER.error( sessionLabel, logMsg, TimeDuration.fromCurrent( startTime ) ); + //System.out.println( logMsg.get() ); } } StatisticsClient.incrementStat( pwmDomain, Statistic.GENERATED_PASSWORDS ); - LOGGER.trace( sessionLabel, () -> "real-time random password generator called" - + " (" + TimeDuration.compactFromCurrent( startTime ) + ")" ); + LOGGER.trace( sessionLabel, () -> "real-time random password generator called", TimeDuration.fromCurrent( startTime ) ); - return new PasswordData( mutatorResult.getPassword() ); + //System.out.println( "total: " + TimeDuration.compactFromCurrent( startTime ) ); + return new PasswordData( mutatorResult.password() ); } - @Value - private static class MutatorResult - { - private final String password; - private final boolean validPassword; - private final int rounds; - } - - private static PwmPasswordPolicy makeRandomGenPwdPolicy( - final RandomGeneratorConfig effectiveConfig, - final PwmDomain pwmDomain - ) - { - final PwmPasswordPolicy defaultPolicy = PwmPasswordPolicy.defaultPolicy(); - final Map newPolicyMap = new HashMap<>( defaultPolicy.getPolicyMap() ); - - newPolicyMap.put( PwmPasswordRule.MaximumLength.getKey(), String.valueOf( effectiveConfig.getMaximumLength() ) ); - if ( effectiveConfig.getMinimumLength() > defaultPolicy.ruleHelper().readIntValue( PwmPasswordRule.MinimumLength ) ) - { - newPolicyMap.put( PwmPasswordRule.MinimumLength.getKey(), String.valueOf( effectiveConfig.getMinimumLength() ) ); - } - if ( effectiveConfig.getMaximumLength() < defaultPolicy.ruleHelper().readIntValue( PwmPasswordRule.MaximumLength ) ) - { - newPolicyMap.put( PwmPasswordRule.MaximumLength.getKey(), String.valueOf( effectiveConfig.getMaximumLength() ) ); - } - if ( effectiveConfig.getMinimumStrength() > defaultPolicy.ruleHelper().readIntValue( PwmPasswordRule.MinimumStrength ) ) - { - newPolicyMap.put( PwmPasswordRule.MinimumStrength.getKey(), String.valueOf( effectiveConfig.getMinimumStrength() ) ); - } - return PwmPasswordPolicy.createPwmPasswordPolicy( pwmDomain.getDomainID(), newPolicyMap ); - } - - private static MutatorResult passwordMutator( - final SessionLabel sessionLabel, - final PwmDomain pwmDomain, - final SeedMachine seedMachine, - final RandomGeneratorConfig effectiveConfig, - final PwmPasswordPolicy randomGenPolicy + private static MutatorResult mutatePassword( + final RandomGeneratorRequest request ) throws PwmUnrecoverableException { + final RandomGeneratorConfig effectiveConfig = request.randomGeneratorConfig(); + final PwmPasswordPolicy randomGenPolicy = request.randomGenPolicy(); - final int maxTryCount = effectiveConfig.getMaximumAttempts(); - final int jitterCount = effectiveConfig.getJitter(); - final PwmRandom pwmRandom = pwmDomain.getSecureService().pwmRandom(); + final int maxTryCount = effectiveConfig.maximumAttempts(); - final StringBuilder password = new StringBuilder(); - password.append( generateNewPassword( pwmRandom, seedMachine, effectiveConfig.getMinimumLength() ) ); + final PwmPasswordRuleValidator pwmPasswordRuleValidator = PwmPasswordRuleValidator.create( + request.sessionLabel(), request.pwmDomain(), randomGenPolicy, PwmPasswordRuleValidator.Flag.FailFast ); + final String newPassword = generateNewPassword( request ); - final PwmPasswordRuleValidator pwmPasswordRuleValidator = PwmPasswordRuleValidator.create( - sessionLabel, pwmDomain, randomGenPolicy, PwmPasswordRuleValidator.Flag.FailFast ); int tryCount = 0; boolean validPassword = false; + + final MutablePassword mutablePassword = new MutablePassword( request, request.randomGeneratorConfig().seedMachine(), request.pwmRandom(), newPassword ); + while ( !validPassword && tryCount < maxTryCount ) { tryCount++; validPassword = true; - if ( tryCount % jitterCount == 0 ) - { - password.delete( 0, password.length() ); - password.append( generateNewPassword( pwmRandom, seedMachine, effectiveConfig.getMinimumLength() ) ); - } - final List errors = pwmPasswordRuleValidator.internalPwmPolicyValidator( - password.toString(), null, null ); + mutablePassword.value(), null, null ); + if ( errors != null && !errors.isEmpty() ) { validPassword = false; - modifyPasswordBasedOnErrors( pwmRandom, password, errors, seedMachine ); + modifyPasswordBasedOnErrors( mutablePassword, errors ); } - else if ( checkPasswordAgainstDisallowedHttpValues( pwmDomain.getConfig(), password.toString() ) ) + else if ( checkPasswordAgainstDisallowedHttpValues( request.pwmDomain().getConfig(), mutablePassword.value() ) ) { validPassword = false; - password.delete( 0, password.length() ); - password.append( generateNewPassword( pwmRandom, seedMachine, effectiveConfig.getMinimumLength() ) ); + mutablePassword.reset( generateNewPassword( request ) ); } } - return new MutatorResult( password.toString(), validPassword, tryCount ); + return new MutatorResult( mutablePassword.value(), validPassword, tryCount ); } private static void modifyPasswordBasedOnErrors( - final PwmRandom pwmRandom, - final StringBuilder password, - final List errors, - final SeedMachine seedMachine + final MutablePassword mutablePassword, + final List errors ) { - if ( password == null || errors == null || errors.isEmpty() ) + if ( errors == null || errors.isEmpty() ) { return; } final Set errorMessages = EnumSet.noneOf( PwmError.class ); - for ( final ErrorInformation errorInfo : errors ) - { - errorMessages.add( errorInfo.getError() ); - } + errors.forEach( errorInfo -> errorMessages.add( errorInfo.getError() ) ); boolean touched = false; if ( errorMessages.contains( PwmError.PASSWORD_TOO_SHORT ) ) { - addRandChar( pwmRandom, password, seedMachine.getAllChars() ); + mutablePassword.addRandChar(); touched = true; } if ( errorMessages.contains( PwmError.PASSWORD_TOO_LONG ) ) { - password.deleteCharAt( pwmRandom.nextInt( password.length() ) ); - touched = true; - } - - if ( errorMessages.contains( PwmError.PASSWORD_FIRST_IS_NUMERIC ) || errorMessages.contains( PwmError.PASSWORD_FIRST_IS_SPECIAL ) ) - { - password.deleteCharAt( 0 ); - touched = true; - } - - if ( errorMessages.contains( PwmError.PASSWORD_LAST_IS_NUMERIC ) || errorMessages.contains( PwmError.PASSWORD_LAST_IS_SPECIAL ) ) - { - password.deleteCharAt( password.length() - 1 ); - touched = true; - } - - if ( errorMessages.contains( PwmError.PASSWORD_NOT_ENOUGH_NUM ) ) - { - addRandChar( pwmRandom, password, seedMachine.getNumChars() ); - touched = true; - } - - if ( errorMessages.contains( PwmError.PASSWORD_NOT_ENOUGH_SPECIAL ) ) - { - addRandChar( pwmRandom, password, seedMachine.getSpecialChars() ); - touched = true; - } - - if ( errorMessages.contains( PwmError.PASSWORD_NOT_ENOUGH_UPPER ) ) - { - addRandChar( pwmRandom, password, seedMachine.getUpperChars() ); + mutablePassword.deleteRandChar(); touched = true; } - if ( errorMessages.contains( PwmError.PASSWORD_NOT_ENOUGH_LOWER ) ) + if ( errorMessages.contains( PwmError.PASSWORD_FIRST_IS_NUMERIC ) + || errorMessages.contains( PwmError.PASSWORD_FIRST_IS_SPECIAL ) ) { - addRandChar( pwmRandom, password, seedMachine.getLowerChars() ); + mutablePassword.deleteFirstChar(); touched = true; } - PasswordCharCounter passwordCharCounter = new PasswordCharCounter( password.toString() ); - if ( errorMessages.contains( PwmError.PASSWORD_TOO_MANY_NUMERIC ) && passwordCharCounter.getNumericCharCount() > 0 ) + if ( errorMessages.contains( PwmError.PASSWORD_LAST_IS_NUMERIC ) + || errorMessages.contains( PwmError.PASSWORD_LAST_IS_SPECIAL ) ) { - deleteRandChar( pwmRandom, password, passwordCharCounter.getNumericChars() ); + mutablePassword.deleteLastChar(); touched = true; - passwordCharCounter = new PasswordCharCounter( password.toString() ); } - if ( errorMessages.contains( PwmError.PASSWORD_TOO_MANY_SPECIAL ) && passwordCharCounter.getSpecialCharsCount() > 0 ) + if ( errorMessages.contains( PwmError.PASSWORD_TOO_WEAK ) ) { - deleteRandChar( pwmRandom, password, passwordCharCounter.getSpecialChars() ); + mutablePassword.randomPasswordCharModifier(); touched = true; - passwordCharCounter = new PasswordCharCounter( password.toString() ); } - if ( errorMessages.contains( PwmError.PASSWORD_TOO_MANY_UPPER ) && passwordCharCounter.getUpperCharCount() > 0 ) + if ( checkForTooFewErrors( mutablePassword, errorMessages ) ) { - deleteRandChar( pwmRandom, password, passwordCharCounter.getUpperChars() ); touched = true; - passwordCharCounter = new PasswordCharCounter( password.toString() ); } - if ( errorMessages.contains( PwmError.PASSWORD_TOO_MANY_LOWER ) && passwordCharCounter.getLowerCharCount() > 0 ) + if ( checkForTooManyErrors( mutablePassword, errorMessages ) ) { - deleteRandChar( pwmRandom, password, passwordCharCounter.getLowerChars() ); - touched = true; - } - - if ( errorMessages.contains( PwmError.PASSWORD_TOO_WEAK ) ) - { - randomPasswordModifier( pwmRandom, password, seedMachine ); touched = true; } if ( !touched ) { - // dunno whats wrong, try just deleting a pwmRandom char, and hope a re-insert will add another. - randomPasswordModifier( pwmRandom, password, seedMachine ); + // dunno what is wrong, try just deleting a pwmRandom char, and hope a re-insert will add another. + mutablePassword.randomPasswordCharModifier(); } } - private static void deleteRandChar( - final PwmRandom pwmRandom, - final StringBuilder password, - final String charsToRemove + private static boolean checkForTooFewErrors( + final MutablePassword mutablePassword, + final Set errorMessages ) - throws ImpossiblePasswordPolicyException { - final List removePossibilities = new ArrayList<>(); - for ( int i = 0; i < password.length(); i++ ) + boolean touched = false; + + for ( final PasswordCharType passwordCharType : PasswordCharType.values() ) { - final char loopChar = password.charAt( i ); - final int index = charsToRemove.indexOf( loopChar ); - if ( index != -1 ) + final Optional tooFewError = passwordCharType.getTooFewError(); + if ( tooFewError.isPresent() && errorMessages.contains( tooFewError.get() ) ) { - removePossibilities.add( i ); + if ( mutablePassword.getPwmRandom().nextBoolean() ) + { + mutablePassword.deleteRandCharExceptType( passwordCharType ); + } + mutablePassword.addRandChar( passwordCharType ); + touched = true; } } - if ( removePossibilities.isEmpty() ) - { - throw new ImpossiblePasswordPolicyException( ImpossiblePasswordPolicyException.ErrorEnum.UNEXPECTED_ERROR ); - } - final Integer charToDelete = removePossibilities.get( pwmRandom.nextInt( removePossibilities.size() ) ); - password.deleteCharAt( charToDelete ); - } - private static void randomPasswordModifier( - final PwmRandom pwmRandom, - final StringBuilder password, - final SeedMachine seedMachine - ) - { - switch ( pwmRandom.nextInt( 6 ) ) - { - case 0: - case 1: - addRandChar( pwmRandom, password, seedMachine.getSpecialChars() ); - break; - case 2: - case 3: - addRandChar( pwmRandom, password, seedMachine.getNumChars() ); - break; - case 4: - addRandChar( pwmRandom, password, seedMachine.getUpperChars() ); - break; - case 5: - addRandChar( pwmRandom, password, seedMachine.getLowerChars() ); - break; - default: - switchRandomCase( pwmRandom, password ); - break; - } + return touched; } - private static void switchRandomCase( - final PwmRandom pwmRandom, - final StringBuilder password + private static boolean checkForTooManyErrors( + final MutablePassword mutablePassword, + final Set errorMessages ) { - for ( int i = 0; i < password.length(); i++ ) + boolean touched = false; + + for ( final PasswordCharType passwordCharType : PasswordCharType.values() ) { - final int randspot = pwmRandom.nextInt( password.length() ); - final char oldChar = password.charAt( randspot ); - if ( Character.isLetter( oldChar ) ) + final Optional tooManyError = passwordCharType.getTooManyError(); + if ( tooManyError.isPresent() && errorMessages.contains( tooManyError.get() ) ) { - final char newChar = Character.isUpperCase( oldChar ) ? Character.toLowerCase( oldChar ) : Character.toUpperCase( oldChar ); - password.deleteCharAt( randspot ); - password.insert( randspot, newChar ); - return; + final PasswordCharCounter passwordCharCounter = mutablePassword.getPasswordCharCounter(); + if ( passwordCharCounter.hasCharsOfType( passwordCharType ) ) + { + mutablePassword.deleteRandChar( passwordCharType ); + if ( mutablePassword.getPwmRandom().nextBoolean() ) + { + mutablePassword.addRandCharExceptType( passwordCharType ); + } + touched = true; + } } } - } - private static void addRandChar( final PwmRandom pwmRandom, final StringBuilder password, final String allowedChars ) - throws ImpossiblePasswordPolicyException - { - final int insertPosition = password.length() < 1 ? 0 : pwmRandom.nextInt( password.length() ); - addRandChar( pwmRandom, password, allowedChars, insertPosition ); - } - - private static void addRandChar( final PwmRandom pwmRandom, final StringBuilder password, final String allowedChars, final int insertPosition ) - throws ImpossiblePasswordPolicyException - { - if ( allowedChars.length() < 1 ) - { - throw new ImpossiblePasswordPolicyException( ImpossiblePasswordPolicyException.ErrorEnum.REQUIRED_CHAR_NOT_ALLOWED ); - } - else - { - final int newCharPosition = pwmRandom.nextInt( allowedChars.length() ); - final char charToAdd = allowedChars.charAt( newCharPosition ); - password.insert( insertPosition, charToAdd ); - } + return touched; } private static boolean checkPasswordAgainstDisallowedHttpValues( final DomainConfig config, final String password ) @@ -458,160 +293,50 @@ private static boolean checkPasswordAgainstDisallowedHttpValues( final DomainCon return false; } - private RandomPasswordGenerator( ) + private static String generateNewPassword( + final RandomGeneratorRequest request + ) { - } + final RandomGeneratorConfig randomGeneratorConfig = request.randomGeneratorConfig(); + final PwmRandom pwmRandom = request.pwmRandom(); + final SeedMachine seedMachine = request.randomGeneratorConfig().seedMachine(); - protected static class SeedMachine - { - private final Collection seeds; - private final PwmRandom pwmRandom; + final int effectiveLengthRange = randomGeneratorConfig.maximumLength() - randomGeneratorConfig.minimumLength(); + final int desiredLength = effectiveLengthRange > 1 + ? randomGeneratorConfig.minimumLength() + pwmRandom.nextInt( effectiveLengthRange ) + : randomGeneratorConfig.maximumLength(); - private String allChars; - private String numChars; - private String specialChars; - private String upperChars; - private String lowerChars; + final Map charTypeCounter = request.maxCharsPerType().entrySet() + .stream() + .filter( entry -> entry.getValue() > 0 ) + .collect( Collectors.toMap( + Map.Entry::getKey, + entry -> new MutableInt( entry.getValue() ) ) ); - public SeedMachine( final PwmRandom pwmRandom, final Collection seeds ) - { - this.pwmRandom = pwmRandom; - this.seeds = seeds; - } + final StringBuilder password = new StringBuilder( desiredLength ); - public String getRandomSeed( ) - { - return new ArrayList<>( seeds ).get( pwmRandom.nextInt( seeds.size() ) ); - } + // list copy of charTypeCounter.keySet() required because cannot pick random value from set/map. SequencedMap would be a better fit if it existed. + final List list = new ArrayList<>( charTypeCounter.keySet() ); - public String getAllChars( ) + while ( password.length() < desiredLength && !charTypeCounter.isEmpty() ) { - if ( allChars == null ) + final PasswordCharType type = list.get( pwmRandom.nextInt( list.size() ) ); + if ( charTypeCounter.get( type ).decrementAndGet() == 0 ) { - final StringBuilder sb = new StringBuilder(); - for ( final String s : seeds ) - { - for ( final Character c : s.toCharArray() ) - { - if ( sb.indexOf( c.toString() ) == -1 ) - { - sb.append( c ); - } - } - } - allChars = sb.length() > 2 ? sb.toString() : ( new SeedMachine( pwmRandom, DEFAULT_SEED_PHRASES ) ).getAllChars(); + charTypeCounter.remove( type ); + list.remove( type ); } - return allChars; + final String seedChars = seedMachine.charsOfType( type ); + final char nextChar = seedChars.charAt( pwmRandom.nextInt( seedChars.length() ) ); + password.append( nextChar ); } - public String getNumChars( ) + while ( password.length() < desiredLength ) { - if ( numChars == null ) - { - final StringBuilder sb = new StringBuilder(); - for ( final Character c : getAllChars().toCharArray() ) - { - if ( Character.isDigit( c ) ) - { - sb.append( c ); - } - } - numChars = sb.length() > 2 ? sb.toString() : ( new SeedMachine( pwmRandom, DEFAULT_SEED_PHRASES ) ).getNumChars(); - } - - return numChars; - } - - public String getSpecialChars( ) - { - if ( specialChars == null ) - { - final StringBuilder sb = new StringBuilder(); - for ( final Character c : getAllChars().toCharArray() ) - { - if ( !Character.isLetterOrDigit( c ) ) - { - sb.append( c ); - } - } - specialChars = sb.length() > 2 ? sb.toString() : ( new SeedMachine( pwmRandom, DEFAULT_SEED_PHRASES ) ).getSpecialChars(); - } - - return specialChars; - } - - public String getUpperChars( ) - { - if ( upperChars == null ) - { - final StringBuilder sb = new StringBuilder(); - for ( final Character c : getAllChars().toCharArray() ) - { - if ( Character.isUpperCase( c ) ) - { - sb.append( c ); - } - } - upperChars = sb.length() > 0 ? sb.toString() : ( new SeedMachine( pwmRandom, DEFAULT_SEED_PHRASES ) ).getUpperChars(); - } - return upperChars; - } - - public String getLowerChars( ) - { - if ( lowerChars == null ) - { - final StringBuilder sb = new StringBuilder(); - for ( final Character c : getAllChars().toCharArray() ) - { - if ( Character.isLowerCase( c ) ) - { - sb.append( c ); - } - } - lowerChars = sb.length() > 0 ? sb.toString() : ( new SeedMachine( pwmRandom, DEFAULT_SEED_PHRASES ) ).getLowerChars(); - } - - return lowerChars; - } - } - - private static String generateNewPassword( final PwmRandom pwmRandom, final SeedMachine seedMachine, final int desiredLength ) - { - final StringBuilder password = new StringBuilder(); - - while ( password.length() < ( desiredLength - 1 ) ) - { - //loop around until we're long enough password.append( seedMachine.getRandomSeed() ); } - if ( pwmRandom.nextInt( 3 ) == 0 ) - { - final SeedMachine defaultSeedMachine = new SeedMachine( pwmRandom, DEFAULT_SEED_PHRASES ); - addRandChar( pwmRandom, password, defaultSeedMachine.getNumChars(), pwmRandom.nextInt( password.length() ) ); - } - - if ( pwmRandom.nextBoolean() ) - { - switchRandomCase( pwmRandom, password ); - } - return password.toString(); } - - private static Collection normalizeSeeds( final Collection inputSeeds ) - { - if ( inputSeeds == null ) - { - return DEFAULT_SEED_PHRASES; - } - - final Collection newSeeds = new HashSet<>( inputSeeds ); - newSeeds.removeIf( s -> s == null || s.length() < 1 ); - - return newSeeds.isEmpty() ? DEFAULT_SEED_PHRASES : newSeeds; - } - } diff --git a/server/src/main/java/password/pwm/util/password/SeedMachine.java b/server/src/main/java/password/pwm/util/password/SeedMachine.java new file mode 100644 index 000000000..500a62a20 --- /dev/null +++ b/server/src/main/java/password/pwm/util/password/SeedMachine.java @@ -0,0 +1,146 @@ +/* + * Password Management Servlets (PWM) + * http://www.pwm-project.org + * + * Copyright (c) 2006-2009 Novell, Inc. + * Copyright (c) 2009-2021 The PWM Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package password.pwm.util.password; + +import password.pwm.util.java.CollectionUtil; +import password.pwm.util.secure.PwmRandom; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; +import java.util.stream.IntStream; + +class SeedMachine +{ + private static final List DEFAULT_SEED_PHRASES = makeDefaultSeedPhrases(); + private static final SeedMachine DEFAULT_SEED_MACHINE = create( PwmRandom.getInstance(), DEFAULT_SEED_PHRASES ); + + private final Collection seeds; + private final PwmRandom pwmRandom; + + private final Map cachedCharsOfType = new EnumMap<>( PasswordCharType.class ); + private final Map cachedCharsOfTypeException = new EnumMap<>( PasswordCharType.class ); + private final Supplier allChars = this::figureAllChars; + + private SeedMachine( final PwmRandom pwmRandom, final Collection seeds ) + { + this.pwmRandom = Objects.requireNonNull( pwmRandom ); + this.seeds = Objects.requireNonNull( seeds ); + } + + static SeedMachine defaultSeedMachine() + { + return DEFAULT_SEED_MACHINE; + } + + static SeedMachine create( final PwmRandom pwmRandom, final Collection seeds ) + { + final List normalizedSeeds = normalizeSeeds( seeds ); + return CollectionUtil.isEmpty( normalizedSeeds ) + ? DEFAULT_SEED_MACHINE + : new SeedMachine( pwmRandom, normalizedSeeds ); + } + + public String getRandomSeed() + { + return new ArrayList<>( seeds ).get( pwmRandom.nextInt( seeds.size() ) ); + } + + public String getAllChars() + { + return allChars.get(); + } + + private String figureAllChars() + { + final String sb = uniqueChars( seeds ); + return sb.length() > 2 ? sb.toString() : uniqueChars( DEFAULT_SEED_PHRASES ); + } + + public String charsExceptOfType( final PasswordCharType passwordCharType ) + { + return cachedCharsOfTypeException.computeIfAbsent( passwordCharType, passwordCharType1 -> + { + final String value = PasswordCharType.charsExceptOfType( getAllChars(), passwordCharType ); + return value.length() > 0 + ? value + : PasswordCharType.charsExceptOfType( getAllChars(), passwordCharType1 ); + } ); + } + + public String charsOfType( final PasswordCharType passwordCharType ) + { + return cachedCharsOfType.computeIfAbsent( passwordCharType, passwordCharType1 -> + { + final String value = PasswordCharType.charsOfType( getAllChars(), passwordCharType ); + return value.length() > 0 + ? value + : PasswordCharType.charsOfType( getAllChars(), passwordCharType1 ); + } ); + } + + private static String uniqueChars( final Collection input ) + { + if ( input == null || input.isEmpty() ) + { + return ""; + } + + final StringBuilder sb = new StringBuilder(); + for ( final String s : input ) + { + for ( final Character c : s.toCharArray() ) + { + if ( sb.indexOf( c.toString() ) == -1 ) + { + sb.append( c ); + } + } + } + return sb.toString(); + } + + private static List makeDefaultSeedPhrases() + { + final List asciiChars = IntStream.range( 33, 126 ) + .boxed() + .map( Character::toString ) + .toList(); + return List.copyOf( asciiChars ); + } + + private static List normalizeSeeds( final Collection inputSeeds ) + { + if ( inputSeeds == null ) + { + return DEFAULT_SEED_PHRASES; + } + + final List newSeeds = new ArrayList<>( inputSeeds ); + newSeeds.removeIf( s -> s == null || s.length() < 1 ); + + return newSeeds.isEmpty() ? DEFAULT_SEED_PHRASES : List.copyOf( newSeeds ); + } +} diff --git a/server/src/main/java/password/pwm/ws/server/rest/RestRandomPasswordServer.java b/server/src/main/java/password/pwm/ws/server/rest/RestRandomPasswordServer.java index 7521d151d..c033fcc95 100644 --- a/server/src/main/java/password/pwm/ws/server/rest/RestRandomPasswordServer.java +++ b/server/src/main/java/password/pwm/ws/server/rest/RestRandomPasswordServer.java @@ -40,7 +40,6 @@ import password.pwm.util.password.PasswordUtility; import password.pwm.util.password.RandomGeneratorConfig; import password.pwm.util.password.RandomGeneratorConfigRequest; -import password.pwm.util.password.RandomPasswordGenerator; import password.pwm.ws.server.RestMethodHandler; import password.pwm.ws.server.RestRequest; import password.pwm.ws.server.RestResultBean; @@ -213,7 +212,10 @@ private static JsonOutput doOperation( } final RandomGeneratorConfig randomConfig = jsonInputToRandomConfig( jsonInput, restRequest.getDomain(), pwmPasswordPolicy ); - final PasswordData randomPassword = RandomPasswordGenerator.createRandomPassword( restRequest.getSessionLabel(), randomConfig, restRequest.getDomain() ); + final PasswordData randomPassword = PasswordUtility.generateRandom( + restRequest.getSessionLabel(), + randomConfig, + restRequest.getDomain() ); final JsonOutput outputMap = new JsonOutput(); outputMap.password = randomPassword.getStringValue(); diff --git a/server/src/main/java/password/pwm/ws/server/rest/RestSetPasswordServer.java b/server/src/main/java/password/pwm/ws/server/rest/RestSetPasswordServer.java index bb31e7ec0..b4696e50d 100644 --- a/server/src/main/java/password/pwm/ws/server/rest/RestSetPasswordServer.java +++ b/server/src/main/java/password/pwm/ws/server/rest/RestSetPasswordServer.java @@ -40,7 +40,6 @@ import password.pwm.util.PasswordData; import password.pwm.util.logging.PwmLogger; import password.pwm.util.password.PasswordUtility; -import password.pwm.util.password.RandomPasswordGenerator; import password.pwm.ws.server.RestMethodHandler; import password.pwm.ws.server.RestRequest; import password.pwm.ws.server.RestResultBean; @@ -172,9 +171,10 @@ private static RestResultBean doSetPassword( restRequest.getSessionLabel(), targetUserIdentity.getUserIdentity(), targetUserIdentity.getChaiUser() ); - newPassword = RandomPasswordGenerator.createRandomPassword( + newPassword = PasswordUtility.generateRandom( restRequest.getSessionLabel(), - passwordPolicy, restRequest.getDomain() + passwordPolicy, + restRequest.getDomain() ); } else diff --git a/server/src/main/resources/password/pwm/AppProperty.properties b/server/src/main/resources/password/pwm/AppProperty.properties index 091fec53e..5b8312ce7 100644 --- a/server/src/main/resources/password/pwm/AppProperty.properties +++ b/server/src/main/resources/password/pwm/AppProperty.properties @@ -265,10 +265,9 @@ otp.qrImage.height=200 otp.qrImage.width=200 otp.encryptionAlg=AES password.randomGenerator.maxAttempts=2000 -password.randomGenerator.maxLength=1024 +password.randomGenerator.maxLength=10000 password.randomGenerator.minLength=12 password.randomGenerator.defaultStrength=50 -password.randomGenerator.jitter.count=50 password.strength.threshold.veryStrong=100 password.strength.threshold.strong=75 password.strength.threshold.good=45 diff --git a/server/src/test/java/password/pwm/util/password/RandomPasswordGeneratorTest.java b/server/src/test/java/password/pwm/util/password/RandomPasswordGeneratorTest.java index d8a56011c..485a93fcf 100644 --- a/server/src/test/java/password/pwm/util/password/RandomPasswordGeneratorTest.java +++ b/server/src/test/java/password/pwm/util/password/RandomPasswordGeneratorTest.java @@ -22,51 +22,230 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.ThrowingConsumer; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import password.pwm.PwmApplication; import password.pwm.PwmDomain; import password.pwm.bean.DomainID; import password.pwm.bean.SessionLabel; import password.pwm.config.profile.PwmPasswordPolicy; import password.pwm.config.profile.PwmPasswordRule; -import password.pwm.error.PwmUnrecoverableException; +import password.pwm.error.ErrorInformation; import password.pwm.util.PasswordData; import password.pwm.util.localdb.TestHelper; -import java.io.IOException; import java.nio.file.Path; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.IntStream; public class RandomPasswordGeneratorTest { + private static final int LOOP_COUNT = 1_000; + @TempDir public Path temporaryFolder; + @Test + public void specialCharsRulesTest() + throws Throwable + { + final int minSpecial = 33; + final int maxSpecial = 44; + final Map policyMap = new HashMap<>( PwmPasswordPolicy.defaultPolicy().getPolicyMap() ); + policyMap.put( PwmPasswordRule.AllowSpecial.getKey(), "true" ); + policyMap.put( PwmPasswordRule.MinimumSpecial.getKey(), String.valueOf( minSpecial ) ); + policyMap.put( PwmPasswordRule.MaximumSpecial.getKey(), String.valueOf( maxSpecial ) ); + + final ThrowingConsumer charTypeCheck = passwordString -> + { + final long specialCount = passwordString.chars().filter( v -> !Character.isLetterOrDigit( v ) ).count(); + if ( specialCount < minSpecial || specialCount > maxSpecial ) + { + Assertions.fail( () -> "generated password has incorrect special char count: " + specialCount + "; password: " + passwordString ); + } + }; + + generalPolicyTester( policyMap, List.of( charTypeCheck, new DupeValueChecker() ) ); + } @Test - public void generateRandomPasswordsTest() - throws PwmUnrecoverableException, IOException + public void numericPolicyTest() + throws Throwable { - final PwmApplication pwmApplication = TestHelper.makeTestPwmApplication( temporaryFolder ); - final PwmDomain pwmDomain = pwmApplication.domains().get( DomainID.DOMAIN_ID_DEFAULT ); + final int minNumeric = 33; + final int maxNumeric = 44; final Map policyMap = new HashMap<>( PwmPasswordPolicy.defaultPolicy().getPolicyMap() ); policyMap.put( PwmPasswordRule.AllowNumeric.getKey(), "true" ); - final PwmPasswordPolicy pwmPasswordPolicy = PwmPasswordPolicy.createPwmPasswordPolicy( PwmPasswordPolicy.defaultPolicy().getDomainID(), policyMap ); + policyMap.put( PwmPasswordRule.MinimumNumeric.getKey(), String.valueOf( minNumeric ) ); + policyMap.put( PwmPasswordRule.MaximumNumeric.getKey(), String.valueOf( maxNumeric ) ); + + final ThrowingConsumer charTypeCheck = passwordString -> + { + final long numericCount = passwordString.chars().filter( Character::isDigit ).count(); + if ( numericCount < minNumeric || numericCount > maxNumeric ) + { + Assertions.fail( () -> "generated password has incorrect numeric char count: " + numericCount + "; password: " + passwordString ); + } + }; + + generalPolicyTester( policyMap, List.of( charTypeCheck, new DupeValueChecker() ) ); + } + + @ParameterizedTest + @ValueSource( ints = { 10, 20, 50, 100, 150, 500, 1000, 2000, 5000 } ) + public void testLargePasswordSizes( final int minimumLength ) + throws Throwable + { + final int maxLength = minimumLength + 10; + + final Map policyMap = new HashMap<>( PwmPasswordPolicy.defaultPolicy().getPolicyMap() ); + policyMap.put( PwmPasswordRule.AllowNumeric.getKey(), "true" ); + policyMap.put( PwmPasswordRule.MinimumLength.getKey(), Integer.toString( minimumLength ) ); + policyMap.put( PwmPasswordRule.MaximumLength.getKey(), Integer.toString( maxLength ) ); + + generalPolicyTester( policyMap, List.of( new DupeValueChecker() ) ); + } + + @ParameterizedTest + @ValueSource( ints = { 1, 2, 3, 4, 5, 6 } ) + public void testSmolPasswordSizes( final int maxLength ) + throws Throwable + { + final int minLength = maxLength - 1; + + final Map policyMap = new HashMap<>( PwmPasswordPolicy.defaultPolicy().getPolicyMap() ); + policyMap.put( PwmPasswordRule.AllowNumeric.getKey(), "true" ); + policyMap.put( PwmPasswordRule.MinimumLength.getKey(), Integer.toString( minLength ) ); + policyMap.put( PwmPasswordRule.MaximumLength.getKey(), Integer.toString( maxLength ) ); + + generalPolicyTester( policyMap, List.of() ); + } - final int loopCount = 1_000; - final Set seenValues = new HashSet<>(); - for ( int i = 0; i < loopCount; i++ ) + @Test + public void testFixedPasswordSizes( ) + throws Throwable + { + final int[] lengths = IntStream.range( 1, 100 ).toArray(); + + for ( final int length : lengths ) + { + final Map policyMap = new HashMap<>( PwmPasswordPolicy.defaultPolicy().getPolicyMap() ); + policyMap.put( PwmPasswordRule.AllowNumeric.getKey(), "true" ); + policyMap.put( PwmPasswordRule.MinimumLength.getKey(), Integer.toString( length ) ); + policyMap.put( PwmPasswordRule.MaximumLength.getKey(), Integer.toString( length ) ); + + final ThrowingConsumer lengthCheck = passwordString -> + { + final long numericCount = passwordString.length(); + if ( numericCount < length || numericCount > length ) + { + Assertions.fail( () -> "generated password has incorrect char count: expected=" + + length + ", actual=" + + numericCount + "; password: " + passwordString ); + } + }; + generalPolicyTester( policyMap, List.of( lengthCheck ) ); + } + } + + @Test + public void policy1Test() + throws Throwable + { + final Map policyMap = new HashMap<>( PwmPasswordPolicy.defaultPolicy().getPolicyMap() ); + policyMap.put( "chai.pwrule.ADComplexityMaxViolation", "2" ); + policyMap.put( "chai.pwrule.caseSensitive", "true" ); + policyMap.put( "chai.pwrule.changeMessage", "" ); + policyMap.put( "chai.pwrule.disallowedAttributes", "cn\ngivenName\nsn" ); + policyMap.put( "chai.pwrule.disallowedValues", "password\ntest" ); + policyMap.put( "chai.pwrule.expirationInterval", "2592000" ); + policyMap.put( "chai.pwrule.length.max", "64" ); + policyMap.put( "chai.pwrule.length.min", "2" ); + policyMap.put( "chai.pwrule.lifetime.minimum", "0" ); + policyMap.put( "chai.pwrule.lower.max", "0" ); + policyMap.put( "chai.pwrule.lower.min", "0" ); + policyMap.put( "chai.pwrule.numeric.allow", "true" ); + policyMap.put( "chai.pwrule.numeric.allowFirst", "true" ); + policyMap.put( "chai.pwrule.numeric.allowLast", "true" ); + policyMap.put( "chai.pwrule.numeric.max", "0" ); + policyMap.put( "chai.pwrule.numeric.min", "0" ); + policyMap.put( "chai.pwrule.policyEnabled", "true" ); + policyMap.put( "chai.pwrule.repeat.max", "0" ); + policyMap.put( "chai.pwrule.sequentialRepeat.max", "0" ); + policyMap.put( "chai.pwrule.special.allow", "true" ); + policyMap.put( "chai.pwrule.special.allowFirst", "true" ); + policyMap.put( "chai.pwrule.special.allowLast", "true" ); + policyMap.put( "chai.pwrule.special.max", "0" ); + policyMap.put( "chai.pwrule.special.min", "0" ); + policyMap.put( "chai.pwrule.unique.min", "0" ); + policyMap.put( "chai.pwrule.uniqueRequired", "false" ); + policyMap.put( "chai.pwrule.upper.max", "0" ); + policyMap.put( "chai.pwrule.upper.min", "0" ); + policyMap.put( "password.policy.ADComplexityLevel", "NONE" ); + policyMap.put( "password.policy.allowMacroInRegexSetting", "true" ); + policyMap.put( "password.policy.allowNonAlpha", "true" ); + policyMap.put( "password.policy.charGroup.minimumMatch", "0" ); + policyMap.put( "password.policy.charGroup.regExValues", ".*[0-9]\n.*[a-z]\n.*[A-Z]\n.*[^A-Za-z0-9]" ); + policyMap.put( "password.policy.checkWordlist", "false" ); + policyMap.put( "password.policy.disallowCurrent", "false" ); + policyMap.put( "password.policy.maximumAlpha", "0" ); + policyMap.put( "password.policy.maximumConsecutive", "0" ); + policyMap.put( "password.policy.maximumNonAlpha", "0" ); + policyMap.put( "password.policy.minimumAlpha", "0" ); + policyMap.put( "password.policy.minimumNonAlpha", "0" ); + policyMap.put( "password.policy.minimumStrength", "0" ); + policyMap.put( "password.policy.regExMatch", "" ); + policyMap.put( "password.policy.regExNoMatch", "" ); + generalPolicyTester( policyMap, List.of( new DupeValueChecker() ) ); + } + + + private void generalPolicyTester( final Map policyMap, final List> extraChecks ) + throws Throwable + { + final PwmApplication pwmApplication = TestHelper.makeTestPwmApplication( temporaryFolder ); + final PwmDomain pwmDomain = pwmApplication.domains().get( DomainID.DOMAIN_ID_DEFAULT ); + + final PwmPasswordPolicy pwmPasswordPolicy = PwmPasswordPolicy.createPwmPasswordPolicy( + PwmPasswordPolicy.defaultPolicy().getDomainID(), + policyMap ); + + + for ( int i = 0; i < LOOP_COUNT; i++ ) { - final PasswordData passwordData = RandomPasswordGenerator.createRandomPassword( + final PasswordData passwordData = PasswordUtility.generateRandom( SessionLabel.TEST_SESSION_LABEL, pwmPasswordPolicy, pwmDomain ); final String passwordString = passwordData.getStringValue(); + + final List errors = PasswordRuleChecks.extendedPolicyRuleChecker( SessionLabel.TEST_SESSION_LABEL, pwmDomain, + pwmPasswordPolicy, passwordString, null, null, PwmPasswordRuleValidator.Flag.FailFast ); + Assertions.assertTrue( errors.isEmpty(), () -> "random generated rule failed validation check: " + errors.get( 0 ).toDebugStr() ); + + for ( final ThrowingConsumer extraCheck : extraChecks ) + { + extraCheck.accept( passwordString ); + } + } + } + + private static class DupeValueChecker implements ThrowingConsumer + { + final Set seenValues = new HashSet<>(); + + @Override + public void accept( final String passwordString ) + throws Throwable + { if ( seenValues.contains( passwordString ) ) { Assertions.fail( "repeated random generated password" ); diff --git a/webapp/src/main/webapp/public/resources/js/changepassword.js b/webapp/src/main/webapp/public/resources/js/changepassword.js index 713c6d236..42c306a50 100644 --- a/webapp/src/main/webapp/public/resources/js/changepassword.js +++ b/webapp/src/main/webapp/public/resources/js/changepassword.js @@ -237,25 +237,22 @@ PWM_CHANGEPW.doRandomGeneration=function(randomConfig) { dialogBody += "

"; dialogBody += ''; - for (let i = 0; i < 20; i++) { - dialogBody += ''; - for (let j = 0; j < 2; j++) { - i = i + j; - (function(index) { - const elementID = "randomGen" + index; - dialogBody += ''; - eventHandlers.push(function(){ - PWM_MAIN.addEventHandler(elementID,'click',function(){ - const value = PWM_MAIN.getObject(elementID).innerHTML; - const parser = new DOMParser(); - const dom = parser.parseFromString(value, 'text/html'); - const domString = dom.body.textContent; - finishAction(domString); - }); + for (let i = 0; i < 10; i++) { + (function(index) { + const elementID = "randomGen" + index; + dialogBody += ''; + dialogBody += ''; + eventHandlers.push(function(){ + PWM_MAIN.addEventHandler(elementID,'click',function(){ + const value = PWM_MAIN.getObject(elementID).innerHTML; + const parser = new DOMParser(); + const dom = parser.parseFromString(value, 'text/html'); + const domString = dom.body.textContent; + finishAction(domString); }); - })(i); - } - dialogBody += ''; + }); + dialogBody += ''; + })(i); } dialogBody += "


"; @@ -279,7 +276,6 @@ PWM_CHANGEPW.doRandomGeneration=function(randomConfig) { const titleString = randomConfig['title'] ? randomConfig['title'] : PWM_MAIN.showString('Title_RandomPasswords'); PWM_MAIN.showDialog({ title:titleString, - dialogClass:'narrow', text:dialogBody, showOk:false, showClose:true,