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:
commit
d503655534
@ -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