Browse Source

issue #701 - address bugs and limitations of random pw generator, improve performance and rule compatability

Jason Rivard 2 years ago
parent
commit
5df623c70e
26 changed files with 1282 additions and 722 deletions
  1. 0 1
      server/src/main/java/password/pwm/AppProperty.java
  2. 1 2
      server/src/main/java/password/pwm/health/LDAPHealthChecker.java
  3. 3 4
      server/src/main/java/password/pwm/http/servlet/GuestRegistrationServlet.java
  4. 1 2
      server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java
  5. 6 3
      server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java
  6. 2 4
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java
  7. 2 3
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java
  8. 5 6
      server/src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java
  9. 6 1
      server/src/main/java/password/pwm/ldap/LdapDebugDataGenerator.java
  10. 1 2
      server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java
  11. 220 0
      server/src/main/java/password/pwm/util/password/MutablePassword.java
  12. 21 145
      server/src/main/java/password/pwm/util/password/PasswordCharCounter.java
  13. 190 0
      server/src/main/java/password/pwm/util/password/PasswordCharType.java
  14. 11 11
      server/src/main/java/password/pwm/util/password/PasswordRuleChecks.java
  15. 55 24
      server/src/main/java/password/pwm/util/password/PasswordUtility.java
  16. 12 11
      server/src/main/java/password/pwm/util/password/PwmPasswordAdRuleUtil.java
  17. 110 57
      server/src/main/java/password/pwm/util/password/RandomGeneratorConfig.java
  18. 3 3
      server/src/main/java/password/pwm/util/password/RandomGeneratorConfigRequest.java
  19. 143 0
      server/src/main/java/password/pwm/util/password/RandomGeneratorRequest.java
  20. 131 406
      server/src/main/java/password/pwm/util/password/RandomPasswordGenerator.java
  21. 146 0
      server/src/main/java/password/pwm/util/password/SeedMachine.java
  22. 4 2
      server/src/main/java/password/pwm/ws/server/rest/RestRandomPasswordServer.java
  23. 3 3
      server/src/main/java/password/pwm/ws/server/rest/RestSetPasswordServer.java
  24. 1 2
      server/src/main/resources/password/pwm/AppProperty.properties
  25. 190 11
      server/src/test/java/password/pwm/util/password/RandomPasswordGeneratorTest.java
  26. 15 19
      webapp/src/main/webapp/public/resources/js/changepassword.js

+ 0 - 1
server/src/main/java/password/pwm/AppProperty.java

@@ -290,7 +290,6 @@ public enum AppProperty
     PASSWORD_RANDOMGEN_MAX_LENGTH                   ( "password.randomGenerator.maxLength" ),
     PASSWORD_RANDOMGEN_MAX_LENGTH                   ( "password.randomGenerator.maxLength" ),
     PASSWORD_RANDOMGEN_MIN_LENGTH                   ( "password.randomGenerator.minLength" ),
     PASSWORD_RANDOMGEN_MIN_LENGTH                   ( "password.randomGenerator.minLength" ),
     PASSWORD_RANDOMGEN_DEFAULT_STRENGTH             ( "password.randomGenerator.defaultStrength" ),
     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) */
     /* 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" ),
     PASSWORD_STRENGTH_THRESHOLD_VERY_STRONG         ( "password.strength.threshold.veryStrong" ),

+ 1 - 2
server/src/main/java/password/pwm/health/LDAPHealthChecker.java

@@ -66,7 +66,6 @@ import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.password.PasswordUtility;
-import password.pwm.util.password.RandomPasswordGenerator;
 import password.pwm.ws.server.rest.bean.PublicHealthData;
 import password.pwm.ws.server.rest.bean.PublicHealthData;
 
 
 import java.net.InetAddress;
 import java.net.InetAddress;
@@ -357,7 +356,7 @@ public class LDAPHealthChecker implements HealthSupplier
                     }
                     }
                     if ( doPasswordChange )
                     if ( doPasswordChange )
                     {
                     {
-                        final PasswordData newPassword = RandomPasswordGenerator.createRandomPassword( null, passwordPolicy, pwmDomain );
+                        final PasswordData newPassword = PasswordUtility.generateRandom( sessionLabel, passwordPolicy, pwmDomain );
                         try
                         try
                         {
                         {
                             theUser.setPassword( newPassword.getStringValue() );
                             theUser.setPassword( newPassword.getStringValue() );

+ 3 - 4
server/src/main/java/password/pwm/http/servlet/GuestRegistrationServlet.java

@@ -48,22 +48,21 @@ import password.pwm.http.PwmSession;
 import password.pwm.http.bean.GuestRegistrationBean;
 import password.pwm.http.bean.GuestRegistrationBean;
 import password.pwm.i18n.Message;
 import password.pwm.i18n.Message;
 import password.pwm.ldap.LdapOperationsHelper;
 import password.pwm.ldap.LdapOperationsHelper;
-import password.pwm.user.UserInfo;
 import password.pwm.ldap.UserInfoFactory;
 import password.pwm.ldap.UserInfoFactory;
 import password.pwm.ldap.search.SearchConfiguration;
 import password.pwm.ldap.search.SearchConfiguration;
 import password.pwm.ldap.search.UserSearchService;
 import password.pwm.ldap.search.UserSearchService;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsClient;
 import password.pwm.svc.stats.StatisticsClient;
+import password.pwm.user.UserInfo;
 import password.pwm.util.FormMap;
 import password.pwm.util.FormMap;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PasswordData;
 import password.pwm.util.form.FormUtility;
 import password.pwm.util.form.FormUtility;
-import password.pwm.util.java.PwmUtil;
 import password.pwm.util.java.PwmDateFormat;
 import password.pwm.util.java.PwmDateFormat;
+import password.pwm.util.java.PwmUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.operations.ActionExecutor;
 import password.pwm.util.operations.ActionExecutor;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.password.PasswordUtility;
-import password.pwm.util.password.RandomPasswordGenerator;
 
 
 import javax.servlet.ServletException;
 import javax.servlet.ServletException;
 import javax.servlet.annotation.WebServlet;
 import javax.servlet.annotation.WebServlet;
@@ -478,7 +477,7 @@ public class GuestRegistrationServlet extends ControlledPwmServlet
                     userIdentity,
                     userIdentity,
                     theUser );
                     theUser );
 
 
-            final PasswordData newPassword = RandomPasswordGenerator.createRandomPassword( pwmRequest.getLabel(), passwordPolicy, pwmDomain );
+            final PasswordData newPassword = PasswordUtility.generateRandom( pwmRequest.getLabel(), passwordPolicy, pwmDomain );
             theUser.setPassword( newPassword.getStringValue() );
             theUser.setPassword( newPassword.getStringValue() );
 
 
 
 

+ 1 - 2
server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java

@@ -64,7 +64,6 @@ import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.password.PwmPasswordRuleValidator;
 import password.pwm.util.password.PwmPasswordRuleValidator;
-import password.pwm.util.password.RandomPasswordGenerator;
 import password.pwm.ws.server.RestResultBean;
 import password.pwm.ws.server.RestResultBean;
 import password.pwm.ws.server.rest.RestCheckPasswordServer;
 import password.pwm.ws.server.rest.RestCheckPasswordServer;
 import password.pwm.ws.server.rest.RestRandomPasswordServer;
 import password.pwm.ws.server.rest.RestRandomPasswordServer;
@@ -458,7 +457,7 @@ public abstract class ChangePasswordServlet extends ControlledPwmServlet
     @ActionHandler( action = "randomPassword" )
     @ActionHandler( action = "randomPassword" )
     public ProcessStatus processRandomPasswordAction( final PwmRequest pwmRequest ) throws IOException, PwmUnrecoverableException, ChaiUnavailableException
     public ProcessStatus processRandomPasswordAction( final PwmRequest pwmRequest ) throws IOException, PwmUnrecoverableException, ChaiUnavailableException
     {
     {
-        final PasswordData passwordData = RandomPasswordGenerator.createRandomPassword(
+        final PasswordData passwordData = PasswordUtility.generateRandom(
                 pwmRequest.getLabel(),
                 pwmRequest.getLabel(),
                 pwmRequest.getPwmSession().getUserInfo().getPasswordPolicy(),
                 pwmRequest.getPwmSession().getUserInfo().getPasswordPolicy(),
                 pwmRequest.getPwmDomain() );
                 pwmRequest.getPwmDomain() );

+ 6 - 3
server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java

@@ -62,12 +62,12 @@ import password.pwm.http.bean.ConfigManagerBean;
 import password.pwm.http.servlet.AbstractPwmServlet;
 import password.pwm.http.servlet.AbstractPwmServlet;
 import password.pwm.http.servlet.ControlledPwmServlet;
 import password.pwm.http.servlet.ControlledPwmServlet;
 import password.pwm.http.servlet.PwmServletDefinition;
 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.NavTreeDataMaker;
 import password.pwm.http.servlet.configeditor.data.NavTreeItem;
 import password.pwm.http.servlet.configeditor.data.NavTreeItem;
 import password.pwm.http.servlet.configeditor.data.NavTreeSettings;
 import password.pwm.http.servlet.configeditor.data.NavTreeSettings;
 import password.pwm.http.servlet.configeditor.data.SettingData;
 import password.pwm.http.servlet.configeditor.data.SettingData;
 import password.pwm.http.servlet.configeditor.data.SettingDataMaker;
 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.Config;
 import password.pwm.i18n.Message;
 import password.pwm.i18n.Message;
 import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.i18n.PwmLocaleBundle;
@@ -85,8 +85,8 @@ import password.pwm.util.java.TimeDuration;
 import password.pwm.util.json.JsonFactory;
 import password.pwm.util.json.JsonFactory;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.macro.MacroRequest;
+import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.password.RandomGeneratorConfig;
 import password.pwm.util.password.RandomGeneratorConfig;
-import password.pwm.util.password.RandomPasswordGenerator;
 import password.pwm.ws.server.RestResultBean;
 import password.pwm.ws.server.RestResultBean;
 import password.pwm.ws.server.rest.RestRandomPasswordServer;
 import password.pwm.ws.server.rest.RestRandomPasswordServer;
 import password.pwm.ws.server.rest.bean.PublicHealthData;
 import password.pwm.ws.server.rest.bean.PublicHealthData;
@@ -934,7 +934,10 @@ public class ConfigEditorServlet extends ControlledPwmServlet
     {
     {
         final RestRandomPasswordServer.JsonInput jsonInput = pwmRequest.readBodyAsJsonObject( RestRandomPasswordServer.JsonInput.class );
         final RestRandomPasswordServer.JsonInput jsonInput = pwmRequest.readBodyAsJsonObject( RestRandomPasswordServer.JsonInput.class );
         final RandomGeneratorConfig randomConfig = RestRandomPasswordServer.jsonInputToRandomConfig( jsonInput, pwmRequest.getPwmDomain(), PwmPasswordPolicy.defaultPolicy() );
         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();
         final RestRandomPasswordServer.JsonOutput outputMap = new RestRandomPasswordServer.JsonOutput();
         outputMap.setPassword( randomPassword.getStringValue() );
         outputMap.setPassword( randomPassword.getStringValue() );
 
 

+ 2 - 4
server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java

@@ -76,7 +76,6 @@ import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.password.PasswordUtility;
-import password.pwm.util.password.RandomPasswordGenerator;
 
 
 import javax.servlet.ServletException;
 import javax.servlet.ServletException;
 import java.io.IOException;
 import java.io.IOException;
@@ -465,11 +464,10 @@ public class ForgottenPasswordUtil
                     + theUser.getEntryDN() );
                     + theUser.getEntryDN() );
 
 
             // create new password
             // create new password
-            final PasswordData newPassword = RandomPasswordGenerator.createRandomPassword(
+            final PasswordData newPassword = PasswordUtility.generateRandom(
                     pwmRequest.getLabel(),
                     pwmRequest.getLabel(),
                     userInfo.getPasswordPolicy(),
                     userInfo.getPasswordPolicy(),
-                    pwmDomain
-            );
+                    pwmDomain );
             LOGGER.trace( pwmRequest, () -> "generated random password value based on password policy for "
             LOGGER.trace( pwmRequest, () -> "generated random password value based on password policy for "
                     + userIdentity.toDisplayString() );
                     + userIdentity.toDisplayString() );
 
 

+ 2 - 3
server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java

@@ -92,7 +92,6 @@ import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.operations.ActionExecutor;
 import password.pwm.util.operations.ActionExecutor;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.password.RandomGeneratorConfig;
 import password.pwm.util.password.RandomGeneratorConfig;
-import password.pwm.util.password.RandomPasswordGenerator;
 import password.pwm.ws.server.RestResultBean;
 import password.pwm.ws.server.RestResultBean;
 import password.pwm.ws.server.rest.RestCheckPasswordServer;
 import password.pwm.ws.server.rest.RestCheckPasswordServer;
 import password.pwm.ws.server.rest.RestRandomPasswordServer;
 import password.pwm.ws.server.rest.RestRandomPasswordServer;
@@ -1276,7 +1275,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
                     pwmRequest.getLabel(),
                     pwmRequest.getLabel(),
                     userIdentity,
                     userIdentity,
                     chaiUser );
                     chaiUser );
-            newPassword = RandomPasswordGenerator.createRandomPassword(
+            newPassword = PasswordUtility.generateRandom(
                     pwmRequest.getLabel(),
                     pwmRequest.getLabel(),
                     passwordPolicy,
                     passwordPolicy,
                     pwmRequest.getPwmDomain()
                     pwmRequest.getPwmDomain()
@@ -1336,7 +1335,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
         );
         );
 
 
         final RandomGeneratorConfig randomConfig = RandomGeneratorConfig.make( pwmRequest.getPwmDomain(), userInfo.getPasswordPolicy() );
         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();
         final RestRandomPasswordServer.JsonOutput jsonOutput = new RestRandomPasswordServer.JsonOutput();
         jsonOutput.setPassword( randomPassword.getStringValue() );
         jsonOutput.setPassword( randomPassword.getStringValue() );
 
 

+ 5 - 6
server/src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java

@@ -55,8 +55,6 @@ import password.pwm.http.PwmRequestAttribute;
 import password.pwm.http.PwmSession;
 import password.pwm.http.PwmSession;
 import password.pwm.http.bean.NewUserBean;
 import password.pwm.http.bean.NewUserBean;
 import password.pwm.http.servlet.forgottenpw.RemoteVerificationMethod;
 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.PwmAuthenticationSource;
 import password.pwm.ldap.auth.SessionAuthenticator;
 import password.pwm.ldap.auth.SessionAuthenticator;
 import password.pwm.ldap.search.SearchConfiguration;
 import password.pwm.ldap.search.SearchConfiguration;
@@ -67,20 +65,21 @@ import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsClient;
 import password.pwm.svc.stats.StatisticsClient;
 import password.pwm.svc.token.TokenType;
 import password.pwm.svc.token.TokenType;
 import password.pwm.svc.token.TokenUtil;
 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.PasswordData;
 import password.pwm.util.form.FormUtility;
 import password.pwm.util.form.FormUtility;
 import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.PwmUtil;
 import password.pwm.util.java.PwmUtil;
-import password.pwm.util.json.JsonFactory;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
+import password.pwm.util.json.JsonFactory;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroReplacer;
 import password.pwm.util.macro.MacroReplacer;
 import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.operations.ActionExecutor;
 import password.pwm.util.operations.ActionExecutor;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.password.RandomGeneratorConfig;
 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.FormDataRequestBean;
 import password.pwm.ws.client.rest.form.FormDataResponseBean;
 import password.pwm.ws.client.rest.form.FormDataResponseBean;
 import password.pwm.ws.client.rest.form.RestFormDataClient;
 import password.pwm.ws.client.rest.form.RestFormDataClient;
@@ -161,7 +160,7 @@ class NewUserUtils
         else
         else
         {
         {
             final PwmPasswordPolicy pwmPasswordPolicy = newUserProfile.getNewUserPasswordPolicy( pwmRequest.getPwmRequestContext() );
             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
         // set up the user creation attributes
@@ -216,7 +215,7 @@ class NewUserUtils
                 final RandomGeneratorConfig randomGeneratorConfig = RandomGeneratorConfig.make( pwmRequest.getPwmDomain(),
                 final RandomGeneratorConfig randomGeneratorConfig = RandomGeneratorConfig.make( pwmRequest.getPwmDomain(),
                          newUserProfile.getNewUserPasswordPolicy( pwmRequest.getPwmRequestContext() ) );
                          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 );
             final ChaiUser proxiedUser = chaiProvider.getEntryFactory().newChaiUser( newUserDN );
             try
             try

+ 6 - 1
server/src/main/java/password/pwm/ldap/LdapDebugDataGenerator.java

@@ -66,7 +66,10 @@ public class LdapDebugDataGenerator
 
 
             try
             try
             {
             {
-                final ChaiConfiguration profileChaiConf = LdapOperationsHelper.createChaiConfiguration( domainConfig, ldapProfile );
+                final DomainConfig nonObfuscatedDomainConf = pwmDomain.getConfig();
+                final ChaiConfiguration profileChaiConf = LdapOperationsHelper.createChaiConfiguration(
+                        nonObfuscatedDomainConf,
+                        ldapProfile );
                 final Collection<ChaiConfiguration> chaiConfigurations = ChaiUtility.splitConfigurationPerReplica( profileChaiConf, null );
                 final Collection<ChaiConfiguration> chaiConfigurations = ChaiUtility.splitConfigurationPerReplica( profileChaiConf, null );
 
 
                 for ( final ChaiConfiguration chaiConfiguration : chaiConfigurations )
                 for ( final ChaiConfiguration chaiConfiguration : chaiConfigurations )
@@ -120,6 +123,7 @@ public class LdapDebugDataGenerator
         final LdapDebugServerInfo.LdapDebugServerInfoBuilder builder = LdapDebugServerInfo.builder();
         final LdapDebugServerInfo.LdapDebugServerInfoBuilder builder = LdapDebugServerInfo.builder();
 
 
         builder.ldapServerlUrl( chaiConfiguration.getSetting( ChaiSetting.BIND_URLS ) );
         builder.ldapServerlUrl( chaiConfiguration.getSetting( ChaiSetting.BIND_URLS ) );
+        builder.vendorName( chaiProvider.getDirectoryVendor().name() );
         final ChaiProvider loopProvider = chaiProvider.getProviderFactory().newProvider( chaiConfiguration );
         final ChaiProvider loopProvider = chaiProvider.getProviderFactory().newProvider( chaiConfiguration );
 
 
         {
         {
@@ -188,6 +192,7 @@ public class LdapDebugDataGenerator
     public static class LdapDebugServerInfo
     public static class LdapDebugServerInfo
     {
     {
         private String ldapServerlUrl;
         private String ldapServerlUrl;
+        private String vendorName;
         private String testUserDN;
         private String testUserDN;
         private Map<String, List<String>> testUserAttributes;
         private Map<String, List<String>> testUserAttributes;
         private String proxyDN;
         private String proxyDN;

+ 1 - 2
server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java

@@ -60,7 +60,6 @@ import password.pwm.util.logging.PwmLogLevel;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.password.RandomGeneratorConfig;
 import password.pwm.util.password.RandomGeneratorConfig;
-import password.pwm.util.password.RandomPasswordGenerator;
 
 
 import java.time.Instant;
 import java.time.Instant;
 import java.util.Collections;
 import java.util.Collections;
@@ -485,7 +484,7 @@ class LDAPAuthenticationRequest implements AuthenticationRequest
             // create random password for user
             // create random password for user
             final RandomGeneratorConfig randomGeneratorConfig = RandomGeneratorConfig.make( pwmDomain, passwordPolicy );
             final RandomGeneratorConfig randomGeneratorConfig = RandomGeneratorConfig.make( pwmDomain, passwordPolicy );
 
 
-            final PasswordData currentPass = RandomPasswordGenerator.createRandomPassword( sessionLabel, randomGeneratorConfig, pwmDomain );
+            final PasswordData currentPass = PasswordUtility.generateRandom( sessionLabel, randomGeneratorConfig, pwmDomain );
 
 
             try
             try
             {
             {

+ 220 - 0
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<PasswordCharType> 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<PasswordCharType> 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<PasswordCharType> 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<Integer> 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();
+        }
+    }
+
+}

+ 21 - 145
server/src/main/java/password/pwm/util/password/PasswordCharCounter.java

@@ -20,75 +20,34 @@
 
 
 package password.pwm.util.password;
 package password.pwm.util.password;
 
 
-public class PasswordCharCounter
+import java.util.EnumMap;
+import java.util.Map;
+
+class PasswordCharCounter
 {
 {
     private final String password;
     private final String password;
     private final int passwordLength;
     private final int passwordLength;
+    private final Map<PasswordCharType, String> cache = new EnumMap<>( PasswordCharType.class );
 
 
-    public PasswordCharCounter( final String password )
+    PasswordCharCounter( final String password )
     {
     {
         this.password = password;
         this.password = password;
         this.passwordLength = password.length();
         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( )
     public int getRepeatedChars( )
@@ -145,7 +104,7 @@ public class PasswordCharCounter
         return numberOfRepeats;
         return numberOfRepeats;
     }
     }
 
 
-    public int getSequentialNumericChars( )
+    public int sequentialCharCountOfType( final PasswordCharType passwordCharType )
     {
     {
         int numberOfRepeats = 0;
         int numberOfRepeats = 0;
 
 
@@ -154,7 +113,7 @@ public class PasswordCharCounter
             int loopRepeats = 0;
             int loopRepeats = 0;
             for ( int j = i; j < passwordLength; j++ )
             for ( int j = i; j < passwordLength; j++ )
             {
             {
-                if ( Character.isDigit( password.charAt( j ) ) )
+                if ( passwordCharType.isCharType( password.charAt( j ) ) )
                 {
                 {
                     loopRepeats++;
                     loopRepeats++;
                 }
                 }
@@ -168,36 +127,11 @@ public class PasswordCharCounter
                 numberOfRepeats = loopRepeats;
                 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;
         return numberOfRepeats;
     }
     }
 
 
-    public int getUniqueChars( )
+    public int uniqueCharCount( )
     {
     {
         final StringBuilder sb = new StringBuilder();
         final StringBuilder sb = new StringBuilder();
         final String passwordL = password.toLowerCase();
         final String passwordL = password.toLowerCase();
@@ -212,76 +146,18 @@ public class PasswordCharCounter
         return sb.length();
         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;
     }
     }
 }
 }

+ 190 - 0
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<PasswordCharType> 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<PwmError> getTooManyError()
+    {
+        return Optional.ofNullable( tooManyError );
+    }
+
+    public Optional<PwmError> 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<PasswordCharType> uniqueTypes()
+    {
+        return UNIQUE_TYPES;
+    }
+
+    public static Map<PasswordCharType, Integer> maxCharPerPolicy(
+            final RandomGeneratorConfig randomGeneratorConfig,
+            final PwmPasswordPolicy pwmPasswordPolicy
+    )
+    {
+        final Map<PasswordCharType, Integer> 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<CharTypeRuleAssociations> 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
+    )
+    {
+    }
+
+}

+ 11 - 11
server/src/main/java/password/pwm/util/password/PasswordRuleChecks.java

@@ -245,7 +245,7 @@ public class PasswordRuleChecks
             final PasswordRuleReaderHelper ruleHelper = ruleCheckData.getRuleHelper();
             final PasswordRuleReaderHelper ruleHelper = ruleCheckData.getRuleHelper();
             final PasswordCharCounter charCounter = ruleCheckData.getCharCounter();
             final PasswordCharCounter charCounter = ruleCheckData.getCharCounter();
             {
             {
-                final int numberOfNumericChars = charCounter.getNumericCharCount();
+                final int numberOfNumericChars = charCounter.charTypeCount( PasswordCharType.NUMBER );
                 if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowNumeric ) )
                 if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowNumeric ) )
                 {
                 {
                     if ( numberOfNumericChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumNumeric ) )
                     if ( numberOfNumericChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumNumeric ) )
@@ -260,13 +260,13 @@ public class PasswordRuleChecks
                     }
                     }
 
 
                     if ( !ruleHelper.readBooleanValue(
                     if ( !ruleHelper.readBooleanValue(
-                            PwmPasswordRule.AllowFirstCharNumeric ) && charCounter.isFirstNumeric() )
+                            PwmPasswordRule.AllowFirstCharNumeric ) && charCounter.isFirstCharType( PasswordCharType.NUMBER ) )
                     {
                     {
                         errorList.add( new ErrorInformation( PwmError.PASSWORD_FIRST_IS_NUMERIC ) );
                         errorList.add( new ErrorInformation( PwmError.PASSWORD_FIRST_IS_NUMERIC ) );
                     }
                     }
 
 
                     if ( !ruleHelper.readBooleanValue(
                     if ( !ruleHelper.readBooleanValue(
-                            PwmPasswordRule.AllowLastCharNumeric ) && charCounter.isLastNumeric() )
+                            PwmPasswordRule.AllowLastCharNumeric ) && charCounter.isLastCharType( PasswordCharType.NUMBER ) )
                     {
                     {
                         errorList.add( new ErrorInformation( PwmError.PASSWORD_LAST_IS_NUMERIC ) );
                         errorList.add( new ErrorInformation( PwmError.PASSWORD_LAST_IS_NUMERIC ) );
                     }
                     }
@@ -295,7 +295,7 @@ public class PasswordRuleChecks
 
 
             //check number of upper characters
             //check number of upper characters
             {
             {
-                final int numberOfUpperChars = charCounter.getUpperCharCount();
+                final int numberOfUpperChars = charCounter.charTypeCount( PasswordCharType.UPPERCASE );
                 if ( numberOfUpperChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumUpperCase ) )
                 if ( numberOfUpperChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumUpperCase ) )
                 {
                 {
                     errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_UPPER ) );
                     errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_UPPER ) );
@@ -310,7 +310,7 @@ public class PasswordRuleChecks
 
 
             //check number of lower characters
             //check number of lower characters
             {
             {
-                final int numberOfLowerChars = charCounter.getLowerCharCount();
+                final int numberOfLowerChars = charCounter.charTypeCount( PasswordCharType.LOWERCASE );
                 if ( numberOfLowerChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumLowerCase ) )
                 if ( numberOfLowerChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumLowerCase ) )
                 {
                 {
                     errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_LOWER ) );
                     errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_LOWER ) );
@@ -338,7 +338,7 @@ public class PasswordRuleChecks
 
 
             //check number of alpha characters
             //check number of alpha characters
             {
             {
-                final int numberOfAlphaChars = charCounter.getAlphaCharCount();
+                final int numberOfAlphaChars = charCounter.charTypeCount( PasswordCharType.LETTER );
                 if ( numberOfAlphaChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumAlpha ) )
                 if ( numberOfAlphaChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumAlpha ) )
                 {
                 {
                     errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_ALPHA ) );
                     errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_ALPHA ) );
@@ -353,7 +353,7 @@ public class PasswordRuleChecks
 
 
             //check number of non-alpha characters
             //check number of non-alpha characters
             {
             {
-                final int numberOfNonAlphaChars = charCounter.getNonAlphaCharCount();
+                final int numberOfNonAlphaChars = charCounter.charTypeCount( PasswordCharType.NON_LETTER );
 
 
                 if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowNonAlpha ) )
                 if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowNonAlpha ) )
                 {
                 {
@@ -392,7 +392,7 @@ public class PasswordRuleChecks
 
 
             //check number of special characters
             //check number of special characters
             {
             {
-                final int numberOfSpecialChars = charCounter.getSpecialCharsCount();
+                final int numberOfSpecialChars = charCounter.charTypeCount( PasswordCharType.SPECIAL );
                 if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowSpecial ) )
                 if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowSpecial ) )
                 {
                 {
                     if ( numberOfSpecialChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumSpecial ) )
                     if ( numberOfSpecialChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumSpecial ) )
@@ -407,13 +407,13 @@ public class PasswordRuleChecks
                     }
                     }
 
 
                     if ( !ruleHelper.readBooleanValue(
                     if ( !ruleHelper.readBooleanValue(
-                            PwmPasswordRule.AllowFirstCharSpecial ) && charCounter.isFirstSpecial() )
+                            PwmPasswordRule.AllowFirstCharSpecial ) && charCounter.isFirstCharType( PasswordCharType.SPECIAL ) )
                     {
                     {
                         errorList.add( new ErrorInformation( PwmError.PASSWORD_FIRST_IS_SPECIAL ) );
                         errorList.add( new ErrorInformation( PwmError.PASSWORD_FIRST_IS_SPECIAL ) );
                     }
                     }
 
 
                     if ( !ruleHelper.readBooleanValue(
                     if ( !ruleHelper.readBooleanValue(
-                            PwmPasswordRule.AllowLastCharSpecial ) && charCounter.isLastSpecial() )
+                            PwmPasswordRule.AllowLastCharSpecial ) && charCounter.isLastCharType( PasswordCharType.SPECIAL ) )
                     {
                     {
                         errorList.add( new ErrorInformation( PwmError.PASSWORD_LAST_IS_SPECIAL ) );
                         errorList.add( new ErrorInformation( PwmError.PASSWORD_LAST_IS_SPECIAL ) );
                     }
                     }
@@ -483,7 +483,7 @@ public class PasswordRuleChecks
             //Check minimum unique character
             //Check minimum unique character
             {
             {
                 final int minUnique = ruleHelper.readIntValue( PwmPasswordRule.MinimumUnique );
                 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 ) );
                     errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_UNIQUE ) );
                 }
                 }

+ 55 - 24
server/src/main/java/password/pwm/util/password/PasswordUtility.java

@@ -26,6 +26,7 @@ import com.novell.ldapchai.exception.ChaiException;
 import com.novell.ldapchai.exception.ChaiOperationException;
 import com.novell.ldapchai.exception.ChaiOperationException;
 import com.novell.ldapchai.exception.ChaiPasswordPolicyException;
 import com.novell.ldapchai.exception.ChaiPasswordPolicyException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
+import com.novell.ldapchai.exception.ImpossiblePasswordPolicyException;
 import com.novell.ldapchai.impl.oracleds.entry.OracleDSEntries;
 import com.novell.ldapchai.impl.oracleds.entry.OracleDSEntries;
 import com.novell.ldapchai.provider.ChaiConfiguration;
 import com.novell.ldapchai.provider.ChaiConfiguration;
 import com.novell.ldapchai.provider.ChaiProvider;
 import com.novell.ldapchai.provider.ChaiProvider;
@@ -217,6 +218,41 @@ public class PasswordUtility
         return null;
         return null;
     }
     }
 
 
+    /**
+     * <p>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.</p>
+     *
+     * <p>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.</p>
+     *
+     * @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
     enum PasswordPolicySource
     {
     {
@@ -853,19 +889,14 @@ public class PasswordUtility
         final int zxcvbnScore = strength.getScore();
         final int zxcvbnScore = strength.getScore();
 
 
         // zxcvbn returns a score of 0-4 (see: https://github.com/nulab/zxcvbn4j)
         // 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(
     public static int judgePasswordStrengthUsingTraditionalAlgorithm(
@@ -882,29 +913,29 @@ public class PasswordUtility
 
 
         // -- Additions --
         // -- Additions --
         // amount of unique chars
         // amount of unique chars
-        if ( charCounter.getUniqueChars() > 7 )
+        if ( charCounter.uniqueCharCount() > 7 )
         {
         {
             score = score + 10;
             score = score + 10;
         }
         }
-        score = score + ( ( charCounter.getUniqueChars() ) * 3 );
+        score = score + ( ( charCounter.uniqueCharCount() ) * 3 );
 
 
         // Numbers
         // Numbers
-        if ( charCounter.getNumericCharCount() > 0 )
+        if ( charCounter.hasCharsOfType( PasswordCharType.NUMBER ) )
         {
         {
             score = score + 8;
             score = score + 8;
-            score = score + ( charCounter.getNumericCharCount() ) * 4;
+            score = score + ( charCounter.charTypeCount( PasswordCharType.NUMBER ) ) * 4;
         }
         }
 
 
         // specials
         // specials
-        if ( charCounter.getSpecialCharsCount() > 0 )
+        if ( charCounter.hasCharsOfType( PasswordCharType.SPECIAL ) )
         {
         {
             score = score + 14;
             score = score + 14;
-            score = score + ( charCounter.getSpecialCharsCount() ) * 5;
+            score = score + ( charCounter.charTypeCount( PasswordCharType.SPECIAL ) ) * 5;
         }
         }
 
 
         // mixed case
         // 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;
             score = score + 10;
         }
         }
@@ -912,9 +943,9 @@ public class PasswordUtility
         // -- Deductions --
         // -- Deductions --
 
 
         // sequential numbers
         // 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
         // sequential chars
@@ -923,7 +954,7 @@ public class PasswordUtility
             score = score - ( charCounter.getSequentialRepeatedChars() ) * 5;
             score = score - ( charCounter.getSequentialRepeatedChars() ) * 5;
         }
         }
 
 
-        return score > 100 ? 100 : score < 0 ? 0 : score;
+        return score > 100 ? 100 : Math.max( score, 0 );
     }
     }
 
 
 
 

+ 12 - 11
server/src/main/java/password/pwm/util/password/PwmPasswordAdRuleUtil.java

@@ -128,23 +128,23 @@ class PwmPasswordAdRuleUtil
             final List<ErrorInformation> errorList = new ArrayList<>();
             final List<ErrorInformation> errorList = new ArrayList<>();
 
 
             // add errors complexity violations
             // add errors complexity violations
-            if ( charCounter.getUpperCharCount() < 1 )
+            if ( !charCounter.hasCharsOfType( PasswordCharType.UPPERCASE ) )
             {
             {
                 errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_UPPER ) );
                 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 ) );
                 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 ) );
                 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 ) );
                 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 ) );
                 errorList.add( new ErrorInformation( PwmError.PASSWORD_UNKNOWN_VALIDATION ) );
             }
             }
@@ -194,33 +194,34 @@ class PwmPasswordAdRuleUtil
         )
         )
         {
         {
             int complexityPoints = 0;
             int complexityPoints = 0;
-            if ( charCounter.getUpperCharCount() > 0 )
+            if ( charCounter.hasCharsOfType( PasswordCharType.UPPERCASE ) )
             {
             {
                 complexityPoints++;
                 complexityPoints++;
             }
             }
-            if ( charCounter.getLowerCharCount() > 0 )
+            if ( charCounter.hasCharsOfType( PasswordCharType.LOWERCASE ) )
             {
             {
                 complexityPoints++;
                 complexityPoints++;
             }
             }
-            if ( charCounter.getNumericCharCount() > 0 )
+            if ( charCounter.hasCharsOfType( PasswordCharType.NUMBER ) )
             {
             {
                 complexityPoints++;
                 complexityPoints++;
             }
             }
             switch ( complexityLevel )
             switch ( complexityLevel )
             {
             {
                 case AD2003:
                 case AD2003:
-                    if ( charCounter.getSpecialCharsCount() > 0 || charCounter.getOtherLetterCharCount() > 0 )
+                    if ( charCounter.hasCharsOfType( PasswordCharType.SPECIAL )
+                            || charCounter.hasCharsOfType( PasswordCharType.OTHER_LETTER ) )
                     {
                     {
                         complexityPoints++;
                         complexityPoints++;
                     }
                     }
                     break;
                     break;
 
 
                 case AD2008:
                 case AD2008:
-                    if ( charCounter.getSpecialCharsCount() > 0 )
+                    if ( charCounter.hasCharsOfType( PasswordCharType.SPECIAL ) )
                     {
                     {
                         complexityPoints++;
                         complexityPoints++;
                     }
                     }
-                    if ( charCounter.getOtherLetterCharCount() > 0 )
+                    if ( charCounter.hasCharsOfType( PasswordCharType.OTHER_LETTER ) )
                     {
                     {
                         complexityPoints++;
                         complexityPoints++;
                     }
                     }

+ 110 - 57
server/src/main/java/password/pwm/util/password/RandomGeneratorConfig.java

@@ -20,9 +20,6 @@
 
 
 package password.pwm.util.password;
 package password.pwm.util.password;
 
 
-import lombok.AccessLevel;
-import lombok.Builder;
-import lombok.Value;
 import password.pwm.AppProperty;
 import password.pwm.AppProperty;
 import password.pwm.PwmDomain;
 import password.pwm.PwmDomain;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.profile.PwmPasswordPolicy;
@@ -30,42 +27,54 @@ import password.pwm.config.profile.PwmPasswordRule;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 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 MINIMUM_STRENGTH = 0;
     private static final int MAXIMUM_STRENGTH = 100;
     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<String> 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(
     public static RandomGeneratorConfig make(
             final PwmDomain pwmDomain,
             final PwmDomain pwmDomain,
@@ -73,7 +82,6 @@ public class RandomGeneratorConfig
     )
     )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
-
         return make( pwmDomain, pwmPasswordPolicy, RandomGeneratorConfigRequest.builder().build() );
         return make( pwmDomain, pwmPasswordPolicy, RandomGeneratorConfigRequest.builder().build() );
     }
     }
 
 
@@ -84,66 +92,102 @@ public class RandomGeneratorConfig
     )
     )
             throws PwmUnrecoverableException
             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 );
         config.validateSettings( pwmDomain );
 
 
         return config;
         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;
         int returnVal = requestedValue;
+
         if ( requestedValue <= 0 )
         if ( requestedValue <= 0 )
         {
         {
             returnVal = Integer.parseInt( pwmDomain.getConfig().readAppProperty( AppProperty.PASSWORD_RANDOMGEN_MIN_LENGTH ) );
             returnVal = Integer.parseInt( pwmDomain.getConfig().readAppProperty( AppProperty.PASSWORD_RANDOMGEN_MIN_LENGTH ) );
         }
         }
+
         if ( pwmPasswordPolicy != null )
         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 )
             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;
         return returnVal;
     }
     }
 
 
     private static int figureMinimumStrength( final PwmDomain pwmDomain, final PwmPasswordPolicy pwmPasswordPolicy, final int requestedValue )
     private static int figureMinimumStrength( final PwmDomain pwmDomain, final PwmPasswordPolicy pwmPasswordPolicy, final int requestedValue )
     {
     {
-        int policyMin = requestedValue;
+        int returnValue = requestedValue;
         if ( requestedValue <= 0 )
         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 )
         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 )
     void validateSettings( final PwmDomain pwmDomain )
@@ -151,7 +195,7 @@ public class RandomGeneratorConfig
     {
     {
         final int maxLength = Integer.parseInt(
         final int maxLength = Integer.parseInt(
                 pwmDomain.getConfig().readAppProperty( AppProperty.PASSWORD_RANDOMGEN_MAX_LENGTH ) );
                 pwmDomain.getConfig().readAppProperty( AppProperty.PASSWORD_RANDOMGEN_MAX_LENGTH ) );
-        if ( this.getMinimumLength() > maxLength )
+        if ( this.minimumLength() > maxLength )
         {
         {
             throw new PwmUnrecoverableException( new ErrorInformation(
             throw new PwmUnrecoverableException( new ErrorInformation(
                     PwmError.ERROR_INTERNAL,
                     PwmError.ERROR_INTERNAL,
@@ -159,7 +203,15 @@ public class RandomGeneratorConfig
             ) );
             ) );
         }
         }
 
 
-        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(
             throw new PwmUnrecoverableException( new ErrorInformation(
                     PwmError.ERROR_INTERNAL,
                     PwmError.ERROR_INTERNAL,
@@ -167,7 +219,7 @@ public class RandomGeneratorConfig
             ) );
             ) );
         }
         }
 
 
-        if ( this.getMinimumStrength() > RandomGeneratorConfig.MAXIMUM_STRENGTH )
+        if ( this.minimumStrength() > RandomGeneratorConfig.MAXIMUM_STRENGTH )
         {
         {
             throw new PwmUnrecoverableException( new ErrorInformation(
             throw new PwmUnrecoverableException( new ErrorInformation(
                     PwmError.ERROR_INTERNAL,
                     PwmError.ERROR_INTERNAL,
@@ -175,4 +227,5 @@ public class RandomGeneratorConfig
             ) );
             ) );
         }
         }
     }
     }
+
 }
 }

+ 3 - 3
server/src/main/java/password/pwm/util/password/RandomGeneratorConfigRequest.java

@@ -23,8 +23,7 @@ package password.pwm.util.password;
 import lombok.Builder;
 import lombok.Builder;
 import lombok.Value;
 import lombok.Value;
 
 
-import java.util.Collection;
-import java.util.Collections;
+import java.util.List;
 
 
 @Builder
 @Builder
 @Value
 @Value
@@ -35,7 +34,7 @@ public class RandomGeneratorConfigRequest
      * values in the phrases to build a random password that meets rule requirements
      * values in the phrases to build a random password that meets rule requirements
      */
      */
     @Builder.Default
     @Builder.Default
-    private Collection<String> seedlistPhrases = Collections.emptySet();
+    private List<String> seedlistPhrases = List.of();
 
 
     /**
     /**
      * The minimum length desired for the password.  The algorithm will attempt to make
      * The minimum length desired for the password.  The algorithm will attempt to make
@@ -53,4 +52,5 @@ public class RandomGeneratorConfigRequest
      */
      */
     @Builder.Default
     @Builder.Default
     private int minimumStrength = -1;
     private int minimumStrength = -1;
+
 }
 }

+ 143 - 0
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<PasswordCharType, Integer> maxCharsPerType,
+        PwmRandom pwmRandom,
+        PwmDomain pwmDomain
+)
+{
+    RandomGeneratorRequest(
+            final SessionLabel sessionLabel,
+            final PwmPasswordPolicy randomGenPolicy,
+            final RandomGeneratorConfig randomGeneratorConfig,
+            final Map<PasswordCharType, Integer> 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<PasswordCharType, Integer> maxCharsPerType = PasswordCharType.maxCharPerPolicy( randomGeneratorConfig, passwordPolicy );
+
+        return new RandomGeneratorRequest(
+                sessionLabel,
+                passwordPolicy,
+                randomGeneratorConfig,
+                maxCharsPerType,
+                pwmDomain.getSecureService().pwmRandom(),
+                pwmDomain
+        );
+    }
+
+    /**
+     * <p>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.</p>
+     *
+     * <p>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.</p>
+     *
+     * @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<PasswordCharType, Integer> 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<String, String> 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 );
+    }
+
+}

+ 131 - 406
server/src/main/java/password/pwm/util/password/RandomPasswordGenerator.java

@@ -20,14 +20,12 @@
 
 
 package password.pwm.util.password;
 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.PwmDomain;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.SessionLabel;
 import password.pwm.config.DomainConfig;
 import password.pwm.config.DomainConfig;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.profile.PwmPasswordPolicy;
-import password.pwm.config.profile.PwmPasswordRule;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
@@ -41,405 +39,242 @@ import password.pwm.util.secure.PwmRandom;
 
 
 import java.time.Instant;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
 import java.util.EnumSet;
 import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.Set;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
 
 
 /**
 /**
  * Random password generator.
  * Random password generator.
  *
  *
  * @author Jason D. Rivard
  * @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<String> 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 );
     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( )
+    {
+    }
 
 
-    /**
-     * <p>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.</p>
-     *
-     * <p>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.</p>
-     *
-     * @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
             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();
         final Instant startTime = Instant.now();
 
 
         randomGeneratorConfig.validateSettings( pwmDomain );
         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
         // read a rule validator
         // modify until it passes all the rules
         // modify until it passes all the rules
-        final MutatorResult mutatorResult = passwordMutator( sessionLabel, pwmDomain, seedMachine, randomGeneratorConfig, randomGenPolicy );
+        final MutatorResult mutatorResult = mutatePassword( request );
 
 
         // report outcome
         // report outcome
-
-        if ( mutatorResult.isValidPassword() )
+        if ( mutatorResult.validPassword() )
         {
         {
-            LOGGER.trace( sessionLabel, () -> "finished random password generation after " + mutatorResult.getRounds()
-                    + " rounds.", TimeDuration.fromCurrent( startTime ) );
+            final Supplier<CharSequence> logMsg = () -> "finished random password generation after "
+                    + mutatorResult.rounds() + " rounds.";
+            LOGGER.trace( sessionLabel, logMsg, TimeDuration.fromCurrent( startTime ) );
+            //System.out.println( logMsg.get() );
         }
         }
         else
         else
         {
         {
             if ( LOGGER.isInterestingLevel( PwmLogLevel.ERROR ) )
             if ( LOGGER.isInterestingLevel( PwmLogLevel.ERROR ) )
             {
             {
                 final PwmPasswordRuleValidator pwmPasswordRuleValidator = PwmPasswordRuleValidator.create( sessionLabel, pwmDomain, randomGenPolicy );
                 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<CharSequence> 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 );
         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<String, String> 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
             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;
         int tryCount = 0;
         boolean validPassword = false;
         boolean validPassword = false;
+
+        final MutablePassword mutablePassword = new MutablePassword( request, request.randomGeneratorConfig().seedMachine(), request.pwmRandom(), newPassword );
+
         while ( !validPassword && tryCount < maxTryCount )
         while ( !validPassword && tryCount < maxTryCount )
         {
         {
             tryCount++;
             tryCount++;
             validPassword = true;
             validPassword = true;
 
 
-            if ( tryCount % jitterCount == 0 )
-            {
-                password.delete( 0, password.length() );
-                password.append( generateNewPassword( pwmRandom, seedMachine, effectiveConfig.getMinimumLength() ) );
-            }
-
             final List<ErrorInformation> errors = pwmPasswordRuleValidator.internalPwmPolicyValidator(
             final List<ErrorInformation> errors = pwmPasswordRuleValidator.internalPwmPolicyValidator(
-                    password.toString(), null, null );
+                    mutablePassword.value(), null, null );
+
             if ( errors != null && !errors.isEmpty() )
             if ( errors != null && !errors.isEmpty() )
             {
             {
                 validPassword = false;
                 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;
                 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(
     private static void modifyPasswordBasedOnErrors(
-            final PwmRandom pwmRandom,
-            final StringBuilder password,
-            final List<ErrorInformation> errors,
-            final SeedMachine seedMachine
+            final MutablePassword mutablePassword,
+            final List<ErrorInformation> errors
     )
     )
     {
     {
-        if ( password == null || errors == null || errors.isEmpty() )
+        if ( errors == null || errors.isEmpty() )
         {
         {
             return;
             return;
         }
         }
 
 
         final Set<PwmError> errorMessages = EnumSet.noneOf( PwmError.class );
         final Set<PwmError> errorMessages = EnumSet.noneOf( PwmError.class );
-        for ( final ErrorInformation errorInfo : errors )
-        {
-            errorMessages.add( errorInfo.getError() );
-        }
+        errors.forEach( errorInfo -> errorMessages.add( errorInfo.getError() ) );
 
 
         boolean touched = false;
         boolean touched = false;
 
 
         if ( errorMessages.contains( PwmError.PASSWORD_TOO_SHORT ) )
         if ( errorMessages.contains( PwmError.PASSWORD_TOO_SHORT ) )
         {
         {
-            addRandChar( pwmRandom, password, seedMachine.getAllChars() );
+            mutablePassword.addRandChar();
             touched = true;
             touched = true;
         }
         }
 
 
         if ( errorMessages.contains( PwmError.PASSWORD_TOO_LONG ) )
         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;
             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;
             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;
             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;
             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;
             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;
             touched = true;
         }
         }
 
 
         if ( !touched )
         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<PwmError> errorMessages
     )
     )
-            throws ImpossiblePasswordPolicyException
     {
     {
-        final List<Integer> 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<PwmError> 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<PwmError> 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<PwmError> 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 )
     private static boolean checkPasswordAgainstDisallowedHttpValues( final DomainConfig config, final String password )
@@ -458,160 +293,50 @@ public class RandomPasswordGenerator
         return false;
         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<String> 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<PasswordCharType, MutableInt> 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<String> 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<PasswordCharType> 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() );
             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();
         return password.toString();
     }
     }
-
-    private static Collection<String> normalizeSeeds( final Collection<String> inputSeeds )
-    {
-        if ( inputSeeds == null )
-        {
-            return DEFAULT_SEED_PHRASES;
-        }
-
-        final Collection<String> newSeeds = new HashSet<>( inputSeeds );
-        newSeeds.removeIf( s -> s == null || s.length() < 1 );
-
-        return newSeeds.isEmpty() ? DEFAULT_SEED_PHRASES : newSeeds;
-    }
-
 }
 }

+ 146 - 0
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<String> DEFAULT_SEED_PHRASES = makeDefaultSeedPhrases();
+    private static final SeedMachine DEFAULT_SEED_MACHINE = create( PwmRandom.getInstance(), DEFAULT_SEED_PHRASES );
+
+    private final Collection<String> seeds;
+    private final PwmRandom pwmRandom;
+
+    private final Map<PasswordCharType, String> cachedCharsOfType = new EnumMap<>( PasswordCharType.class );
+    private final Map<PasswordCharType, String> cachedCharsOfTypeException = new EnumMap<>( PasswordCharType.class );
+    private final Supplier<String> allChars = this::figureAllChars;
+
+    private SeedMachine( final PwmRandom pwmRandom, final Collection<String> 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<String> seeds )
+    {
+        final List<String> 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<String> 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<String> makeDefaultSeedPhrases()
+    {
+        final List<String> asciiChars = IntStream.range( 33, 126 )
+                .boxed()
+                .map( Character::toString )
+                .toList();
+        return List.copyOf( asciiChars );
+    }
+
+    private static List<String> normalizeSeeds( final Collection<String> inputSeeds )
+    {
+        if ( inputSeeds == null )
+        {
+            return DEFAULT_SEED_PHRASES;
+        }
+
+        final List<String> newSeeds = new ArrayList<>( inputSeeds );
+        newSeeds.removeIf( s -> s == null || s.length() < 1 );
+
+        return newSeeds.isEmpty() ? DEFAULT_SEED_PHRASES : List.copyOf( newSeeds );
+    }
+}

+ 4 - 2
server/src/main/java/password/pwm/ws/server/rest/RestRandomPasswordServer.java

@@ -40,7 +40,6 @@ import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.password.RandomGeneratorConfig;
 import password.pwm.util.password.RandomGeneratorConfig;
 import password.pwm.util.password.RandomGeneratorConfigRequest;
 import password.pwm.util.password.RandomGeneratorConfigRequest;
-import password.pwm.util.password.RandomPasswordGenerator;
 import password.pwm.ws.server.RestMethodHandler;
 import password.pwm.ws.server.RestMethodHandler;
 import password.pwm.ws.server.RestRequest;
 import password.pwm.ws.server.RestRequest;
 import password.pwm.ws.server.RestResultBean;
 import password.pwm.ws.server.RestResultBean;
@@ -213,7 +212,10 @@ public class RestRandomPasswordServer extends RestServlet
         }
         }
 
 
         final RandomGeneratorConfig randomConfig = jsonInputToRandomConfig( jsonInput, restRequest.getDomain(), pwmPasswordPolicy );
         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();
         final JsonOutput outputMap = new JsonOutput();
         outputMap.password = randomPassword.getStringValue();
         outputMap.password = randomPassword.getStringValue();
 
 

+ 3 - 3
server/src/main/java/password/pwm/ws/server/rest/RestSetPasswordServer.java

@@ -40,7 +40,6 @@ import password.pwm.util.BasicAuthInfo;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PasswordData;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.password.PasswordUtility;
-import password.pwm.util.password.RandomPasswordGenerator;
 import password.pwm.ws.server.RestMethodHandler;
 import password.pwm.ws.server.RestMethodHandler;
 import password.pwm.ws.server.RestRequest;
 import password.pwm.ws.server.RestRequest;
 import password.pwm.ws.server.RestResultBean;
 import password.pwm.ws.server.RestResultBean;
@@ -172,9 +171,10 @@ public class RestSetPasswordServer extends RestServlet
                         restRequest.getSessionLabel(),
                         restRequest.getSessionLabel(),
                         targetUserIdentity.getUserIdentity(),
                         targetUserIdentity.getUserIdentity(),
                         targetUserIdentity.getChaiUser() );
                         targetUserIdentity.getChaiUser() );
-                newPassword = RandomPasswordGenerator.createRandomPassword(
+                newPassword = PasswordUtility.generateRandom(
                         restRequest.getSessionLabel(),
                         restRequest.getSessionLabel(),
-                        passwordPolicy, restRequest.getDomain()
+                        passwordPolicy,
+                        restRequest.getDomain()
                 );
                 );
             }
             }
             else
             else

+ 1 - 2
server/src/main/resources/password/pwm/AppProperty.properties

@@ -265,10 +265,9 @@ otp.qrImage.height=200
 otp.qrImage.width=200
 otp.qrImage.width=200
 otp.encryptionAlg=AES
 otp.encryptionAlg=AES
 password.randomGenerator.maxAttempts=2000
 password.randomGenerator.maxAttempts=2000
-password.randomGenerator.maxLength=1024
+password.randomGenerator.maxLength=10000
 password.randomGenerator.minLength=12
 password.randomGenerator.minLength=12
 password.randomGenerator.defaultStrength=50
 password.randomGenerator.defaultStrength=50
-password.randomGenerator.jitter.count=50
 password.strength.threshold.veryStrong=100
 password.strength.threshold.veryStrong=100
 password.strength.threshold.strong=75
 password.strength.threshold.strong=75
 password.strength.threshold.good=45
 password.strength.threshold.good=45

+ 190 - 11
server/src/test/java/password/pwm/util/password/RandomPasswordGeneratorTest.java

@@ -22,51 +22,230 @@ package password.pwm.util.password;
 
 
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.ThrowingConsumer;
 import org.junit.jupiter.api.io.TempDir;
 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.PwmApplication;
 import password.pwm.PwmDomain;
 import password.pwm.PwmDomain;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.SessionLabel;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.profile.PwmPasswordRule;
 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.PasswordData;
 import password.pwm.util.localdb.TestHelper;
 import password.pwm.util.localdb.TestHelper;
 
 
-import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Path;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Map;
 import java.util.Set;
 import java.util.Set;
+import java.util.stream.IntStream;
 
 
 public class RandomPasswordGeneratorTest
 public class RandomPasswordGeneratorTest
 {
 {
+    private static final int LOOP_COUNT = 1_000;
+
     @TempDir
     @TempDir
     public Path temporaryFolder;
     public Path temporaryFolder;
 
 
+    @Test
+    public void specialCharsRulesTest()
+            throws Throwable
+    {
+        final int minSpecial = 33;
+        final int maxSpecial = 44;
+        final Map<String, String> 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<String> 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
     @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<String, String> policyMap = new HashMap<>( PwmPasswordPolicy.defaultPolicy().getPolicyMap() );
         final Map<String, String> policyMap = new HashMap<>( PwmPasswordPolicy.defaultPolicy().getPolicyMap() );
         policyMap.put( PwmPasswordRule.AllowNumeric.getKey(), "true" );
         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<String> 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<String, String> 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<String, String> 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<String> 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<String, String> 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<String> 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<String, String> 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<String, String> policyMap, final List<ThrowingConsumer<String>> 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,
                     SessionLabel.TEST_SESSION_LABEL,
                     pwmPasswordPolicy,
                     pwmPasswordPolicy,
                     pwmDomain );
                     pwmDomain );
 
 
             final String passwordString = passwordData.getStringValue();
             final String passwordString = passwordData.getStringValue();
+
+            final List<ErrorInformation> 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<String> extraCheck : extraChecks )
+            {
+                extraCheck.accept( passwordString );
+            }
+        }
+    }
+
+    private static class DupeValueChecker implements ThrowingConsumer<String>
+    {
+        final Set<String> seenValues = new HashSet<>();
+
+        @Override
+        public void accept( final String passwordString )
+                throws Throwable
+        {
             if ( seenValues.contains( passwordString ) )
             if ( seenValues.contains( passwordString ) )
             {
             {
                 Assertions.fail( "repeated random generated password" );
                 Assertions.fail( "repeated random generated password" );

+ 15 - 19
webapp/src/main/webapp/public/resources/js/changepassword.js

@@ -237,25 +237,22 @@ PWM_CHANGEPW.doRandomGeneration=function(randomConfig) {
     dialogBody += "<br/><br/>";
     dialogBody += "<br/><br/>";
     dialogBody += '<table class="noborder">';
     dialogBody += '<table class="noborder">';
 
 
-    for (let i = 0; i < 20; i++) {
-        dialogBody += '<tr class="noborder">';
-        for (let j = 0; j < 2; j++) {
-            i = i + j;
-            (function(index) {
-                const elementID = "randomGen" + index;
-                dialogBody += '<td class="noborder"><div class="link-randomPasswordValue"  id="' + elementID + '"></div></td>';
-                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 += '<tr class="noborder">';
+            dialogBody += '<td class="noborder"><div class="link-randomPasswordValue"  id="' + elementID + '"></div></td>';
+            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 += '</tr>';
+            });
+            dialogBody += '</tr>';
+        })(i);
     }
     }
     dialogBody += "</table><br/><br/>";
     dialogBody += "</table><br/><br/>";
 
 
@@ -279,7 +276,6 @@ PWM_CHANGEPW.doRandomGeneration=function(randomConfig) {
     const titleString = randomConfig['title'] ? randomConfig['title'] : PWM_MAIN.showString('Title_RandomPasswords');
     const titleString = randomConfig['title'] ? randomConfig['title'] : PWM_MAIN.showString('Title_RandomPasswords');
     PWM_MAIN.showDialog({
     PWM_MAIN.showDialog({
         title:titleString,
         title:titleString,
-        dialogClass:'narrow',
         text:dialogBody,
         text:dialogBody,
         showOk:false,
         showOk:false,
         showClose:true,
         showClose:true,