Merge pull request #49 from informatik-ag-ngl/f/password_in_login_credentials
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";
 | 
						public static final String searchByName = "User.searchByName";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Column(name = "password_hash")
 | 
						@Column(name = "password_hash")
 | 
				
			||||||
	private byte[] passwordHash;
 | 
						private String passwordHash;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Column(name = "last_seen")
 | 
						@Column(name = "last_seen")
 | 
				
			||||||
	private LocalDateTime lastSeen;
 | 
						private LocalDateTime lastSeen;
 | 
				
			||||||
@@ -80,15 +80,15 @@ public class User extends Contact {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * @return the password hash
 | 
						 * @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
 | 
						 * @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
 | 
						 * @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 static envoy.event.HandshakeRejection.*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.time.LocalDateTime;
 | 
					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 java.util.logging.Logger;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import javax.persistence.NoResultException;
 | 
					import javax.persistence.NoResultException;
 | 
				
			||||||
@@ -19,7 +21,8 @@ import envoy.server.data.PersistenceManager;
 | 
				
			|||||||
import envoy.server.data.User;
 | 
					import envoy.server.data.User;
 | 
				
			||||||
import envoy.server.net.ConnectionManager;
 | 
					import envoy.server.net.ConnectionManager;
 | 
				
			||||||
import envoy.server.net.ObjectWriteProxy;
 | 
					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.Bounds;
 | 
				
			||||||
import envoy.util.EnvoyLog;
 | 
					import envoy.util.EnvoyLog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -47,7 +50,7 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
 | 
				
			|||||||
		// Cache this write proxy for user-independant notifications
 | 
							// Cache this write proxy for user-independant notifications
 | 
				
			||||||
		UserStatusChangeProcessor.setWriteProxy(writeProxy);
 | 
							UserStatusChangeProcessor.setWriteProxy(writeProxy);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!VersionUtils.verifyCompatibility(credentials.getClientVersion())) {
 | 
							if (!VersionUtil.verifyCompatibility(credentials.getClientVersion())) {
 | 
				
			||||||
			logger.info("The client has the wrong version.");
 | 
								logger.info("The client has the wrong version.");
 | 
				
			||||||
			writeProxy.write(socketID, new HandshakeRejection(WRONG_VERSION));
 | 
								writeProxy.write(socketID, new HandshakeRejection(WRONG_VERSION));
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
@@ -66,7 +69,7 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
 | 
				
			|||||||
					return;
 | 
										return;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				// Evaluating the correctness of the password hash
 | 
									// 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.");
 | 
										logger.info(user + " has entered the wrong password.");
 | 
				
			||||||
					writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER));
 | 
										writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER));
 | 
				
			||||||
					return;
 | 
										return;
 | 
				
			||||||
@@ -97,7 +100,7 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
 | 
				
			|||||||
				user.setName(credentials.getIdentifier());
 | 
									user.setName(credentials.getIdentifier());
 | 
				
			||||||
				user.setLastSeen(LocalDateTime.now());
 | 
									user.setLastSeen(LocalDateTime.now());
 | 
				
			||||||
				user.setStatus(ONLINE);
 | 
									user.setStatus(ONLINE);
 | 
				
			||||||
				user.setPasswordHash(credentials.getPasswordHash());
 | 
									user.setPasswordHash(PasswordUtil.hash(credentials.getPassword()));
 | 
				
			||||||
				user.setContacts(new HashSet<>());
 | 
									user.setContacts(new HashSet<>());
 | 
				
			||||||
				persistenceManager.addContact(user);
 | 
									persistenceManager.addContact(user);
 | 
				
			||||||
				logger.info("Registered new " + 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.
 | 
					 * and maximal client versions compatible with this server.
 | 
				
			||||||
 * <p>
 | 
					 * <p>
 | 
				
			||||||
 * Project: <strong>envoy-server-standalone</strong><br>
 | 
					 * 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>
 | 
					 * Created: <strong>23.06.2020</strong><br>
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
 * @author Kai S. K. Engelbart
 | 
					 * @author Kai S. K. Engelbart
 | 
				
			||||||
 * @since Envoy Server Standalone v0.1-beta
 | 
					 * @since Envoy Server Standalone v0.1-beta
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
public class VersionUtils {
 | 
					public class VersionUtil {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * The minimal client version compatible with this server.
 | 
						 * 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 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
 | 
						 * Parses an Envoy Client version string and checks whether that version is
 | 
				
			||||||
		Reference in New Issue
	
	Block a user