PasswordSecurity.java

package sk.iway.iwcm.users;

import static sk.iway.iwcm.Tools.isAnyEmpty;

import org.apache.commons.lang3.RandomStringUtils;

import sk.iway.iwcm.Constants;
import sk.iway.iwcm.Tools;
import sk.iway.iwcm.users.crypto.Bcrypt;
import sk.iway.iwcm.users.crypto.Sha512;

/**
 *  PasswordSecurity.java
 *
 *  Contains hash functions helping in
 *  users' password managment and verification.
 *
 *@Title        webjet7
 *@Company      Interway s.r.o. (www.interway.sk)
 *@Copyright    Interway s.r.o. (c) 2001-2011
 *@author       $Author: marosurbanec $
 *@version      $Revision: 1.3 $
 *@created      Date: 4.1.2011 10:38:12
 *@modified     $Date: 2004/08/16 06:26:11 $
 */
public final class PasswordSecurity {

	private PasswordSecurity(){}

	public static String generatePassword(int length) {
		return generatePassword(length, true, true, true, true, true);
	}

	/**
	 * Generates a random string.
	 *
	 * @param length
	 * @param symbols ( e.g. @#$% )
	 * @param numbers ( e.g. 123456 )
	 * @param lowercase ( e.g. abcdefgh )
	 * @param uppercase ( e.g. ABCDEFGH )
	 * @param excludeAmbiguousCharacters ( { } [ ] ( ) / \ ' " ` ~ , ; : . < > )
	 * @return
	 */
	public static String generatePassword(int length, boolean symbols, boolean numbers, boolean lowercase, boolean uppercase, boolean excludeAmbiguousCharacters)
	{
		String symbolsChar = "@#$%&!";
		String numbersChar = "1234567890";
		String lowercaseChar = "abcdefghijklmnopqrstuvwxyz";
		String uppercaseChar = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
		String ambiguousCharactersChar = "{}[]()/\\'\"`~,;:.<>";

		StringBuilder sb = new StringBuilder();
		if (symbols) {
			sb.append(symbolsChar);
		}
		if (numbers) {
			sb.append(numbersChar);
		}
		if (lowercase) {
			sb.append(lowercaseChar);
		}
		if (uppercase) {
			sb.append(uppercaseChar);
		}
		if (!excludeAmbiguousCharacters) {
			sb.append(ambiguousCharactersChar);
		}

		return RandomStringUtils.secureStrong().next(length, sb.toString());
	}

	/**
	 * Generates a random HEX string
	 * chars = [0-9a-f]{16}, lowercase only
	 *
	 * @return {@link String} random hex string
	 */
	public static String generateSalt()
	{
		if(Constants.getString("passwordHashAlgorithm").equals("bcrypt")) {
			Bcrypt bcrypt = new Bcrypt();
			return bcrypt.generateSalt();
		} else {
			Sha512 sha512 = new Sha512();
			return sha512.generateSalt();
		}
	}

	/**
	 * Generates a hash combining password and a salt using concatenation.
	 *
	 * @param password any String
	 * @param salt Generated by generateSalt, must match ^[0-9a-f]{16}$, otherwise rejected
	 * @return [0-9a-f]{128} String
	 */
	public static String calculateHash(String password, String salt)
	{
		if(Constants.getString("passwordHashAlgorithm").equals("bcrypt") && salt.startsWith("bcrypt:")) {
			Bcrypt bcrypt = new Bcrypt();
			return bcrypt.calculateHash(password, salt); //calculateHash can handle salt prefix
		} else {
			Sha512 sha512 = new Sha512();
			return sha512.calculateHash(password, salt);
		}
	}

	public static boolean isPasswordCorrect(String password, String salt, String hash)
	{
		if(isAnyEmpty(salt, hash))
			return false;

		//CVE-2025-22228
		//BCryptPasswordEncoder.matches(CharSequence,String) will incorrectly return true for passwords larger than 72 characters as long as the first 72 characters are the same.
		if (Tools.isNotEmpty(password)) {
			String test = password.trim();
			//verify all characters are not same
			if (test.length() > 64) {
				char c = test.charAt(0);
				int counter = 0;
				for (int i = 1; i < test.length(); i++) {
					if (c == test.charAt(i)) {
						counter++;
					}
				}
				if (counter == test.length() - 1) {
					return false;
				}
			}
		}

		if(hash.startsWith("bcrypt:") && salt.startsWith("bcrypt:")) {
			//!! salt is not required, because is build into hash
			Bcrypt bcrypt = new Bcrypt();
			return bcrypt.isPasswordCorrect(password, salt, hash); //isPasswordCorrect can handle hash prefix
		} else {
			Sha512 sha512 = new Sha512();
			return sha512.isPasswordCorrect(password, salt, hash);
		}
	}
}