diff --git a/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java b/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java index 345f0ed0..c5a2cd3c 100644 --- a/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java @@ -31,7 +31,8 @@ public final class HashGenerator { public enum Algorithm { LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"), LINUX_YESCRYPT(LinuxEtcShadowHashGenerator::hash, "y"), - MYSQL_NATIVE(MySQLNativePasswordHashGenerator::hash, "*"); + MYSQL_NATIVE(MySQLNativePasswordHashGenerator::hash, "*"), + SCRAM_SHA256(PostgreSQLScramSHA256::hash, "*"); final BiFunction implementation; final String prefix; diff --git a/src/main/java/net/hostsharing/hsadminng/hash/PostgreSQLScramSHA256.java b/src/main/java/net/hostsharing/hsadminng/hash/PostgreSQLScramSHA256.java new file mode 100644 index 00000000..500909f1 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hash/PostgreSQLScramSHA256.java @@ -0,0 +1,61 @@ +package net.hostsharing.hsadminng.hash; + +import lombok.SneakyThrows; + +import javax.crypto.Mac; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.Charset; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.Base64; + +public class PostgreSQLScramSHA256 { + + private static final String PBKDF_2_WITH_HMAC_SHA256 = "PBKDF2WithHmacSHA256"; + private static final String HMAC_SHA256 = "HmacSHA256"; + private static final String SHA256 = "SHA-256"; + private static final int ITERATIONS = 4096; + public static final int KEY_LENGTH_IN_BITS = 256; + + private static final PostgreSQLScramSHA256 scram = new PostgreSQLScramSHA256(); + + @SneakyThrows + public static String hash(final HashGenerator generator, final String password) { + if (generator.getSalt() == null) { + throw new IllegalStateException("no salt given"); + } + + final byte[] salt = generator.getSalt().getBytes(Charset.forName("latin1")); // Base64.getEncoder().encode(generator.getSalt().getBytes()); + final byte[] saltedPassword = scram.generateSaltedPassword(password, salt); + final byte[] clientKey = scram.hmacSHA256(saltedPassword, "Client Key".getBytes()); + final byte[] storedKey = MessageDigest.getInstance(SHA256).digest(clientKey); + final byte[] serverKey = scram.hmacSHA256(saltedPassword, "Server Key".getBytes()); + + return "SCRAM-SHA-256${iterations}:{base64EncodedSalt}${base64EncodedStoredKey}:{base64EncodedServerKey}" + .replace("{iterations}", Integer.toString(ITERATIONS)) + .replace("{base64EncodedSalt}", base64(salt)) + .replace("{base64EncodedStoredKey}", base64(storedKey)) + .replace("{base64EncodedServerKey}", base64(serverKey)); + } + + private static String base64(final byte[] salt) { + return Base64.getEncoder().encodeToString(salt); + } + + private byte[] generateSaltedPassword(String password, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException { + final var spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, KEY_LENGTH_IN_BITS); + return SecretKeyFactory.getInstance(PBKDF_2_WITH_HMAC_SHA256).generateSecret(spec).getEncoded(); + } + + private byte[] hmacSHA256(byte[] key, byte[] message) + throws NoSuchAlgorithmException, InvalidKeyException { + final var mac = Mac.getInstance(HMAC_SHA256); + mac.init(new SecretKeySpec(key, HMAC_SHA256)); + return mac.doFinal(message); + } + +} diff --git a/src/test/java/net/hostsharing/hsadminng/hash/HashGeneratorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hash/HashGeneratorUnitTest.java index 6c70bc8e..13207eb6 100644 --- a/src/test/java/net/hostsharing/hsadminng/hash/HashGeneratorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hash/HashGeneratorUnitTest.java @@ -2,8 +2,12 @@ package net.hostsharing.hsadminng.hash; import org.junit.jupiter.api.Test; +import java.nio.charset.Charset; +import java.util.Base64; + import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.LINUX_SHA512; import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.MYSQL_NATIVE; +import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.SCRAM_SHA256; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; @@ -49,8 +53,36 @@ class HashGeneratorUnitTest { } @Test - void verifiesMySqlNativePassword() { + void generatesMySqlNativePasswordHash() { final var hash = HashGenerator.using(MYSQL_NATIVE).hash("Test1234"); assertThat(hash).isEqualTo("*14F1A8C42F8B6D4662BB3ED290FD37BF135FE45C"); } + + @Test + void generatePostgreSqlScramPasswordHash() { + //final var salt = new String(Base64.getDecoder().decode("3gsrkV5e1VZweIFiKfvoeQ==")); + final var postgresBase64Salt = Base64.getDecoder().decode("3gsrkV5e1VZweIFiKfvoeQ=="); + final var reEncodedSalt = Base64.getEncoder().encodeToString(postgresBase64Salt); + final var hash = HashGenerator.using(SCRAM_SHA256).withSalt(new String(postgresBase64Salt, Charset.forName("latin1"))).hash("Test1234"); + + assertThat(hash).isEqualTo( + "SCRAM-SHA-256$4096:3gsrkV5e1VZweIFiKfvoeQ==$/8I29AMTJ+7W9ceeKhc5LsfTrTHF6/+m/qstv2h0kpo=:+MDwFXgAjHHSnlqgU5adOPtW0qpbFUMrYp59Xs7ns0U="); + } + + // ALTER USER your_username WITH PASSWORD 'SCRAM-SHA-256$iterations:base64-encoded-salt$base64-encoded-stored-key$base64-encoded-server-key'; + + @Test + public void xxx() { + String postgresqlBase64Salt = "3gsrkV5e1VZweIFiKfvoeQ=="; // Example Base64 encoded salt from PostgreSQL + + // Decode the base64 salt using the standard Base64 decoder + byte[] decodedSalt = Base64.getDecoder().decode(postgresqlBase64Salt); + + // Re-encode the salt using the standard Base64 encoder + String javaBase64Salt = Base64.getEncoder().encodeToString(decodedSalt); + + // Print both the original and re-encoded salts + System.out.println("Orig PostgreSQL Base64 Salt: " + postgresqlBase64Salt); + System.out.println("Re-encoded Java Base64 Salt: " + javaBase64Salt); + } }