/*
* Password Management Servlets (PWM)
* http://www.pwm-project.org
*
* Copyright (c) 2006-2009 Novell, Inc.
* Copyright (c) 2009-2017 The PWM Project
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package password.pwm.util;
import com.novell.ldapchai.exception.ImpossiblePasswordPolicyException;
import password.pwm.AppProperty;
import password.pwm.PwmApplication;
import password.pwm.bean.SessionLabel;
import password.pwm.config.Configuration;
import password.pwm.config.PwmSetting;
import password.pwm.config.profile.PwmPasswordPolicy;
import password.pwm.config.profile.PwmPasswordRule;
import password.pwm.error.ErrorInformation;
import password.pwm.error.PwmError;
import password.pwm.error.PwmUnrecoverableException;
import password.pwm.http.PwmSession;
import password.pwm.svc.PwmService;
import password.pwm.svc.stats.Statistic;
import password.pwm.svc.wordlist.SeedlistManager;
import password.pwm.util.java.TimeDuration;
import password.pwm.util.logging.PwmLogger;
import password.pwm.util.operations.PasswordUtility;
import password.pwm.util.secure.PwmRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Random password generator
*
* @author Jason D. Rivard
*/
public class RandomPasswordGenerator {
// ------------------------------ FIELDS ------------------------------
/**
* 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 SeedMachine DEFAULT_SEED_MACHINE = new SeedMachine(DEFAULT_SEED_PHRASES);
private static final PwmRandom RANDOM = PwmRandom.getInstance();
private static final PwmLogger LOGGER = PwmLogger.forClass(RandomPasswordGenerator.class);
// -------------------------- STATIC METHODS --------------------------
public static PasswordData createRandomPassword(
final PwmSession pwmSession,
final PwmApplication pwmApplication
)
throws PwmUnrecoverableException
{
final PwmPasswordPolicy userPasswordPolicy = pwmSession.getUserInfoBean().getPasswordPolicy();
return createRandomPassword(pwmSession.getLabel(), userPasswordPolicy, pwmApplication);
}
public static PasswordData createRandomPassword(
final SessionLabel sessionLabel,
final PwmPasswordPolicy passwordPolicy,
final PwmApplication pwmApplication
)
throws PwmUnrecoverableException
{
final RandomGeneratorConfig randomGeneratorConfig = new RandomGeneratorConfig();
randomGeneratorConfig.setPasswordPolicy(passwordPolicy);
return createRandomPassword(
sessionLabel,
randomGeneratorConfig,
pwmApplication
);
}
/**
* Creates a new password that satisfies the password rules. All rules are checked for. If for some
* reason the RANDOM algorithm can not generate a valid password, null will be returned.
* <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.
*
* @param sessionLabel A valid pwmSession
* @param randomGeneratorConfig Policy to be used during generation
* @param pwmApplication Used to read configuration, seedmanager and other services.
* @return A randomly generated password value that meets the requirements of this {@code PasswordPolicy}
* @throws com.novell.ldapchai.exception.ImpossiblePasswordPolicyException
* If there is no way to create a password using the configured rules and
* default seed phrase
*/
public static PasswordData createRandomPassword(
final SessionLabel sessionLabel,
final RandomGeneratorConfig randomGeneratorConfig,
final PwmApplication pwmApplication
)
throws PwmUnrecoverableException
{
final long startTimeMS = System.currentTimeMillis();
validateSettings(pwmApplication, randomGeneratorConfig);
if (randomGeneratorConfig.getSeedlistPhrases() == null || randomGeneratorConfig.getSeedlistPhrases().isEmpty()) {
Set<String> seeds = DEFAULT_SEED_PHRASES;
final SeedlistManager seedlistManager = pwmApplication.getSeedlistManager();
if (seedlistManager != null && seedlistManager.status() == PwmService.STATUS.OPEN && seedlistManager.size() > 0) {
seeds = new HashSet<>();
int safetyCounter = 0;
while (seeds.size() < 10 && safetyCounter < 100) {
safetyCounter++;
final String randomWord = seedlistManager.randomSeed();
if (randomWord != null) {
seeds.add(randomWord);
}
}
}
randomGeneratorConfig.setSeedlistPhrases(seeds);
}
final SeedMachine seedMachine = new SeedMachine(normalizeSeeds(randomGeneratorConfig.getSeedlistPhrases()));
int tryCount = 0;
final StringBuilder password = new StringBuilder();
// determine the password policy to use for random generation
final PwmPasswordPolicy randomGenPolicy;
{
final Map<String, String> newPolicyMap = new HashMap<>();
newPolicyMap.putAll(randomGeneratorConfig.getPasswordPolicy().getPolicyMap());
if (randomGeneratorConfig.getMinimumLength() > randomGeneratorConfig.getPasswordPolicy().getRuleHelper().readIntValue(PwmPasswordRule.MinimumLength)) {
newPolicyMap.put(PwmPasswordRule.MinimumLength.getKey(), String.valueOf(randomGeneratorConfig.getMinimumLength()));
}
if (randomGeneratorConfig.getMaximumLength() < randomGeneratorConfig.getPasswordPolicy().getRuleHelper().readIntValue(PwmPasswordRule.MaximumLength)) {
newPolicyMap.put(PwmPasswordRule.MaximumLength.getKey(), String.valueOf(randomGeneratorConfig.getMaximumLength()));
}
if (randomGeneratorConfig.getMinimumStrength() > randomGeneratorConfig.getPasswordPolicy().getRuleHelper().readIntValue(PwmPasswordRule.MinimumStrength)) {
newPolicyMap.put(PwmPasswordRule.MinimumStrength.getKey(), String.valueOf(randomGeneratorConfig.getMinimumStrength()));
}
randomGenPolicy = PwmPasswordPolicy.createPwmPasswordPolicy(newPolicyMap);
}
// initial creation
password.append(generateNewPassword(seedMachine, randomGeneratorConfig.getMinimumLength()));
// read a rule validator
final PwmPasswordRuleValidator pwmPasswordRuleValidator = new PwmPasswordRuleValidator(pwmApplication, randomGenPolicy);
// modify until it passes all the rules
final int MAX_TRY_COUNT = Integer.parseInt(pwmApplication.getConfig().readAppProperty(AppProperty.PASSWORD_RANDOMGEN_MAX_ATTEMPTS));
final int JITTER_COUNT = Integer.parseInt(pwmApplication.getConfig().readAppProperty(AppProperty.PASSWORD_RANDOMGEN_JITTER_COUNT));
boolean validPassword = false;
while (!validPassword && tryCount < MAX_TRY_COUNT) {
tryCount++;
validPassword = true;
if (tryCount % JITTER_COUNT == 0) {
password.delete(0,password.length());
password.append(generateNewPassword(seedMachine, randomGeneratorConfig.getMinimumLength()));
}
final List<ErrorInformation> errors = pwmPasswordRuleValidator.internalPwmPolicyValidator(
password.toString(), null, null, PwmPasswordRuleValidator.Flag.FailFast);
if (errors != null && !errors.isEmpty()) {
validPassword = false;
modifyPasswordBasedOnErrors(password, errors, seedMachine);
} else if (checkPasswordAgainstDisallowedHttpValues(pwmApplication.getConfig(), password.toString())) {
validPassword = false;
password.delete(0, password.length());
password.append(generateNewPassword(seedMachine, randomGeneratorConfig.getMinimumLength()));
}
}
// report outcome
{
final TimeDuration td = TimeDuration.fromCurrent(startTimeMS);
if (validPassword) {
LOGGER.trace(sessionLabel, "finished random password generation in " + td.asCompactString() + " after " + tryCount + " tries.");
} else {
final List<ErrorInformation> errors = pwmPasswordRuleValidator.internalPwmPolicyValidator(password.toString(), null, null);
final int judgeLevel = PasswordUtility.judgePasswordStrength(password.toString());
final StringBuilder sb = new StringBuilder();
sb.append("failed random password generation after ").append(td.asCompactString()).append(" after ").append(tryCount).append(" tries. ");
sb.append("(errors=").append(errors.size()).append(", judgeLevel=").append(judgeLevel);
LOGGER.error(sessionLabel, sb.toString());
}
}
if (pwmApplication != null && pwmApplication.getStatisticsManager() != null) {
pwmApplication.getStatisticsManager().incrementValue(Statistic.GENERATED_PASSWORDS);
}
final StringBuilder sb = new StringBuilder();
sb.append("real-time random password generator called");
sb.append(" (").append(TimeDuration.fromCurrent(startTimeMS).asCompactString());
sb.append(")");
LOGGER.trace(sessionLabel, sb.toString());
return new PasswordData(password.toString());
}
private static void modifyPasswordBasedOnErrors(
final StringBuilder password,
final List<ErrorInformation> errors,
final SeedMachine seedMachine
) {
if (password == null || errors == null || errors.isEmpty()) {
return;
}
final Set<PwmError> errorMessages = new HashSet<>();
for (final ErrorInformation errorInfo : errors) {
errorMessages.add(errorInfo.getError());
}
boolean touched = false;
if (errorMessages.contains(PwmError.PASSWORD_TOO_SHORT)) {
addRandChar(password, seedMachine.getAllChars());
touched = true;
}
if (errorMessages.contains(PwmError.PASSWORD_TOO_LONG)) {
password.deleteCharAt(RANDOM.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(password, seedMachine.getNumChars());
touched = true;
}
if (errorMessages.contains(PwmError.PASSWORD_NOT_ENOUGH_SPECIAL)) {
addRandChar(password, seedMachine.getSpecialChars());
touched = true;
}
if (errorMessages.contains(PwmError.PASSWORD_NOT_ENOUGH_UPPER)) {
addRandChar(password, seedMachine.getUpperChars());
touched = true;
}
if (errorMessages.contains(PwmError.PASSWORD_NOT_ENOUGH_LOWER)) {
addRandChar(password, seedMachine.getLowerChars());
touched = true;
}
PasswordCharCounter passwordCharCounter = new PasswordCharCounter(password.toString());
if (errorMessages.contains(PwmError.PASSWORD_TOO_MANY_NUMERIC) && passwordCharCounter.getNumericCharCount() > 0) {
deleteRandChar(password, passwordCharCounter.getNumericChars());
touched = true;
passwordCharCounter = new PasswordCharCounter(password.toString());
}
if (errorMessages.contains(PwmError.PASSWORD_TOO_MANY_SPECIAL) && passwordCharCounter.getSpecialCharsCount() > 0) {
deleteRandChar(password, passwordCharCounter.getSpecialChars());
touched = true;
passwordCharCounter = new PasswordCharCounter(password.toString());
}
if (errorMessages.contains(PwmError.PASSWORD_TOO_MANY_UPPER) && passwordCharCounter.getUpperCharCount() > 0) {
deleteRandChar(password, passwordCharCounter.getUpperChars());
touched = true;
passwordCharCounter = new PasswordCharCounter(password.toString());
}
if (errorMessages.contains(PwmError.PASSWORD_TOO_MANY_LOWER) && passwordCharCounter.getLowerCharCount() > 0) {
deleteRandChar(password, passwordCharCounter.getLowerChars());
touched = true;
}
if (errorMessages.contains(PwmError.PASSWORD_TOO_WEAK)) {
randomPasswordModifier(password, seedMachine);
touched = true;
}
if (!touched) { // dunno whats wrong, try just deleting a RANDOM char, and hope a re-insert will add another.
randomPasswordModifier(password, seedMachine);
}
}
private static void deleteRandChar(final StringBuilder password, final String charsToRemove)
throws ImpossiblePasswordPolicyException {
final List<Integer> removePossibilities = new ArrayList<>();
for (int i = 0; i < password.length(); 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(RANDOM.nextInt(removePossibilities.size()));
password.deleteCharAt(charToDelete);
}
private static void randomPasswordModifier(final StringBuilder password, final SeedMachine seedMachine) {
switch (RANDOM.nextInt(6)) {
case 0:
case 1:
addRandChar(password, seedMachine.getSpecialChars());
break;
case 2:
case 3:
addRandChar(password, seedMachine.getNumChars());
break;
case 4:
addRandChar(password, seedMachine.getUpperChars());
break;
case 5:
addRandChar(password, seedMachine.getLowerChars());
break;
default:
switchRandomCase(password);
break;
}
}
private static void switchRandomCase(final StringBuilder password) {
for (int i = 0; i < password.length(); i++) {
final int randspot = RANDOM.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;
}
}
}
private static void addRandChar(final StringBuilder password, final String allowedChars)
throws ImpossiblePasswordPolicyException {
final int insertPosition = RANDOM.nextInt(password.length());
addRandChar(password, allowedChars, insertPosition);
}
private static void addRandChar(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 = RANDOM.nextInt(allowedChars.length());
final char charToAdd = allowedChars.charAt(newCharPosition);
password.insert(insertPosition, charToAdd);
}
}
private static boolean checkPasswordAgainstDisallowedHttpValues(final Configuration config, final String password) {
if (config != null && password != null) {
final List<String> disallowedInputs = config.readSettingAsStringArray(PwmSetting.DISALLOWED_HTTP_INPUTS);
for (final String loopRegex : disallowedInputs) {
if (password.matches(loopRegex)) {
return true;
}
}
}
return false;
}
// --------------------------- CONSTRUCTORS ---------------------------
private RandomPasswordGenerator() {
}
// -------------------------- INNER CLASSES --------------------------
protected static class SeedMachine {
private final Collection<String> seeds;
private final String allChars;
private final String numChars;
private final String specialChars;
private final String upperChars;
private final String lowerChars;
public SeedMachine(final Collection<String> seeds) {
this.seeds = seeds;
{
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(DEFAULT_SEED_PHRASES)).getAllChars();
}
{
final StringBuilder sb = new StringBuilder();
for (final Character c : allChars.toCharArray()) {
if (Character.isDigit(c)) {
sb.append(c);
}
}
numChars = sb.length() > 2 ? sb.toString() : (new SeedMachine(DEFAULT_SEED_PHRASES)).getNumChars();
}
{
final StringBuilder sb = new StringBuilder();
for (final Character c : allChars.toCharArray()) {
if (!Character.isLetterOrDigit(c)) {
sb.append(c);
}
}
specialChars = sb.length() > 2 ? sb.toString() : (new SeedMachine(DEFAULT_SEED_PHRASES)).getSpecialChars();
}
{
final StringBuilder sb = new StringBuilder();
for (final Character c : allChars.toCharArray()) {
if (Character.isLowerCase(c)) {
sb.append(c);
}
}
lowerChars = sb.length() > 0 ? sb.toString() : (new SeedMachine(DEFAULT_SEED_PHRASES)).getLowerChars();
}
{
final StringBuilder sb = new StringBuilder();
for (final Character c : allChars.toCharArray()) {
if (Character.isUpperCase(c)) {
sb.append(c);
}
}
upperChars = sb.length() > 0 ? sb.toString() : (new SeedMachine(DEFAULT_SEED_PHRASES)).getUpperChars();
}
}
public String getRandomSeed() {
return new ArrayList<>(seeds).get(RANDOM.nextInt(seeds.size()));
}
public String getAllChars() {
return allChars;
}
public String getNumChars() {
return numChars;
}
public String getSpecialChars() {
return specialChars;
}
public String getUpperChars() {
return upperChars;
}
public String getLowerChars() {
return lowerChars;
}
}
private static String generateNewPassword(final SeedMachine seedMachine, final int desiredLength) {
final StringBuilder password = new StringBuilder();
while (password.length() < (desiredLength - 1)) {//loop around until we're long enough
password.append(seedMachine.getRandomSeed());
}
if (RANDOM.nextInt(3) == 0) {
addRandChar(password, DEFAULT_SEED_MACHINE.getNumChars(), RANDOM.nextInt(password.length()));
}
if (RANDOM.nextBoolean()) {
switchRandomCase(password);
}
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<>();
newSeeds.addAll(inputSeeds);
for (final Iterator<String> iter = newSeeds.iterator(); iter.hasNext(); ) {
final String s = iter.next();
if (s == null || s.length() < 1) {
iter.remove();
}
}
return newSeeds.isEmpty() ? DEFAULT_SEED_PHRASES : newSeeds;
}
public static class RandomGeneratorConfig {
public static final int DEFAULT_MINIMUM_LENGTH = 6;
public static final int DEFAULT_MAXIMUM_LENGTH = 16;
public static final int DEFAULT_DESIRED_STRENGTH = 45;
public static final int MINIMUM_STRENGTH = 0;
public static final int MAXIMUM_STRENGTH = 100;
private Collection<String> seedlistPhrases = Collections.emptySet();
private int minimumLength = DEFAULT_MINIMUM_LENGTH;
private int maximumLength = DEFAULT_MAXIMUM_LENGTH;
private int minimumStrength = DEFAULT_DESIRED_STRENGTH;
private PwmPasswordPolicy passwordPolicy = PwmPasswordPolicy.defaultPolicy();
public Collection<String> getSeedlistPhrases() {
return seedlistPhrases;
}
/**
* @param seedlistPhrases A set of phrases (Strings) used to generate the RANDOM passwords. There must be enough
* values in the phrases to build a resonably RANDOM password that meets rule requirements
*/
public void setSeedlistPhrases(final Collection<String> seedlistPhrases) {
this.seedlistPhrases = seedlistPhrases;
}
public int getMinimumLength() {
return minimumLength;
}
/**
* @param minimumLength The minimum length desired for the password. The algorith will attempt to make
* the returned value at least this long, but it is not guarenteed.
*/
public void setMinimumLength(final int minimumLength) {
this.minimumLength = minimumLength;
}
public int getMaximumLength() {
return maximumLength;
}
public void setMaximumLength(final int maximumLength) {
this.maximumLength = maximumLength;
}
public int getMinimumStrength() {
return minimumStrength;
}
/**
* @param minimumStrength The minimum length desired strength. The algorith will attempt to make
* the returned value at least this strong, but it is not guarenteed.
*/
public void setMinimumStrength(final int minimumStrength) {
int desiredStrength = minimumStrength > MAXIMUM_STRENGTH ? MAXIMUM_STRENGTH : minimumStrength;
desiredStrength = desiredStrength < MINIMUM_STRENGTH ? MINIMUM_STRENGTH : desiredStrength;
this.minimumStrength = desiredStrength;
}
public PwmPasswordPolicy getPasswordPolicy() {
return passwordPolicy;
}
public void setPasswordPolicy(final PwmPasswordPolicy passwordPolicy) {
this.passwordPolicy = passwordPolicy;
}
}
public static void validateSettings(final PwmApplication pwmApplication, final RandomGeneratorConfig randomGeneratorConfig)
throws PwmUnrecoverableException
{
final int maxLength = Integer.parseInt(
pwmApplication.getConfig().readAppProperty(AppProperty.PASSWORD_RANDOMGEN_MAX_LENGTH));
if (randomGeneratorConfig.getMinimumLength() > maxLength) {
throw new PwmUnrecoverableException(new ErrorInformation(
PwmError.ERROR_UNKNOWN,
"minimum random generated password length exceeds preset random generator threshold"
));
}
if (randomGeneratorConfig.getMaximumLength() > maxLength) {
throw new PwmUnrecoverableException(new ErrorInformation(
PwmError.ERROR_UNKNOWN,
"maximum random generated password length exceeds preset random generator threshold"
));
}
if (randomGeneratorConfig.getMinimumStrength() > RandomGeneratorConfig.MAXIMUM_STRENGTH) {
throw new PwmUnrecoverableException(new ErrorInformation(
PwmError.ERROR_UNKNOWN,
"minimum random generated password strength exceeds maximum possible"
));
}
}
}