From 1f44d0393417da7b82d8f0d5ec782a45b762f95b Mon Sep 17 00:00:00 2001 From: kske Date: Fri, 10 Jul 2020 23:25:13 +0200 Subject: [PATCH] Add strong salted password hashing using PBKDF2 --- src/main/java/envoy/server/data/User.java | 10 +- .../processors/LoginCredentialProcessor.java | 13 ++- .../java/envoy/server/util/PasswordUtil.java | 98 +++++++++++++++++++ .../{VersionUtils.java => VersionUtil.java} | 6 +- 4 files changed, 114 insertions(+), 13 deletions(-) create mode 100644 src/main/java/envoy/server/util/PasswordUtil.java rename src/main/java/envoy/server/util/{VersionUtils.java => VersionUtil.java} (96%) diff --git a/src/main/java/envoy/server/data/User.java b/src/main/java/envoy/server/data/User.java index 9a1b03f..81b4d9e 100755 --- a/src/main/java/envoy/server/data/User.java +++ b/src/main/java/envoy/server/data/User.java @@ -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 diff --git a/src/main/java/envoy/server/processors/LoginCredentialProcessor.java b/src/main/java/envoy/server/processors/LoginCredentialProcessor.java index 538ca31..24e3b46 100755 --- a/src/main/java/envoy/server/processors/LoginCredentialProcessor.java +++ b/src/main/java/envoy/server/processors/LoginCredentialProcessor.java @@ -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()); persistenceManager.addContact(user); logger.info("Registered new " + user); diff --git a/src/main/java/envoy/server/util/PasswordUtil.java b/src/main/java/envoy/server/util/PasswordUtil.java new file mode 100644 index 0000000..5d0ba7a --- /dev/null +++ b/src/main/java/envoy/server/util/PasswordUtil.java @@ -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. + *

+ * Project: envoy-server-standalone
+ * File: PasswordUtil.java
+ * Created: 10.07.2020
+ * + * @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; + } +} diff --git a/src/main/java/envoy/server/util/VersionUtils.java b/src/main/java/envoy/server/util/VersionUtil.java similarity index 96% rename from src/main/java/envoy/server/util/VersionUtils.java rename to src/main/java/envoy/server/util/VersionUtil.java index db4a845..8f623e1 100644 --- a/src/main/java/envoy/server/util/VersionUtils.java +++ b/src/main/java/envoy/server/util/VersionUtil.java @@ -7,13 +7,13 @@ import java.util.regex.Pattern; * and maximal client versions compatible with this server. *

* Project: envoy-server-standalone
- * File: VersionUtils.java
+ * File: VersionUtil.java
* Created: 23.06.2020
* * @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("(?\\d).(?\\d)(?:-(?\\w+))?"); - private VersionUtils() {} + private VersionUtil() {} /** * Parses an Envoy Client version string and checks whether that version is