2 * Copyright (C) 2016 - 2020 Free Software Foundation
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.
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.
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/>.
17 package org.mxchange.juserlogincore.login;
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;
33 * An utilities class for user logins
35 * @author Roland Häder<roland@mxchange.org>
37 public class UserLoginUtils implements Serializable {
42 private static final String PASSWORD_ALPHABET;
45 * Password alphabet parts
47 private static final String[] PASSWORD_ALPHABET_PARTS = {
49 "abcdefghijklmnopqrstuvwxyz", //NOI18N
52 "ABCDEFGHIJKLMNOPQRSTUVWXYZ", //NOI18N
55 "0123456789", //NOI18N
58 "~^!$%&/()=?{[]}@+*#-_,.;:<|>" //NOI18N
62 * Hard-coded minimum password length
64 private static final Integer PASSWORD_MINIMUM_LENGTH = 5;
67 * Random number generator
69 private static final Random RANDOM_NUMBER_GENERATOR;
74 private static final long serialVersionUID = 18_356_847_120_972L;
81 RANDOM_NUMBER_GENERATOR = new SecureRandom();
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];
91 * Calculates entropy value for given password, higher means better. This
93 * http://stackoverflow.com/questions/14591701/how-to-check-password-strength-in-vaadin
95 * @param password Clear text password
97 * @return Entropy factor
99 public static double calculateEntropyFactor (final String password) {
100 // Should not be null
101 if (null == password) {
103 throw new NullPointerException("password is null"); //NOI18N
107 double entropyFactor = password.length() * Math.log10(PASSWORD_ALPHABET.length()) / Math.log10(2);
110 return entropyFactor;
114 * Determines given password's strength: 0 = worst, 100 = best. This method
116 * http://stackoverflow.com/questions/1614811/how-do-i-measure-the-strength-of-a-password
118 * @param password Clear-text password
120 * @return Strength of password
122 public static double calculatePasswordScore (final String password) {
123 // Should not be null
124 if (null == password) {
126 throw new NullPointerException("password is null"); //NOI18N
127 } else if (password.isEmpty()) {
136 score += password.length() * calculateEntropyFactor(password) / 100;
138 // Password has 3 numbers
139 if (ifRegExFoundInString("(.*[0-9].*[0-9].*[0-9].*)+", password)) { //NOI18N
143 // Password has 2 symbols
144 if (ifRegExFoundInString("(.*[!,@,#,$,%,^,&,*,/,?,_,~,=,.,-,;,:].*[!,@,#,$,%,^,&,*,/,?,_,~,=,.,-,;,:].*)+", password)) { //NOI18N
148 // Password has Upper and Lower chars
149 if (ifRegExFoundInString("(.*[a-z].*[A-Z])|([A-Z].*[a-z].*)+", password)) { //NOI18N
153 // Password has number and chars
154 if (ifRegExFoundInString("(.*[a-zA-Z].*)+", password) && ifRegExFoundInString("(.*[0-9].*)+", password)) { //NOI18N
158 // Password has number and symbol
159 if (ifRegExFoundInString("(.*[!,@,#,$,%,^,&,*,/,?,_,~,=,.,-,;,:].*)+", password) && ifRegExFoundInString("(.*[0-9].*)+", password)) { //NOI18N
163 // Password has char and symbol
164 if (ifRegExFoundInString("(.*[!,@,#,$,%,^,&,*,/,?,_,~,=,.,-,;,:].*)+", password) && ifRegExFoundInString("(.*[a-zA-Z].*)+", password)) { //NOI18N
168 // Password is just numbers or chars
169 if (ifRegExFoundInString("^[a-zA-Z]+$", password) || ifRegExFoundInString("^[0-9]+$", password)) { //NOI18N
173 // Larger than 100 is not allowed
174 score = Math.max(Math.min(score, 100.0f), 0.0f);
181 * Creates a pseudo-random password with given length
183 * @param length Length of the password
185 * @return Pseudo-random password
187 public static String createRandomPassword (final Integer length) {
188 // Parameter should be valid
189 if (null == length) {
191 throw new NullPointerException("length is null"); //NOI18N
192 } else if (length < PASSWORD_MINIMUM_LENGTH) {
194 throw new IllegalArgumentException(MessageFormat.format("Password length {0} is to short, minimum: {1}", length, PASSWORD_MINIMUM_LENGTH)); //NOI18N
198 final StringBuilder password = new StringBuilder(length);
201 for (int i = 0; i < length; i++) {
203 final String alphabet = PASSWORD_ALPHABET_PARTS[RANDOM_NUMBER_GENERATOR.nextInt(PASSWORD_ALPHABET_PARTS.length)];
205 // Generate random number
206 final int pos = RANDOM_NUMBER_GENERATOR.nextInt(alphabet.length());
208 // Get char at this position and add it to the final password
209 password.append(String.valueOf(alphabet.charAt(pos)));
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
216 return password.toString();
220 * Hashes given user password and adds a salt to it
222 * @param userPassword User password to be hashed
224 * @return Hashed user password
226 public static String encryptPassword (final String userPassword) {
227 // Is it null or empty?
228 if (null == userPassword) {
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
236 // Generate large number
237 final String number = Long.toString(RANDOM_NUMBER_GENERATOR.nextLong() * 10_000_000_000L);
240 final String salt = Crypt.crypt(number);
242 // First encrypt password
243 final String encryptedPassword = Crypt.crypt(userPassword, salt);
246 return encryptedPassword;
250 * Generate a key suitable for confirmation. This is basicly a large and
251 * strong hash with a lop entropy.
253 * @param user User instance to use as additional entropy source
255 * @return Generated key
257 public static String generatedConfirmationKey (final User user) {
259 * Generates random string by creating a random, encrypted password
260 * which gives nice entropy to start with.
262 final StringBuilder key = new StringBuilder(encryptPassword(Users.generateRandomUserName()));
265 if (user instanceof User) {
266 // Add it's name, too
267 key.append(":").append(user); //NOI18N
270 if (user.getUserName() instanceof String) {
272 key.append(":").append(user.getUserName()); //NOI18N
276 if (user.getUserEncryptedPassword() instanceof String) {
278 key.append(":").append(user.getUserEncryptedPassword()); //NOI18N
281 // Get contact instance
282 final Contact contact = user.getUserContact();
285 if (contact instanceof Contact) {
287 key.append(":").append(contact); //NOI18N
289 // Is email address set?
290 if (contact.getContactEmailAddress() instanceof String) {
292 key.append(":").append(contact.getContactEmailAddress()); //NOI18N
298 final String hash = DigestUtils.sha256Hex(key.toString());
305 * Checks if password from container matches the updatedUser's password
307 * @param container Container holding user instance and clear-text
309 * @param updatedUser Updated user instance from database
311 * @return Whether the password matches
313 public static boolean ifPasswordMatches (final LoginContainer container, final User updatedUser) {
314 // Validate parameters
315 if (null == container) {
317 throw new NullPointerException("container is null"); //NOI18N
318 } else if (null == updatedUser) {
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
333 return ifPasswordMatches(container.getUserPassword(), updatedUser);
337 * Checks if direct password the updatedUser's password
339 * @param clearTextPassword Clear-text (direct) password
340 * @param updatedUser Updated user instance from database
342 * @return Whether the password matches
344 public static boolean ifPasswordMatches (final String clearTextPassword, final User updatedUser) {
345 // Validate parameters
346 if (null == clearTextPassword) {
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) {
354 throw new NullPointerException("updatedUser is null"); //NOI18N
357 // First encrypt password
358 final String encryptedPassword = Crypt.crypt(clearTextPassword, updatedUser.getUserEncryptedPassword());
361 return encryptedPassword.equals(updatedUser.getUserEncryptedPassword());
365 * Checks if password from container matches with from user instance.
367 * @param container Container holding user instance and clear-text password
369 * @return Whether it maches
371 public static boolean ifPasswordMatches (final LoginContainer container) {
372 // Validate parameters
373 if (null == container) {
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
388 return ifPasswordMatches(container.getUserPassword(), container.getUser());
392 * Checks if the regular expression is found in given string
394 * @param pattern Regular expression
397 * @return Whether it is found
399 private static boolean ifRegExFoundInString (final String pattern, final String str) {
400 // Mus be valid parameters
401 if (null == pattern) {
403 throw new NullPointerException("pattern is null"); //NOI18N
404 } else if (pattern.isEmpty()) {
406 throw new IllegalArgumentException("pattern is empty"); //NOI18N
407 } else if (null == str) {
409 throw new NullPointerException("str is null"); //NOI18N
413 Pattern r = Pattern.compile(pattern);
416 Matcher m = r.matcher(str);
418 // Check if it is found
423 * No instance from this class
425 private UserLoginUtils () {