]> git.mxchange.org Git - juser-login-core.git/blob - src/org/mxchange/juserlogincore/login/UserLoginUtils.java
Continued:
[juser-login-core.git] / src / org / mxchange / juserlogincore / login / UserLoginUtils.java
1 /*
2  * Copyright (C) 2016 - 2020 Free Software Foundation
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17 package org.mxchange.juserlogincore.login;
18
19 import java.io.Serializable;
20 import java.security.SecureRandom;
21 import java.text.MessageFormat;
22 import java.util.Random;
23 import java.util.regex.Matcher;
24 import java.util.regex.Pattern;
25 import org.apache.commons.codec.digest.Crypt;
26 import org.apache.commons.codec.digest.DigestUtils;
27 import org.mxchange.jcontacts.model.contact.Contact;
28 import org.mxchange.jusercore.model.user.User;
29 import org.mxchange.jusercore.model.user.Users;
30 import org.mxchange.juserlogincore.container.login.LoginContainer;
31
32 /**
33  * An utilities class for user logins
34  * <p>
35  * @author Roland Häder<roland@mxchange.org>
36  */
37 public class UserLoginUtils implements Serializable {
38
39         /**
40          * Password alphabet
41          */
42         private static final String PASSWORD_ALPHABET;
43
44         /**
45          * Password alphabet parts
46          */
47         private static final String[] PASSWORD_ALPHABET_PARTS = {
48                 // lower-case
49                 "abcdefghijklmnopqrstuvwxyz", //NOI18N
50
51                 // upper-case
52                 "ABCDEFGHIJKLMNOPQRSTUVWXYZ", //NOI18N
53
54                 // numbers
55                 "0123456789", //NOI18N
56
57                 // characters
58                 "~^!$%&/()=?{[]}@+*#-_,.;:<|>" //NOI18N
59         };
60
61         /**
62          * Hard-coded minimum password length
63          */
64         private static final Integer PASSWORD_MINIMUM_LENGTH = 5;
65
66         /**
67          * Random number generator
68          */
69         private static final Random RANDOM_NUMBER_GENERATOR;
70
71         /**
72          * Serial number
73          */
74         private static final long serialVersionUID = 18_356_847_120_972L;
75
76         /**
77          * Static initializer
78          */
79         static {
80                 // Init RNG
81                 RANDOM_NUMBER_GENERATOR = new SecureRandom();
82
83                 // Init alphabet
84                 PASSWORD_ALPHABET = UserLoginUtils.PASSWORD_ALPHABET_PARTS[0] +
85                                                         UserLoginUtils.PASSWORD_ALPHABET_PARTS[1] +
86                                                         UserLoginUtils.PASSWORD_ALPHABET_PARTS[2] +
87                                                         UserLoginUtils.PASSWORD_ALPHABET_PARTS[3];
88         }
89
90         /**
91          * Calculates entropy value for given password, higher means better. This
92          * method is based on
93          * http://stackoverflow.com/questions/14591701/how-to-check-password-strength-in-vaadin
94          * <p>
95          * @param password Clear text password
96          * <p>
97          * @return Entropy factor
98          */
99         public static double calculateEntropyFactor (final String password) {
100                 // Should not be null
101                 if (null == password) {
102                         // Throw NPE
103                         throw new NullPointerException("password is null"); //NOI18N
104                 }
105
106                 // Calculate it
107                 double entropyFactor = password.length() * Math.log10(PASSWORD_ALPHABET.length()) / Math.log10(2);
108
109                 // Return it
110                 return entropyFactor;
111         }
112
113         /**
114          * Determines given password's strength: 0 = worst, 100 = best. This method
115          * is based on
116          * http://stackoverflow.com/questions/1614811/how-do-i-measure-the-strength-of-a-password
117          * <p>
118          * @param password Clear-text password
119          * <p>
120          * @return Strength of password
121          */
122         public static double calculatePasswordScore (final String password) {
123                 // Should not be null
124                 if (null == password) {
125                         // Throw NPE
126                         throw new NullPointerException("password is null"); //NOI18N
127                 } else if (password.isEmpty()) {
128                         // Is empty
129                         return 0.0f;
130                 }
131
132                 // Init score
133                 double score = 0.0f;
134
135                 // Password length
136                 score += password.length() * calculateEntropyFactor(password) / 100;
137
138                 // Password has 3 numbers
139                 if (ifRegExFoundInString("(.*[0-9].*[0-9].*[0-9].*)+", password)) { //NOI18N
140                         score += 5;
141                 }
142
143                 // Password has 2 symbols
144                 if (ifRegExFoundInString("(.*[!,@,#,$,%,^,&,*,/,?,_,~,=,.,-,;,:].*[!,@,#,$,%,^,&,*,/,?,_,~,=,.,-,;,:].*)+", password)) { //NOI18N
145                         score += 5;
146                 }
147
148                 // Password has Upper and Lower chars
149                 if (ifRegExFoundInString("(.*[a-z].*[A-Z])|([A-Z].*[a-z].*)+", password)) { //NOI18N
150                         score += 10;
151                 }
152
153                 // Password has number and chars
154                 if (ifRegExFoundInString("(.*[a-zA-Z].*)+", password) && ifRegExFoundInString("(.*[0-9].*)+", password)) { //NOI18N
155                         score += 15;
156                 }
157
158                 // Password has number and symbol
159                 if (ifRegExFoundInString("(.*[!,@,#,$,%,^,&,*,/,?,_,~,=,.,-,;,:].*)+", password) && ifRegExFoundInString("(.*[0-9].*)+", password)) { //NOI18N
160                         score += 15;
161                 }
162
163                 // Password has char and symbol
164                 if (ifRegExFoundInString("(.*[!,@,#,$,%,^,&,*,/,?,_,~,=,.,-,;,:].*)+", password) && ifRegExFoundInString("(.*[a-zA-Z].*)+", password)) { //NOI18N
165                         score += 15;
166                 }
167
168                 // Password is just numbers or chars
169                 if (ifRegExFoundInString("^[a-zA-Z]+$", password) || ifRegExFoundInString("^[0-9]+$", password)) { //NOI18N
170                         score -= 10;
171                 }
172
173                 // Larger than 100 is not allowed
174                 score = Math.max(Math.min(score, 100.0f), 0.0f);
175
176                 // Return it
177                 return score;
178         }
179
180         /**
181          * Creates a pseudo-random password with given length
182          * <p>
183          * @param length Length of the password
184          * <p>
185          * @return Pseudo-random password
186          */
187         public static String createRandomPassword (final Integer length) {
188                 // Parameter should be valid
189                 if (null == length) {
190                         // Throw NPE
191                         throw new NullPointerException("length is null"); //NOI18N
192                 } else if (length < PASSWORD_MINIMUM_LENGTH) {
193                         // To weak passwords
194                         throw new IllegalArgumentException(MessageFormat.format("Password length {0} is to short, minimum: {1}", length, PASSWORD_MINIMUM_LENGTH)); //NOI18N
195                 }
196
197                 // Init variable
198                 final StringBuilder password = new StringBuilder(length);
199
200                 // Start creating it
201                 for (int i = 0; i < length; i++) {
202                         // Take random part
203                         final String alphabet = PASSWORD_ALPHABET_PARTS[RANDOM_NUMBER_GENERATOR.nextInt(PASSWORD_ALPHABET_PARTS.length)];
204
205                         // Generate random number
206                         final int pos = RANDOM_NUMBER_GENERATOR.nextInt(alphabet.length());
207
208                         // Get char at this position and add it to the final password
209                         password.append(String.valueOf(alphabet.charAt(pos)));
210                 }
211
212                 // Should have the wanted length
213                 assert (password.length() == length) : MessageFormat.format("Password length {0} doesn't match requested: {1}", password.length(), length); //NOI18N
214
215                 // Return it
216                 return password.toString();
217         }
218
219         /**
220          * Hashes given user password and adds a salt to it
221          * <p>
222          * @param userPassword User password to be hashed
223          * <p>
224          * @return Hashed user password
225          */
226         public static String encryptPassword (final String userPassword) {
227                 // Is it null or empty?
228                 if (null == userPassword) {
229                         // Throw NPE
230                         throw new NullPointerException("userPassword is null"); //NOI18N
231                 } else if (userPassword.isEmpty()) {
232                         // Empty passwords are hardcoded not allowed due to security risks
233                         throw new IllegalArgumentException("userPassword is empty"); //NOI18N
234                 }
235
236                 // Generate large number
237                 final String number = Long.toString(RANDOM_NUMBER_GENERATOR.nextLong() * 10_000_000_000L);
238
239                 // Generate salt
240                 final String salt = Crypt.crypt(number);
241
242                 // First encrypt password
243                 final String encryptedPassword = Crypt.crypt(userPassword, salt);
244
245                 // Return it
246                 return encryptedPassword;
247         }
248
249         /**
250          * Generate a key suitable for confirmation. This is basicly a large and
251          * strong hash with a lop entropy.
252          * <p>
253          * @param user User instance to use as additional entropy source
254          * <p>
255          * @return Generated key
256          */
257         public static String generatedConfirmationKey (final User user) {
258                 /**
259                  * Generates random string by creating a random, encrypted password
260                  * which gives nice entropy to start with.
261                  */
262                 final StringBuilder key = new StringBuilder(encryptPassword(Users.generateRandomUserName()));
263
264                 // Is user set?
265                 if (user instanceof User) {
266                         // Add it's name, too
267                         key.append(":").append(user); //NOI18N
268
269                         // Is user name set?
270                         if (user.getUserName() instanceof String) {
271                                 // Add it
272                                 key.append(":").append(user.getUserName()); //NOI18N
273                         }
274
275                         // Is password set?
276                         if (user.getUserEncryptedPassword() instanceof String) {
277                                 // Add it, too
278                                 key.append(":").append(user.getUserEncryptedPassword()); //NOI18N
279                         }
280
281                         // Get contact instance
282                         final Contact contact = user.getUserContact();
283
284                         // Is contact set?
285                         if (contact instanceof Contact) {
286                                 // Add it, too
287                                 key.append(":").append(contact); //NOI18N
288
289                                 // Is email address set?
290                                 if (contact.getContactEmailAddress() instanceof String) {
291                                         // Add it, too
292                                         key.append(":").append(contact.getContactEmailAddress()); //NOI18N
293                                 }
294                         }
295                 }
296
297                 // Hash key
298                 final String hash = DigestUtils.sha256Hex(key.toString());
299
300                 // Return it
301                 return hash;
302         }
303
304         /**
305          * Checks if password from container matches the updatedUser's password
306          * <p>
307          * @param container   Container holding user instance and clear-text
308          *                    password
309          * @param updatedUser Updated user instance from database
310          * <p>
311          * @return Whether the password matches
312          */
313         public static boolean ifPasswordMatches (final LoginContainer container, final User updatedUser) {
314                 // Validate parameters
315                 if (null == container) {
316                         // Throw NPE
317                         throw new NullPointerException("container is null"); //NOI18N
318                 } else if (null == updatedUser) {
319                         // And again NPE ...
320                         throw new NullPointerException("updatedUser is null"); //NOI18N
321                 } else if (container.getUser() == null) {
322                         // NPE for user in container
323                         throw new NullPointerException("container.user is null"); //NOI18N
324                 } else if (container.getUserPassword() == null) {
325                         // NPE for user password in container
326                         throw new NullPointerException("container.userPassword is null"); //NOI18N
327                 } else if (container.getUserPassword().isEmpty()) {
328                         // Empty password in container
329                         throw new IllegalArgumentException("container.userPassword is empty"); //NOI18N
330                 }
331
332                 // Call below method
333                 return ifPasswordMatches(container.getUserPassword(), updatedUser);
334         }
335
336         /**
337          * Checks if direct password the updatedUser's password
338          * <p>
339          * @param clearTextPassword Clear-text (direct) password
340          * @param updatedUser       Updated user instance from database
341          * <p>
342          * @return Whether the password matches
343          */
344         public static boolean ifPasswordMatches (final String clearTextPassword, final User updatedUser) {
345                 // Validate parameters
346                 if (null == clearTextPassword) {
347                         // Throw NPE
348                         throw new NullPointerException("clearTextPassword is null"); //NOI18N
349                 } else if (clearTextPassword.isEmpty()) {
350                         // NPE for user in container
351                         throw new NullPointerException("clearTextPassword is empty."); //NOI18N
352                 } else if (null == updatedUser) {
353                         // And again NPE ...
354                         throw new NullPointerException("updatedUser is null"); //NOI18N
355                 }
356
357                 // First encrypt password
358                 final String encryptedPassword = Crypt.crypt(clearTextPassword, updatedUser.getUserEncryptedPassword());
359
360                 // Is it matching?
361                 return encryptedPassword.equals(updatedUser.getUserEncryptedPassword());
362         }
363
364         /**
365          * Checks if password from container matches with from user instance.
366          * <p>
367          * @param container Container holding user instance and clear-text password
368          * <p>
369          * @return Whether it maches
370          */
371         public static boolean ifPasswordMatches (final LoginContainer container) {
372                 // Validate parameters
373                 if (null == container) {
374                         // Throw NPE
375                         throw new NullPointerException("container is null"); //NOI18N
376                 } else if (container.getUser() == null) {
377                         // NPE for user in container
378                         throw new NullPointerException("container.user is null"); //NOI18N
379                 } else if (container.getUserPassword() == null) {
380                         // NPE for user password in container
381                         throw new NullPointerException("container.userPassword is null"); //NOI18N
382                 } else if (container.getUserPassword().isEmpty()) {
383                         // Empty password in container
384                         throw new IllegalArgumentException("container.userPassword is empty"); //NOI18N
385                 }
386
387                 // Call other method
388                 return ifPasswordMatches(container.getUserPassword(), container.getUser());
389         }
390
391         /**
392          * Checks if the regular expression is found in given string
393          * <p>
394          * @param pattern Regular expression
395          * @param str     String
396          * <p>
397          * @return Whether it is found
398          */
399         private static boolean ifRegExFoundInString (final String pattern, final String str) {
400                 // Mus be valid parameters
401                 if (null == pattern) {
402                         // Throw NPE
403                         throw new NullPointerException("pattern is null"); //NOI18N
404                 } else if (pattern.isEmpty()) {
405                         // Is empty
406                         throw new IllegalArgumentException("pattern is empty"); //NOI18N
407                 } else if (null == str) {
408                         // Throw NPE
409                         throw new NullPointerException("str is null"); //NOI18N
410                 }
411
412                 // Compile pattern
413                 Pattern r = Pattern.compile(pattern);
414
415                 // Get matcher
416                 Matcher m = r.matcher(str);
417
418                 // Check if it is found
419                 return m.find();
420         }
421
422         /**
423          * No instance from this class
424          */
425         private UserLoginUtils () {
426         }
427
428 }