View Javadoc
1   /*
2    * Copyright (C) 2020-2023 Dipl.-Inform. Kai Hofmann. All rights reserved!
3    */
4   package de.powerstat.validation.values.strategies;
5   
6   
7   import java.util.HashSet;
8   import java.util.Map;
9   import java.util.Objects;
10  import java.util.Set;
11  import java.util.concurrent.ConcurrentHashMap;
12  
13  import de.powerstat.validation.containers.NTuple9;
14  
15  
16  /**
17   * Configurable password strategy.
18   *
19   * TODO rainbow tables
20   * TODO https://haveibeenpwned.com/
21   *
22   * TODO struct parameter
23   *
24   * TODO chain strategy
25   */
26  public class PasswordConfigurableStrategy implements IPasswordStrategy
27   {
28    /**
29     * Cache for singletons.
30     */
31    private static final Map<NTuple9<Integer, Integer, String, Integer, Integer, Integer, Integer, Integer, Integer>, PasswordConfigurableStrategy> CACHE = new ConcurrentHashMap<>();
32  
33    /**
34     * Minimum allowed username length.
35     */
36    private final int minLength;
37  
38    /**
39     * Maximum allowed username length.
40     */
41    private final int maxLength;
42  
43    /**
44     * Regular expression for matching allowed characters.
45     */
46    private final String regexp;
47  
48    /**
49     * Minimum number of required numeric characters.
50     */
51    private final int minNumeric;
52  
53    /**
54     * Minimum number of lower case characters.
55     */
56    private final int minLower;
57  
58    /**
59     * Minimum number of upper case characters.
60     */
61    private final int minUpper;
62  
63    /**
64     * Minimum number of special characters.
65     */
66    private final int minSpecial;
67  
68    /**
69     * Minimum number of unique characters.
70     */
71    private final int minUnique;
72  
73    /**
74     * Maximum number of allowed repeated characters after each other.
75     */
76    private final int maxRepeated;
77  
78  
79    /**
80     * Constructor.
81     *
82     * @param minLength Minimum allowed username length, must be &gt;= 1
83     * @param maxLength Maximum allowed username length, must be &gt;= minLength and &lt;= INTEGER.MAX_VALUE
84     * @param regexp Regular expression for matching characters. Must start with ^ and end with $. Example: ^[@./_0-9a-zA-Z-]+$
85     * @param minNumeric Minimum required numeric characters
86     * @param minLower Minimum required lower case characters
87     * @param minUpper Minimum required upper case characters
88     * @param minSpecial Minimum required special characters
89     * @param minUnique Minimum required unique characters
90     * @param maxRepeated Maximum number of allowed repeated characters after each other
91     * @throws IllegalArgumentException If arguments are not as required
92     * @throws NullPointerException If regexp is null
93     * TODO parameters via struct
94     */
95    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)
96     {
97      super();
98      Objects.requireNonNull(regexp, "regexp"); //$NON-NLS-1$
99      if (minLength <= 0)
100      {
101       throw new IllegalArgumentException("minLength must be >= 1"); //$NON-NLS-1$
102      }
103     if (maxLength < minLength)
104      {
105       throw new IllegalArgumentException("maxLength >= minLength"); //$NON-NLS-1$
106      }
107     if ((regexp.charAt(0) != '^') || !regexp.endsWith("$")) //$NON-NLS-1$
108      {
109       throw new IllegalArgumentException("regexp does not start with ^ or ends with $"); //$NON-NLS-1$
110      }
111     if ((minNumeric < 0) || (minNumeric > maxLength))
112      {
113       throw new IllegalArgumentException("minNumeric must be >= 0 && <= maxLength"); //$NON-NLS-1$
114      }
115     if ((minLower < 0) || (minLower > maxLength))
116      {
117       throw new IllegalArgumentException("minLower must be >= 0 && <= maxLength"); //$NON-NLS-1$
118      }
119     if ((minUpper < 0) || (minUpper > maxLength))
120      {
121       throw new IllegalArgumentException("minUpper must be >= 0 && <= maxLength"); //$NON-NLS-1$
122      }
123     if ((minSpecial < 0) || (minSpecial > maxLength))
124      {
125       throw new IllegalArgumentException("minSpecial must be >= 0 && <= maxLength"); //$NON-NLS-1$
126      }
127     if ((minNumeric + minLower + minUpper + minSpecial) > maxLength)
128      {
129       throw new IllegalArgumentException("minNumeric + minLower + minUpper + minSpecial > maxLength"); //$NON-NLS-1$
130      }
131     if ((minUnique < 0) || (minUnique > maxLength))
132      {
133       throw new IllegalArgumentException("minUnique must be >= 0 && <= maxLength"); //$NON-NLS-1$
134      }
135     if ((maxRepeated < 0) || (maxRepeated > maxLength))
136      {
137       throw new IllegalArgumentException("maxRepeated must be >= 0 && <= maxLength"); //$NON-NLS-1$
138      }
139     this.minLength = minLength;
140     this.maxLength = maxLength;
141     this.regexp = regexp;
142     this.minNumeric = minNumeric;
143     this.minLower = minLower;
144     this.minUpper = minUpper;
145     this.minSpecial = minSpecial;
146     this.minUnique = minUnique;
147     this.maxRepeated = maxRepeated;
148    }
149 
150 
151   /**
152    * Password validation strategy factory.
153    *
154    * @param minLength Minimum allowed username length, must be &gt;= 1
155    * @param maxLength Maximum allowed username length, must be &gt;= minLength and &lt;= INTEGER.MAX_VALUE
156    * @param regexp Regular expression for matching characters. Must start with ^ and end with $. Example: ^[@./_0-9a-zA-Z-]+$
157    * @param minNumeric Minimum required numeric characters
158    * @param minLower Minimum required lower case characters
159    * @param minUpper Minimum required upper case characters
160    * @param minSpecial Minimum required special characters
161    * @param minUnique Minimum required unique characters
162    * @param maxRepeated Maximum number of allowed repeated characters after each other
163    * @return UsernameStrategy object
164    * @throws IllegalArgumentException If arguments
165    * @throws NullPointerException If regexp is null
166    */
167   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)
168    {
169     final NTuple9<Integer, Integer, String, Integer, Integer, Integer, Integer, Integer, Integer> tuple = NTuple9.of(minLength, maxLength, regexp, minNumeric, minLower, minUpper, minSpecial, minUnique, maxRepeated);
170     synchronized (PasswordConfigurableStrategy.class)
171      {
172       PasswordConfigurableStrategy obj = PasswordConfigurableStrategy.CACHE.get(tuple);
173       if (obj != null)
174        {
175         return obj;
176        }
177       obj = new PasswordConfigurableStrategy(minLength, maxLength, regexp, minNumeric, minLower, minUpper, minSpecial, minUnique, maxRepeated);
178       PasswordConfigurableStrategy.CACHE.put(tuple, obj);
179       return obj;
180      }
181    }
182 
183 
184   /**
185    * Validation strategy.
186    *
187    * @param password Password
188    * @throws IllegalArgumentException If the password does not match the configured parameters
189    */
190   @Override
191   public void validationStrategy(final String password)
192    {
193     if ((password.length() < this.minLength) || (password.length() > this.maxLength))
194      {
195       throw new IllegalArgumentException("To short or long for a password"); //$NON-NLS-1$
196      }
197     if (!password.matches(this.regexp))
198      {
199       throw new IllegalArgumentException("Password contains illegal character"); //$NON-NLS-1$
200      }
201     int upper = 0;
202     int lower = 0;
203     int numeric = 0;
204     int special = 0;
205     int same = 1;
206     char lastChar = '\0';
207     final Set<Character> cset = new HashSet<>(password.length());
208     for (int i = 0; i < password.length(); ++i)
209      {
210       final char chr = password.charAt(i);
211       cset.add(chr);
212       if (Character.isUpperCase(chr))
213        {
214         ++upper;
215        }
216       else if (Character.isLowerCase(chr))
217        {
218         ++lower;
219        }
220       else if (Character.isDigit(chr))
221        {
222         ++numeric;
223        }
224       else
225        {
226         ++special;
227        }
228       if (chr == lastChar)
229        {
230         ++same;
231         if ((this.maxRepeated != 0) && (same > this.maxRepeated))
232          {
233           throw new IllegalArgumentException("To much repeated characters after each other in password"); //$NON-NLS-1$
234          }
235        }
236       else
237        {
238         same = 1;
239         lastChar = chr;
240        }
241      }
242     if (numeric < this.minNumeric)
243      {
244       throw new IllegalArgumentException("Not enougth numeric characters in password"); //$NON-NLS-1$
245      }
246     if (lower < this.minLower)
247      {
248       throw new IllegalArgumentException("Not enougth lower case characters in password"); //$NON-NLS-1$
249      }
250     if (upper < this.minUpper)
251      {
252       throw new IllegalArgumentException("Not enougth upper case characters in password"); //$NON-NLS-1$
253      }
254     if (special < this.minSpecial)
255      {
256       throw new IllegalArgumentException("Not enougth special characters in password"); //$NON-NLS-1$
257      }
258     if (cset.size() < this.minUnique)
259      {
260       throw new IllegalArgumentException("Not enougth unique characters in password"); //$NON-NLS-1$
261      }
262     }
263 
264  }