Add strong salted password hashing using PBKDF2
This commit is contained in:
		| @@ -63,7 +63,7 @@ public class User extends Contact { | ||||
| 	public static final String searchByName = "User.searchByName"; | ||||
|  | ||||
| 	@Column(name = "password_hash") | ||||
| 	private byte[] passwordHash; | ||||
| 	private String passwordHash; | ||||
|  | ||||
| 	@Column(name = "last_seen") | ||||
| 	private LocalDateTime lastSeen; | ||||
| @@ -80,15 +80,15 @@ public class User extends Contact { | ||||
|  | ||||
| 	/** | ||||
| 	 * @return the password hash | ||||
| 	 * @since Envoy Server Standalone v0.1-alpha | ||||
| 	 * @since Envoy Server Standalone v0.1-beta | ||||
| 	 */ | ||||
| 	public byte[] getPasswordHash() { return passwordHash; } | ||||
| 	public String getPasswordHash() { return passwordHash; } | ||||
|  | ||||
| 	/** | ||||
| 	 * @param passwordHash the password hash to set | ||||
| 	 * @since Envoy Server Standalone v0.1-alpha | ||||
| 	 * @since Envoy Server Standalone v0.1-beta | ||||
| 	 */ | ||||
| 	public void setPasswordHash(byte[] passwordHash) { this.passwordHash = passwordHash; } | ||||
| 	public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; } | ||||
|  | ||||
| 	/** | ||||
| 	 * @return the last date the user has been online | ||||
|   | ||||
| @@ -5,7 +5,9 @@ import static envoy.data.User.UserStatus.ONLINE; | ||||
| import static envoy.event.HandshakeRejection.*; | ||||
|  | ||||
| import java.time.LocalDateTime; | ||||
| import java.util.*; | ||||
| import java.util.Collections; | ||||
| import java.util.HashSet; | ||||
| import java.util.List; | ||||
| import java.util.logging.Logger; | ||||
|  | ||||
| import javax.persistence.NoResultException; | ||||
| @@ -19,7 +21,8 @@ import envoy.server.data.PersistenceManager; | ||||
| import envoy.server.data.User; | ||||
| import envoy.server.net.ConnectionManager; | ||||
| import envoy.server.net.ObjectWriteProxy; | ||||
| import envoy.server.util.VersionUtils; | ||||
| import envoy.server.util.PasswordUtil; | ||||
| import envoy.server.util.VersionUtil; | ||||
| import envoy.util.Bounds; | ||||
| import envoy.util.EnvoyLog; | ||||
|  | ||||
| @@ -47,7 +50,7 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred | ||||
| 		// Cache this write proxy for user-independant notifications | ||||
| 		UserStatusChangeProcessor.setWriteProxy(writeProxy); | ||||
|  | ||||
| 		if (!VersionUtils.verifyCompatibility(credentials.getClientVersion())) { | ||||
| 		if (!VersionUtil.verifyCompatibility(credentials.getClientVersion())) { | ||||
| 			logger.info("The client has the wrong version."); | ||||
| 			writeProxy.write(socketID, new HandshakeRejection(WRONG_VERSION)); | ||||
| 			return; | ||||
| @@ -66,7 +69,7 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred | ||||
| 					return; | ||||
| 				} | ||||
| 				// Evaluating the correctness of the password hash | ||||
| 				if (!Arrays.equals(credentials.getPasswordHash(), user.getPasswordHash())) { | ||||
| 				if (!PasswordUtil.validate(credentials.getPassword(), user.getPasswordHash())) { | ||||
| 					logger.info(user + " has entered the wrong password."); | ||||
| 					writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER)); | ||||
| 					return; | ||||
| @@ -97,7 +100,7 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred | ||||
| 				user.setName(credentials.getIdentifier()); | ||||
| 				user.setLastSeen(LocalDateTime.now()); | ||||
| 				user.setStatus(ONLINE); | ||||
| 				user.setPasswordHash(credentials.getPasswordHash()); | ||||
| 				user.setPasswordHash(PasswordUtil.hash(credentials.getPassword())); | ||||
| 				user.setContacts(new HashSet<>()); | ||||
| 				persistenceManager.addContact(user); | ||||
| 				logger.info("Registered new " + user); | ||||
|   | ||||
							
								
								
									
										98
									
								
								src/main/java/envoy/server/util/PasswordUtil.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								src/main/java/envoy/server/util/PasswordUtil.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | ||||
| package envoy.server.util; | ||||
|  | ||||
| import java.math.BigInteger; | ||||
| import java.security.GeneralSecurityException; | ||||
| import java.security.NoSuchAlgorithmException; | ||||
| import java.security.SecureRandom; | ||||
|  | ||||
| import javax.crypto.SecretKeyFactory; | ||||
| import javax.crypto.spec.PBEKeySpec; | ||||
|  | ||||
| /** | ||||
|  * Provides a password hashing and comparison mechanism using the | ||||
|  * {@code PBKDF2WithHmacSHA1} algorithm. | ||||
|  * <p> | ||||
|  * Project: <strong>envoy-server-standalone</strong><br> | ||||
|  * File: <strong>PasswordUtil.java</strong><br> | ||||
|  * Created: <strong>10.07.2020</strong><br> | ||||
|  *  | ||||
|  * @author Kai S. K. Engelbart | ||||
|  * @since Envoy Server Standalone v0.1-beta | ||||
|  */ | ||||
| public 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; | ||||
| 	} | ||||
| } | ||||
| @@ -7,13 +7,13 @@ import java.util.regex.Pattern; | ||||
|  * and maximal client versions compatible with this server. | ||||
|  * <p> | ||||
|  * Project: <strong>envoy-server-standalone</strong><br> | ||||
|  * File: <strong>VersionUtils.java</strong><br> | ||||
|  * File: <strong>VersionUtil.java</strong><br> | ||||
|  * Created: <strong>23.06.2020</strong><br> | ||||
|  *  | ||||
|  * @author Kai S. K. Engelbart | ||||
|  * @since Envoy Server Standalone v0.1-beta | ||||
|  */ | ||||
| public class VersionUtils { | ||||
| public class VersionUtil { | ||||
| 
 | ||||
| 	/** | ||||
| 	 * The minimal client version compatible with this server. | ||||
| @@ -31,7 +31,7 @@ public class VersionUtils { | ||||
| 
 | ||||
| 	private static final Pattern versionPattern = Pattern.compile("(?<major>\\d).(?<minor>\\d)(?:-(?<suffix>\\w+))?"); | ||||
| 
 | ||||
| 	private VersionUtils() {} | ||||
| 	private VersionUtil() {} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Parses an Envoy Client version string and checks whether that version is | ||||
		Reference in New Issue
	
	Block a user