package envoy.server.util; import java.math.BigInteger; import java.security.*; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; /** * Provides a password hashing and comparison mechanism using the {@code PBKDF2WithHmacSHA1} * algorithm. * * @author Kai S. K. Engelbart * @since Envoy Server Standalone v0.1-beta */ public final class PasswordUtil { private static final int ITERATIONS = 1000; private static final int KEY_LENGTH = 64 * 8; private static final String SALT_ALGORITHM = "SHA1PRNG"; private static final String SECRET_KEY_ALGORITHM = "PBKDF2WithHmacSHA1"; private PasswordUtil() {} /** * Validates a password against a stored password hash * * @param current the password to validate * @param stored the hash to validate against * @return {@code true} if the password is correct * @since Envoy Server Standalone v0.1-beta */ public static boolean validate(String current, String stored) { try { String[] parts = stored.split(":"); int iterations = Integer.parseInt(parts[0]); byte[] salt = fromHex(parts[1]); byte[] hash = fromHex(parts[2]); var spec = new PBEKeySpec(current.toCharArray(), salt, iterations, KEY_LENGTH); var skf = SecretKeyFactory.getInstance(SECRET_KEY_ALGORITHM); byte[] testHash = skf.generateSecret(spec).getEncoded(); int diff = hash.length ^ testHash.length; for (int i = 0; i < hash.length && i < testHash.length; ++i) diff |= hash[i] ^ testHash[i]; return diff == 0; } catch (GeneralSecurityException e) { throw new RuntimeException(e); } } /** * Creates a parameterized salted password hash. * * @param password the password to hash * @return a result string in the form of {@code iterations:salt:hash} * @since Envoy Server Standalone v0.1-beta */ public static String hash(String password) { try { byte[] salt = salt(); var spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, KEY_LENGTH); var skf = SecretKeyFactory.getInstance(SECRET_KEY_ALGORITHM); byte[] hash = skf.generateSecret(spec).getEncoded(); return ITERATIONS + ":" + toHex(salt) + ":" + toHex(hash); } catch (GeneralSecurityException e) { throw new RuntimeException(e); } } private static byte[] salt() throws NoSuchAlgorithmException { SecureRandom sr = SecureRandom.getInstance(SALT_ALGORITHM); byte[] salt = new byte[16]; sr.nextBytes(salt); return salt; } private static String toHex(byte[] bytes) { String hex = new BigInteger(1, bytes).toString(16); int padding = bytes.length * 2 - hex.length(); return padding > 0 ? String.format("%0" + padding + "d", 0) + hex : hex; } private static byte[] fromHex(String hex) { byte[] bytes = new byte[hex.length() / 2]; for (int i = 0; i < bytes.length; ++i) bytes[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16); return bytes; } }