add PostgreSQLScramSHA256 password encoding (working, but needs cleanup)

This commit is contained in:
Michael Hoennig 2024-07-12 17:32:20 +02:00
parent 46fce275ae
commit 29994a851a
3 changed files with 96 additions and 2 deletions

View File

@ -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<HashGenerator, String, String> implementation;
final String prefix;

View File

@ -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);
}
}

View File

@ -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);
}
}