PasswordConfigurableStrategy.java
/*
* Copyright (C) 2020-2023 Dipl.-Inform. Kai Hofmann. All rights reserved!
*/
package de.powerstat.validation.values.strategies;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import de.powerstat.validation.containers.NTuple9;
/**
* Configurable password strategy.
*
* TODO rainbow tables
* TODO https://haveibeenpwned.com/
*
* TODO struct parameter
*
* TODO chain strategy
*/
public class PasswordConfigurableStrategy implements IPasswordStrategy
{
/**
* Cache for singletons.
*/
private static final Map<NTuple9<Integer, Integer, String, Integer, Integer, Integer, Integer, Integer, Integer>, PasswordConfigurableStrategy> CACHE = new ConcurrentHashMap<>();
/**
* Minimum allowed username length.
*/
private final int minLength;
/**
* Maximum allowed username length.
*/
private final int maxLength;
/**
* Regular expression for matching allowed characters.
*/
private final String regexp;
/**
* Minimum number of required numeric characters.
*/
private final int minNumeric;
/**
* Minimum number of lower case characters.
*/
private final int minLower;
/**
* Minimum number of upper case characters.
*/
private final int minUpper;
/**
* Minimum number of special characters.
*/
private final int minSpecial;
/**
* Minimum number of unique characters.
*/
private final int minUnique;
/**
* Maximum number of allowed repeated characters after each other.
*/
private final int maxRepeated;
/**
* Constructor.
*
* @param minLength Minimum allowed username length, must be >= 1
* @param maxLength Maximum allowed username length, must be >= minLength and <= INTEGER.MAX_VALUE
* @param regexp Regular expression for matching characters. Must start with ^ and end with $. Example: ^[@./_0-9a-zA-Z-]+$
* @param minNumeric Minimum required numeric characters
* @param minLower Minimum required lower case characters
* @param minUpper Minimum required upper case characters
* @param minSpecial Minimum required special characters
* @param minUnique Minimum required unique characters
* @param maxRepeated Maximum number of allowed repeated characters after each other
* @throws IllegalArgumentException If arguments are not as required
* @throws NullPointerException If regexp is null
* TODO parameters via struct
*/
protected PasswordConfigurableStrategy(final int minLength, final int maxLength, final String regexp, final int minNumeric, final int minLower, final int minUpper, final int minSpecial, final int minUnique, final int maxRepeated)
{
super();
Objects.requireNonNull(regexp, "regexp"); //$NON-NLS-1$
if (minLength <= 0)
{
throw new IllegalArgumentException("minLength must be >= 1"); //$NON-NLS-1$
}
if (maxLength < minLength)
{
throw new IllegalArgumentException("maxLength >= minLength"); //$NON-NLS-1$
}
if ((regexp.charAt(0) != '^') || !regexp.endsWith("$")) //$NON-NLS-1$
{
throw new IllegalArgumentException("regexp does not start with ^ or ends with $"); //$NON-NLS-1$
}
if ((minNumeric < 0) || (minNumeric > maxLength))
{
throw new IllegalArgumentException("minNumeric must be >= 0 && <= maxLength"); //$NON-NLS-1$
}
if ((minLower < 0) || (minLower > maxLength))
{
throw new IllegalArgumentException("minLower must be >= 0 && <= maxLength"); //$NON-NLS-1$
}
if ((minUpper < 0) || (minUpper > maxLength))
{
throw new IllegalArgumentException("minUpper must be >= 0 && <= maxLength"); //$NON-NLS-1$
}
if ((minSpecial < 0) || (minSpecial > maxLength))
{
throw new IllegalArgumentException("minSpecial must be >= 0 && <= maxLength"); //$NON-NLS-1$
}
if ((minNumeric + minLower + minUpper + minSpecial) > maxLength)
{
throw new IllegalArgumentException("minNumeric + minLower + minUpper + minSpecial > maxLength"); //$NON-NLS-1$
}
if ((minUnique < 0) || (minUnique > maxLength))
{
throw new IllegalArgumentException("minUnique must be >= 0 && <= maxLength"); //$NON-NLS-1$
}
if ((maxRepeated < 0) || (maxRepeated > maxLength))
{
throw new IllegalArgumentException("maxRepeated must be >= 0 && <= maxLength"); //$NON-NLS-1$
}
this.minLength = minLength;
this.maxLength = maxLength;
this.regexp = regexp;
this.minNumeric = minNumeric;
this.minLower = minLower;
this.minUpper = minUpper;
this.minSpecial = minSpecial;
this.minUnique = minUnique;
this.maxRepeated = maxRepeated;
}
/**
* Password validation strategy factory.
*
* @param minLength Minimum allowed username length, must be >= 1
* @param maxLength Maximum allowed username length, must be >= minLength and <= INTEGER.MAX_VALUE
* @param regexp Regular expression for matching characters. Must start with ^ and end with $. Example: ^[@./_0-9a-zA-Z-]+$
* @param minNumeric Minimum required numeric characters
* @param minLower Minimum required lower case characters
* @param minUpper Minimum required upper case characters
* @param minSpecial Minimum required special characters
* @param minUnique Minimum required unique characters
* @param maxRepeated Maximum number of allowed repeated characters after each other
* @return UsernameStrategy object
* @throws IllegalArgumentException If arguments
* @throws NullPointerException If regexp is null
*/
public static IPasswordStrategy of(final int minLength, final int maxLength, final String regexp, final int minNumeric, final int minLower, final int minUpper, final int minSpecial, final int minUnique, final int maxRepeated)
{
final NTuple9<Integer, Integer, String, Integer, Integer, Integer, Integer, Integer, Integer> tuple = NTuple9.of(minLength, maxLength, regexp, minNumeric, minLower, minUpper, minSpecial, minUnique, maxRepeated);
synchronized (PasswordConfigurableStrategy.class)
{
PasswordConfigurableStrategy obj = PasswordConfigurableStrategy.CACHE.get(tuple);
if (obj != null)
{
return obj;
}
obj = new PasswordConfigurableStrategy(minLength, maxLength, regexp, minNumeric, minLower, minUpper, minSpecial, minUnique, maxRepeated);
PasswordConfigurableStrategy.CACHE.put(tuple, obj);
return obj;
}
}
/**
* Validation strategy.
*
* @param password Password
* @throws IllegalArgumentException If the password does not match the configured parameters
*/
@Override
public void validationStrategy(final String password)
{
if ((password.length() < this.minLength) || (password.length() > this.maxLength))
{
throw new IllegalArgumentException("To short or long for a password"); //$NON-NLS-1$
}
if (!password.matches(this.regexp))
{
throw new IllegalArgumentException("Password contains illegal character"); //$NON-NLS-1$
}
int upper = 0;
int lower = 0;
int numeric = 0;
int special = 0;
int same = 1;
char lastChar = '\0';
final Set<Character> cset = new HashSet<>(password.length());
for (int i = 0; i < password.length(); ++i)
{
final char chr = password.charAt(i);
cset.add(chr);
if (Character.isUpperCase(chr))
{
++upper;
}
else if (Character.isLowerCase(chr))
{
++lower;
}
else if (Character.isDigit(chr))
{
++numeric;
}
else
{
++special;
}
if (chr == lastChar)
{
++same;
if ((this.maxRepeated != 0) && (same > this.maxRepeated))
{
throw new IllegalArgumentException("To much repeated characters after each other in password"); //$NON-NLS-1$
}
}
else
{
same = 1;
lastChar = chr;
}
}
if (numeric < this.minNumeric)
{
throw new IllegalArgumentException("Not enougth numeric characters in password"); //$NON-NLS-1$
}
if (lower < this.minLower)
{
throw new IllegalArgumentException("Not enougth lower case characters in password"); //$NON-NLS-1$
}
if (upper < this.minUpper)
{
throw new IllegalArgumentException("Not enougth upper case characters in password"); //$NON-NLS-1$
}
if (special < this.minSpecial)
{
throw new IllegalArgumentException("Not enougth special characters in password"); //$NON-NLS-1$
}
if (cset.size() < this.minUnique)
{
throw new IllegalArgumentException("Not enougth unique characters in password"); //$NON-NLS-1$
}
}
}